Location: PHPKode > scripts > DumbledORM > jasonmoo-DumbledORM-9d6f9d5/dumbledorm.php
<?php
/**
 *
 *  DumbledORM
 *  
 *  @version 0.1.1
 *  @author Jason Mooberry <hide@address.com>
 *  @link http://github.com/jasonmoo/DumbledORM
 *  @package DumbledORM
 * 
 *  DumbledORM is a novelty PHP ORM
 * 
 *  Copyright (c) 2010 Jason Mooberry
 * 
 *  Permission is hereby granted, free of charge, to any person obtaining a copy
 *  of this software and associated documentation files (the "Software"), to deal
 *  in the Software without restriction, including without limitation the rights
 *  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 *  copies of the Software, and to permit persons to whom the Software is furnished
 *  to do so, subject to the following conditions:
 * 
 *  The above copyright notice and this permission notice shall be included in all
 *  copies or substantial portions of the Software.
 * 
 *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 *  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 *  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 *  THE SOFTWARE.
 *
 */

/**
 * exceptional moments defined here
 */
class RecordNotFoundException extends Exception {}

/**
 * Class for denoting sql that should be inserted into the query directly without escaping
 *
 */
final class PlainSql {
  private $_sql;
  public function __construct($sql) { $this->_sql = $sql; }
  public function __toString() { return $this->_sql; }
}

/**
 * Builder class for required for generating base classes
 *
 */
abstract class Builder {
  
  /**
   * simple cameCasing method
   *
   * @param string $string 
   * @return string
   */
  public static function camelCase($string)	{
    return ucfirst(preg_replace("/_(\w)/e","strtoupper('\\1')",strtolower($string)));
  }
  
  /**
   * simple un_camel_casing method
   *
   * @param string $string 
   * @return string
   */
  public static function unCamelCase($string)	{
    return strtolower(preg_replace("/(\w)([A-Z])/","\\1_\\2",$string));
  }
  
  /**
   * re/generates base classes for db schema
   *
   * @param string $prefix 
   * @param string $dir 
   * @return void
   */
  public static function generateBase($prefix=null,$dir='model') {
    $tables = array();
    foreach (Db::query('show tables',null,PDO::FETCH_NUM) as $row) {
      foreach (Db::query('show columns from `'.$row[0].'`') as $col) {
        if ($col['Key'] === 'PRI') {
          $tables[$row[0]]['pk'] = $col['Field']; break;
        }
      }
    }
    foreach (array_keys($tables) as $table) {
      foreach (Db::query('show columns from `'.$table.'`') as $col) {
        if (substr($col['Field'],-3,3) === '_id') {
          $rel = substr($col['Field'],0,-3);
          if (array_key_exists($rel,$tables)) {
            if ($table === "{$rel}_meta") {
              $tables[$rel]['meta']['class'] = self::camelCase($table);
              $tables[$rel]['meta']['field'] = $col['Field'];
            }
            $tables[$table]['relations'][$rel] = array('fk' => 'id', 'lk' => $col['Field']);
            $tables[$rel]['relations'][$table] = array('fk' => $col['Field'], 'lk' => 'id');
          }
        }
      }
    }    
    $basetables = "<?php\nspl_autoload_register(function(\$class) { @include(__DIR__.\"/\$class.class.php\"); });\n";
    foreach ($tables as $table => $conf) {
      $relations = preg_replace('/[\n\t\s]+/','',var_export((array)@$conf['relations'],true));
      $meta = isset($conf['meta']) ? "\$meta_class = '{$conf['meta']['class']}', \$meta_field = '{$conf['meta']['field']}'," : '';
      $basetables .= "class ".$prefix.self::camelCase($table)."Base extends BaseTable { protected static \$table = '$table', \$pk = '{$conf['pk']}', $meta \$relations = $relations; }\n";
    }
    @mkdir("./$dir",0777,true);
    file_put_contents("./$dir/base.php",$basetables);
    foreach (array_keys($tables) as $table) {
      $file = "./$dir/$prefix".self::camelCase($table).'.class.php';
      if (!file_exists($file)) {
        file_put_contents($file,"<?php\nclass ".$prefix.self::camelCase($table).' extends '.$prefix.self::camelCase($table).'Base {}');
      }
    }
  }
  
}

/**
 * thin wrapper for PDO access 
 *
 */
abstract class Db {
  
  /**
   * singleton variable for PDO connection
   *
   */
  private static $_pdo;
  
  /**
   * singleton getter for PDO connection
   *
   * @return PDO 
   */
  public static function pdo() {
    if (!self::$_pdo) {
      self::$_pdo = new PDO('mysql:host='.DbConfig::HOST.';port='.DbConfig::PORT.';dbname='.DbConfig::DBNAME, DbConfig::USER, DbConfig::PASSWORD);
      self::$_pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    }
    return self::$_pdo;
  }
  
