/dumbledorm.php
PHP | 504 lines | 230 code | 40 blank | 234 comment | 27 complexity | 4922c7eb11597040e8aed8c0be31e154 MD5 | raw file
- <?php
- /**
- *
- * DumbledORM
- *
- * @version 0.1.1
- * @author Jason Mooberry <jasonmoo@me.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);
- }
- }