  /**
   * execute sql as a prepared statement
   *
   * @param string $sql 
   * @param mixed $params 
   * @return PDOStatement
   */
  public static function execute($sql,$params=null) {
    
    $params = is_array($params) ? $params : array($params);

    if ($params) {
      // using preg_replace_callback ensures that any inserted PlainSql
      // with ?'s in it will not be confused for replacement markers
      $sql = preg_replace_callback('/\?/',function($a) use (&$params) {
        $a = array_shift($params);
        if ($a instanceof PlainSql) {
          return $a;
        }
        $params[] = $a;
        return '?';
      },$sql);
    }
    $stmt = self::pdo()->prepare($sql);
    $stmt->execute($params);
    return $stmt;
  }
  
  /**
   * execute sql as a prepared statement and return all records
   *
   * @param string $query 
   * @param mixed $params 
   * @param PDO constant $fetch_style 
   * @return Array
   */
  public static function query($query,$params=null,$fetch_style=PDO::FETCH_ASSOC) {
    return self::execute($query,$params)->fetchAll($fetch_style);
  }
  
  /**
   * run a query and return the results as a ResultSet of BaseTable objects
   *
   * @param BaseTable $obj 
   * @param string $query 
   * @param mixed $params 
   * @return ResultSet
   */
  public static function hydrate(BaseTable $obj,$query,$params=null) {
    $set = array();
    foreach (self::query($query,$params) as $record) {
      $clone = clone $obj;
      $clone->hydrate($record);
      $set[$clone->getId()] = $clone;
    }
    return new ResultSet($set);
  }
  
}

/**
 * class to manage result array more effectively
 *
 */
final class ResultSet extends ArrayIterator {

  /**
   * magic method for applying called methods to all members of result set
   *
   * @param string $method 
   * @param Array $params 
   * @return $this
   */
  public function __call($method,$params=array()) {
    foreach ($this as $obj) {
      call_user_func_array(array($obj,$method),$params);
    }
    return $this;
  }
  
}

/**
 * base functionality available to all objects extending from a generated base class
 *
 */
abstract class BaseTable {
  
  protected static 
    /**
     * table name
     */
    $table, 
    /**
     * primary key
     */
    $pk, 
    /**
     * table relations array
     */
    $relations, 
    /**
     * metadata class name
     */
    $meta_class, 
    /**
     * metadata field
     */
    $meta_field;
    
  protected 
    /**
     * record data array
     */
    $data, 
    /**
     * metadata array
     */
    $meta, 
    /**
     * relation data array
     */
    $relation_data, 
    /**
     * record primary key value
     */
    $id, 
    /**
     * array of data fields that have changed since hydration
     */
    $changed;
  
  /**
   * search for single record in self::$table
   *
   * @param Array $constraints 
   * @return BaseTable
   */
  final public static function one(Array $constraints) {
    return self::select('`'.implode('` = ? and `',array_keys($constraints)).'` = ? limit 1',array_values($constraints))->current();
  }
  
  /**
   * search for any number of records in self::$table
   *
   * @param Array $constraints 
   * @return ResultSet
   */
  final public static function find(Array $constraints) {
    return self::select('`'.implode('` = ? and `',array_keys($constraints)).'` = ?',array_values($constraints));
  }
  
  /**
   * execute a query in self::$table
   *
   * @param string $qs 
   * @param mixed $params 
   * @return ResultSet
   */
  final public static function select($qs,$params=null) {
    return Db::hydrate(new static,'select * from `'.static::$table.'` where '.$qs,$params);    
  }
   
  /**
   * construct object and load supplied data or fetch data by supplied id
   *
   * @param mixed $val 
   */   
  public function __construct($val=null) {
    if (is_array($val)) {
      $this->data = $val;
      $this->changed = array_flip(array_keys($this->data));
      $this->_loadMeta();
    } else if (is_numeric($val)) {
      if (!$obj = self::one(array(static::$pk => $val))) {
        throw new RecordNotFoundException("Nothing to be found with id $val");
      }
      $this->hydrate($obj->toArray());
    }
  }
  
  /**
   * most of the magic in here makes it all work
   * - handles all getters and setters on columns and relations
   *
   * @param string $method 
   * @param Array $params 
   * @return mixed
   */
  final public function __call($method,$params=array()) {
    $name = Builder::unCamelCase(substr($method,3,strlen($method)));
    if (strpos($method,'get')===0) {
      if (array_key_exists($name,$this->data)) {
        return $this->data[$name];
      }
      if (isset(static::$relations[$name])) {
        $class = substr($method,3,strlen($method));
        if (count($params)) {
          if ($params[0] === true) { 
            return @$this->relation_data[$name.'_all'] ?: $this->relation_data[$name.'_all'] = $class::find(array(static::$relations[$name]['fk'] => $this->getId()));
          }
          $qparams = array_merge(array($this->getId()),(array)@$params[1]);
          $qk = md5(serialize(array($name,$params[0],$qparams)));
          return @$this->relation_data[$qk] ?: $this->relation_data[$qk] = $class::select('`'.static::$relations[$name]['fk'].'` = ? and '.$params[0],$qparams);
        }
        return @$this->relation_data[$name] ?: $this->relation_data[$name] = $class::one(array(static::$relations[$name]['fk'] => $this->getId()));
      }
    }
    else if (strpos($method,'set')===0) {
      $this->changed[$name] = true;
      $this->data[$name] = array_shift($params);
      return $this;
    }
    throw new BadMethodCallException("No amount of magic can make $method work..");
  }
  
  /**
   * simple output object data as array
   *
   * @return Array
   */
  final public function toArray() {
    return $this->data;
  }
  
  /**
   * simple output object pk id
   *
   * @return integer
   */
  final public function getId() {
    return $this->id;
  }
  
  /**
   * store supplied data and bring object state to current
   *
   * @param Array $data 
   * @return $this
   */
  final public function hydrate(Array $data) {
    $this->id = $data[static::$pk];
    $this->data = $data;
    $this->_loadMeta();
    $this->changed = array();
    return $this;
  }
  
  /**
   * create an object with a defined relation to this one.
   *
   * @param BaseTable $obj 
   * @return BaseTable
   */
  final public function create(BaseTable $obj) {
    return $obj->{'set'.Builder::camelCase(static::$relations[Builder::unCamelCase(get_class($obj))]['fk'])}($this->id);
  }
  
  /**
   * insert or update modified object data into self::$table and any associated metadata
   *
   * @return void
   */
  public function save() {
    if (empty($this->changed)) return;
    $data = array_intersect_key($this->data,$this->changed);
    
    // use proper sql NULL for values set to php null
    foreach ($data as $key => $value) {
      if ($value === null) {
        $data[$key] = new PlainSql('NULL');
      }
    }
    
    if ($this->id) {
      $query = 'update `'.static::$table.'` set `'.implode('` = ?, `',array_keys($data)).'` = ? where `'.static::$pk.'` = '.$this->id.' limit 1';
    }
    else {
      $query = 'insert into `'.static::$table.'` (`'.implode('`,`',array_keys($data))."`) values (".rtrim(str_repeat('?,',count($data)),',').")";
    }
    Db::execute($query,array_values($data));
    if ($this->id === null) {
      $this->id = Db::pdo()->lastInsertId();
    }
    $this->meta->{'set'.Builder::camelCase(static::$meta_field)}($this->id)->save();
    $this->hydrate(self::one(array(static::$pk => $this->id))->toArray());
  }
  
  /**
   * delete this object's record from self::$table and any associated meta data
   *
   * @return void
   */
  public function delete() {
    Db::execute('delete from `'.static::$table.'` where `'.static::$pk.'` = ? limit 1',$this->getId());
    $this->meta->delete();
  }
  
  /**
   * add an array of key/val to the metadata 
   *
   * @param Array $data 
   * @return $this
   */
  public function addMeta(Array $data) {
    foreach ($data as $field => $val) {
      $this->setMeta($field,$val);
    }
    return $this;
  }
  
  /**
   * set a field of metadata
   *
   * @param string $field 
   * @param string $val 
   * @return $this
   */
  public function setMeta($field,$val) {
    if (empty($this->meta[$field])) {
      $meta_class = static::$meta_class;
      $this->meta[$field] = new $meta_class(array('key' => $field,'val' => $val));
    }
    else {
      $this->meta[$field]->setVal($val);
    }
    return $this;
  }
  
  /**
   * get a field of metadata
   *
   * @param string $field 
   * @return mixed
   */
  public function getMeta($field) {
    return isset($this->meta[$field]) ? $this->meta[$field]->getVal() : null;
  }
  
  /**
   * internally fetch and load any associated metadata
   *
   * @return void
   */
  private function _loadMeta() {
    if (!$meta_class = static::$meta_class) {
      return $this->meta = new ResultSet;
    }
    foreach ($meta_class::find(array(static::$meta_field => $this->getId())) as $obj) {
      $meta[$obj->getKey()] = $obj;
    }
    $this->meta = new ResultSet((array)@$meta);
  }

}
Return current item: DumbledORM