├── ModelStub.tpl ├── ModelBase.tpl ├── config.php ├── db_adapters ├── PDO.php └── MySQL.php ├── Zend ├── Exception.php ├── Json │ ├── Exception.php │ ├── Encoder.php │ └── Decoder.php └── Json.php ├── Association.php ├── LICENSE.txt ├── BelongsTo.php ├── HasOne.php ├── generate.php ├── inflector.php ├── HasMany.php ├── README.md └── ActiveRecord.php /ModelStub.tpl: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /ModelBase.tpl: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /config.php: -------------------------------------------------------------------------------- 1 | 24 | -------------------------------------------------------------------------------- /db_adapters/PDO.php: -------------------------------------------------------------------------------- 1 | setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); 8 | return $dbh; 9 | } 10 | 11 | static function query($query, $dbh=null) { 12 | return $dbh->query($query, PDO::FETCH_ASSOC); 13 | } 14 | 15 | static function quote($string, $dbh=null, $type=null) { 16 | return $dbh->quote($string, $type); 17 | } 18 | 19 | static function last_insert_id($dbh=null, $resource=null) { 20 | return $dbh->lastInsertId($resource); 21 | } 22 | 23 | } 24 | 25 | ?> 26 | -------------------------------------------------------------------------------- /Zend/Exception.php: -------------------------------------------------------------------------------- 1 | 41 | -------------------------------------------------------------------------------- /Association.php: -------------------------------------------------------------------------------- 1 | source_class = get_class($source); 10 | 11 | if (isset($options['class_name'])) { 12 | $this->dest_class = $options['class_name']; 13 | } 14 | else { 15 | $this->dest_class = ActiveRecordInflector::classify($dest); 16 | } 17 | 18 | if (isset($options['foreign_key'])) { 19 | $this->foreign_key = $options['foreign_key']; 20 | } 21 | else { 22 | $this->foreign_key = ActiveRecordInflector::foreign_key($this->source_class); 23 | } 24 | 25 | $this->options = $options; 26 | } 27 | 28 | function needs_saving() { 29 | if (!$this->value instanceof $this->dest_class) 30 | return false; 31 | else 32 | return $this->value->is_new_record() || $this->value->is_modified(); 33 | } 34 | 35 | function destroy(&$source) { 36 | if (isset($this->options['dependent']) && $this->options['dependent'] == 'destroy') { 37 | $this->get($source); 38 | if (is_array($this->value)) { 39 | foreach ($this->value as $val) 40 | $val->destroy(); 41 | } 42 | else { 43 | $this->value->destroy(); 44 | } 45 | } 46 | } 47 | 48 | } 49 | 50 | ?> 51 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2007, Luke Baker 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | * Neither the name of Luke Baker nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 10 | 11 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 12 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 13 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 14 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 15 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 16 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 17 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 18 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 19 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 20 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 21 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 22 | -------------------------------------------------------------------------------- /BelongsTo.php: -------------------------------------------------------------------------------- 1 | foreign_key = ActiveRecordInflector::foreign_key($this->dest_class); 7 | } 8 | } 9 | 10 | function set($value, &$source) { 11 | if ($value instanceof $this->dest_class) { 12 | if (!$value->is_new_record()) 13 | $source->{$this->foreign_key} = $value->{$value->get_primary_key()}; 14 | else 15 | $source->{$this->foreign_key} = null; 16 | $this->value = $value; 17 | } 18 | else { 19 | throw new ActiveRecordException("Did not get expected class: {$this->dest_class}", ActiveRecordException::UnexpectedClass); 20 | } 21 | } 22 | 23 | function get(&$source, $force=false) { 24 | if ($this->value instanceof $this->dest_class && !$force) { 25 | return $this->value; 26 | } 27 | else { 28 | $this->value = call_user_func_array( 29 | array($this->dest_class, 'find'), 30 | array($source->{$this->foreign_key}) ); 31 | return $this->value; 32 | } 33 | } 34 | 35 | function join() { 36 | $dest_table = ActiveRecordInflector::tableize($this->dest_class); 37 | $source_table = ActiveRecordInflector::tableize($this->source_class); 38 | $dest_inst = new $this->dest_class; 39 | $columns = $dest_inst->get_columns(); 40 | $join = "LEFT OUTER JOIN {$dest_table} ON " 41 | . "$source_table.{$this->foreign_key} = $dest_table.".$dest_inst->get_primary_key(); 42 | return array( array($dest_table => $columns), $join); 43 | } 44 | function populate_from_find($attributes) { 45 | // check if all attributes are NULL 46 | $uniq_vals = array_unique(array_values($attributes)); 47 | if (count($uniq_vals) == 1 && is_null(current($uniq_vals))) return; 48 | 49 | $class = $this->dest_class; 50 | $item = new $class($attributes); 51 | $item->new_record = false; 52 | $this->value = $item; 53 | } 54 | 55 | } 56 | ?> 57 | -------------------------------------------------------------------------------- /HasOne.php: -------------------------------------------------------------------------------- 1 | dest_class) { 9 | if (!$source->is_new_record()) { 10 | $value->{$this->foreign_key} = $source->{$source->get_primary_key()}; 11 | $value->save(); 12 | } 13 | else { 14 | $value->{$this->foreign_key} = null; 15 | } 16 | $this->value = $value; 17 | } 18 | else { 19 | throw new ActiveRecordException("Did not get expected class: {$this->dest_class}", ActiveRecordException::UnexpectedClass); 20 | } 21 | } 22 | 23 | function get(&$source, $force=false) { 24 | if (!($this->value instanceof $this->dest_class) || $force) { 25 | if ($source->is_new_record()) { return null; } 26 | $this->value = call_user_func_array( 27 | array($this->dest_class, 'find'), 28 | array('first', 29 | array('conditions' => "{$this->foreign_key} = {$source->{$source->get_primary_key()}}") 30 | )); 31 | } 32 | return $this->value; 33 | } 34 | 35 | function join() { 36 | $dest_table = ActiveRecordInflector::tableize($this->dest_class); 37 | $source_table = ActiveRecordInflector::tableize($this->source_class); 38 | $source_inst = new $this->source_class; 39 | $dest_inst = new $this->dest_class; 40 | $columns = $dest_inst->get_columns(); 41 | $join = "LEFT OUTER JOIN {$dest_table} ON " 42 | . "$source_table.".$source_inst->get_primary_key() ." = $dest_table.{$this->foreign_key}"; 43 | return array( array($dest_table => $columns), $join); 44 | } 45 | function populate_from_find($attributes) { 46 | // check if all attributes are NULL 47 | $uniq_vals = array_unique(array_values($attributes)); 48 | if (count($uniq_vals) == 1 && is_null(current($uniq_vals))) return; 49 | 50 | $class = $this->dest_class; 51 | $item = new $class($attributes); 52 | $item->new_record = false; 53 | $this->value = $item; 54 | } 55 | 56 | } 57 | ?> 58 | -------------------------------------------------------------------------------- /generate.php: -------------------------------------------------------------------------------- 1 | 54 | -------------------------------------------------------------------------------- /Zend/Json.php: -------------------------------------------------------------------------------- 1 | $replacement) { 20 | if (preg_match($rule, $result)) { 21 | $result = preg_replace($rule, $replacement, $result); 22 | break; 23 | } 24 | } 25 | 26 | return $result; 27 | } 28 | } 29 | 30 | function singularize($word) { 31 | $result = strval($word); 32 | 33 | if (in_array(strtolower($result), self::uncountable_words())) { 34 | return $result; 35 | } else { 36 | foreach(self::singular_rules() as $rule => $replacement) { 37 | if (preg_match($rule, $result)) { 38 | $result = preg_replace($rule, $replacement, $result); 39 | break; 40 | } 41 | } 42 | 43 | return $result; 44 | } 45 | } 46 | 47 | function camelize($lower_case_and_underscored_word) { 48 | return preg_replace('/(^|_)(.)/e', "strtoupper('\\2')", strval($lower_case_and_underscored_word)); 49 | } 50 | 51 | function underscore($camel_cased_word) { 52 | return strtolower(preg_replace('/([A-Z]+)([A-Z])/','\1_\2', preg_replace('/([a-z\d])([A-Z])/','\1_\2', strval($camel_cased_word)))); 53 | } 54 | 55 | function humanize($lower_case_and_underscored_word) { 56 | return ucfirst(strtolower(ereg_replace('_', " ", strval($lower_case_and_underscored_word)))); 57 | } 58 | 59 | function demodulize($class_name_in_module) { 60 | return preg_replace('/^.*::/', '', strval($class_name_in_module)); 61 | } 62 | 63 | function tableize($class_name) { 64 | return self::pluralize(self::underscore($class_name)); 65 | } 66 | 67 | function classify($table_name) { 68 | return self::camelize(self::singularize($table_name)); 69 | } 70 | 71 | function foreign_key($class_name, $separate_class_name_and_id_with_underscore = true) { 72 | return self::underscore(self::demodulize($class_name)) . 73 | ($separate_class_name_and_id_with_underscore ? "_id" : "id"); 74 | } 75 | 76 | function constantize($camel_cased_word=NULL) { 77 | } 78 | 79 | function uncountable_words() { #:doc 80 | return array( 'equipment', 'information', 'rice', 'money', 'species', 'series', 'fish' ); 81 | } 82 | 83 | function plural_rules() { #:doc: 84 | return array( 85 | '/^(ox)$/' => '\1\2en', # ox 86 | '/([m|l])ouse$/' => '\1ice', # mouse, louse 87 | '/(matr|vert|ind)ix|ex$/' => '\1ices', # matrix, vertex, index 88 | '/(x|ch|ss|sh)$/' => '\1es', # search, switch, fix, box, process, address 89 | #'/([^aeiouy]|qu)ies$/' => '\1y', -- seems to be a bug(?) 90 | '/([^aeiouy]|qu)y$/' => '\1ies', # query, ability, agency 91 | '/(hive)$/' => '\1s', # archive, hive 92 | '/(?:([^f])fe|([lr])f)$/' => '\1\2ves', # half, safe, wife 93 | '/sis$/' => 'ses', # basis, diagnosis 94 | '/([ti])um$/' => '\1a', # datum, medium 95 | '/(p)erson$/' => '\1eople', # person, salesperson 96 | '/(m)an$/' => '\1en', # man, woman, spokesman 97 | '/(c)hild$/' => '\1hildren', # child 98 | '/(buffal|tomat)o$/' => '\1\2oes', # buffalo, tomato 99 | '/(bu)s$/' => '\1\2ses', # bus 100 | '/(alias|status)/' => '\1es', # alias 101 | '/(octop|vir)us$/' => '\1i', # octopus, virus - virus has no defined plural (according to Latin/dictionary.com), but viri is better than viruses/viruss 102 | '/(ax|cri|test)is$/' => '\1es', # axis, crisis 103 | '/s$/' => 's', # no change (compatibility) 104 | '/$/' => 's' 105 | ); 106 | } 107 | 108 | function singular_rules() { #:doc: 109 | return array( 110 | '/(matr)ices$/' =>'\1ix', 111 | '/(vert|ind)ices$/' => '\1ex', 112 | '/^(ox)en/' => '\1', 113 | '/(alias)es$/' => '\1', 114 | '/([octop|vir])i$/' => '\1us', 115 | '/(cris|ax|test)es$/' => '\1is', 116 | '/(shoe)s$/' => '\1', 117 | '/(o)es$/' => '\1', 118 | '/(bus)es$/' => '\1', 119 | '/([m|l])ice$/' => '\1ouse', 120 | '/(x|ch|ss|sh)es$/' => '\1', 121 | '/(m)ovies$/' => '\1\2ovie', 122 | '/(s)eries$/' => '\1\2eries', 123 | '/([^aeiouy]|qu)ies$/' => '\1y', 124 | '/([lr])ves$/' => '\1f', 125 | '/(tive)s$/' => '\1', 126 | '/(hive)s$/' => '\1', 127 | '/([^f])ves$/' => '\1fe', 128 | '/(^analy)ses$/' => '\1sis', 129 | '/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/' => '\1\2sis', 130 | '/([ti])a$/' => '\1um', 131 | '/(p)eople$/' => '\1\2erson', 132 | '/(m)en$/' => '\1an', 133 | '/(s)tatuses$/' => '\1\2tatus', 134 | '/(c)hildren$/' => '\1\2hild', 135 | '/(n)ews$/' => '\1\2ews', 136 | '/s$/' => '' 137 | ); 138 | } 139 | } 140 | 141 | ?> 142 | -------------------------------------------------------------------------------- /HasMany.php: -------------------------------------------------------------------------------- 1 | is_new_record() || $object->is_new_record()) 10 | && isset($this->options['through']) && $this->options['through']) 11 | throw new ActiveRecordException("HasManyThroughCantAssociateNewRecords", ActiveRecordException::HasManyThroughCantAssociateNewRecords); 12 | if (!$object instanceof $this->dest_class) { 13 | throw new ActiveRecordException("Expected class: {$this->dest_class}; Received: ".get_class($object), ActiveRecordException::UnexpectedClass); 14 | } 15 | if ($source->is_new_record()) { 16 | /* we want to save $object after $source gets saved */ 17 | $object->set_modified(true); 18 | } 19 | elseif (!isset($this->options['through']) || !$this->options['through']) { 20 | /* since source exists, we always want to save $object */ 21 | $object->{$this->foreign_key} = $source->{$source->get_primary_key()}; 22 | $this->get($source); 23 | $object->save(); 24 | } 25 | elseif ($this->options['through']) { 26 | /* $object and $source are guaranteed to exist in the DB */ 27 | $this->get($source); 28 | $skip = false; 29 | foreach ($this->value as $val) 30 | if ($val == $object) $skip = true; 31 | if (!$skip) { 32 | $through_class = ActiveRecordInflector::classify($this->options['through']); 33 | $fk_1 = ActiveRecordInflector::foreign_key($this->dest_class); 34 | $fk_2 = ActiveRecordInflector::foreign_key($this->source_class); 35 | $k1 = $object->{$object->get_primary_key()}; 36 | $k2 = $source->{$source->get_primary_key()}; 37 | $through = new $through_class( array($fk_1 => $k1, $fk_2 => $k2) ); 38 | $through->save(); 39 | } 40 | } 41 | $this->get($source); 42 | array_push($this->value, $object); 43 | } 44 | } 45 | 46 | function get(&$source, $force=false) { 47 | if (!is_array($this->value) || $force) { 48 | if ($source->is_new_record()) { 49 | $this->value = array(); 50 | return $this->value; 51 | } 52 | try { 53 | if (!isset($this->options['through']) || !$this->options['through']) { 54 | $collection = call_user_func_array(array($this->dest_class, 'find'), 55 | array('all', 56 | array('conditions' => "{$this->foreign_key} = ".$source->{$source->get_primary_key()}))); 57 | } 58 | else { 59 | // TODO: $this->options['through'] is not necessarily the table name 60 | $collection = call_user_func_array(array($this->dest_class, 'find'), 61 | array('all', 62 | array('include' => $this->options['through'], 63 | 'conditions' => "{$this->options['through']}.{$this->foreign_key} = ".$source->{$source->get_primary_key()}))); 64 | } 65 | } catch (ActiveRecordExeception $e) { 66 | } 67 | $collection = is_null($collection) ? array() : $collection; 68 | $this->value = $collection; 69 | } 70 | return $this->value; 71 | } 72 | 73 | function get_ids(&$source, $force='false') { 74 | $ids = array(); 75 | $objects = $this->get($source, $force); 76 | foreach ($objects as $object) 77 | $ids[] = $object->{$object->get_primary_key()}; 78 | return $ids; 79 | } 80 | 81 | function set_ids($ids, &$source) { 82 | /* get existing objects in relationship (force=true, don't use cache) */ 83 | $objects = $this->get($source, true); 84 | $existing_ids = $this->get_ids($source, false); 85 | $ids_to_add = array_diff($ids, $existing_ids); 86 | $ids_to_remove = array_diff($existing_ids, $ids); 87 | 88 | /* add relationships that need adding */ 89 | if (count($ids_to_add) > 0) { 90 | $objects_to_add = call_user_func_array(array($this->dest_class, 'find'), 91 | array($ids_to_add)); 92 | $this->push($objects_to_add, $source); 93 | } 94 | 95 | /* remove relationships that need removing */ 96 | if (count($ids_to_remove) > 0) { 97 | $objects_to_rem = call_user_func_array(array($this->dest_class, 'find'), 98 | array($ids_to_remove)); 99 | $this->break_up($objects_to_rem, $source); 100 | } 101 | } 102 | 103 | /* break up the relationship 104 | $objects = array of $objects that are related but should no longer be 105 | $source = source object that we're working with 106 | */ 107 | function break_up($objects, &$source) { 108 | foreach ($objects as $object) { 109 | if (isset($this->options['dependent']) && $this->options['dependent'] == 'destroy') 110 | $object->destroy(); 111 | else { 112 | if (!$this->options['through']) { 113 | $object->{$this->foreign_key} = null; 114 | $object->save(); 115 | } 116 | else { 117 | $through_class = ActiveRecordInflector::classify($this->options['through']); 118 | $fk_1 = ActiveRecordInflector::foreign_key($this->dest_class); 119 | $fk_2 = ActiveRecordInflector::foreign_key($this->source_class); 120 | $k1 = $object->{$object->get_primary_key()}; 121 | $k2 = $source->{$source->get_primary_key()}; 122 | $through = call_user_func_array(array($through_class, 'find'), 123 | array('first', 124 | array('conditions' => "$fk_1 = $k1 AND $fk_2 = $k2"))); 125 | $through->destroy(); 126 | } 127 | } 128 | } 129 | } 130 | 131 | function join() { 132 | $dest_table = ActiveRecordInflector::tableize($this->dest_class); 133 | $source_table = ActiveRecordInflector::tableize($this->source_class); 134 | $source_inst = new $this->source_class; 135 | $dest_inst = new $this->dest_class; 136 | $columns = $dest_inst->get_columns(); 137 | if (!isset($this->options['through']) || !$this->options['through']) { 138 | $join = "LEFT OUTER JOIN $dest_table ON " 139 | . "$dest_table.{$this->foreign_key} = $source_table.".$source_inst->get_primary_key(); 140 | } 141 | else { 142 | $join = "LEFT OUTER JOIN {$this->options['through']} ON " 143 | . "{$this->options['through']}.{$this->foreign_key} = $source_table.".$source_inst->get_primary_key() ." " 144 | . "LEFT OUTER JOIN $dest_table ON " 145 | . "$dest_table.".$dest_inst->get_primary_key() ." = {$this->options['through']}." . ActiveRecordInflector::foreign_key($this->dest_class); 146 | } 147 | return array( array($dest_table => $columns), $join); 148 | } 149 | 150 | function populate_from_find($attributes) { 151 | // check if all attributes are NULL 152 | $uniq_vals = array_unique(array_values($attributes)); 153 | if (count($uniq_vals) == 1 && is_null(current($uniq_vals))) return; 154 | 155 | $class = $this->dest_class; 156 | $item = new $class($attributes); 157 | $item->new_record = false; 158 | if (!is_array($this->value)) 159 | $this->value = array(); 160 | array_push($this->value, $item); 161 | } 162 | 163 | function needs_saving() { 164 | if (!is_array($this->value)) 165 | return false; 166 | else { 167 | foreach ($this->value as $val) 168 | if ($val->is_modified() || $val->is_new_record()) 169 | return true; 170 | } 171 | return false; 172 | } 173 | 174 | function save_as_needed($source) { 175 | foreach ($this->value as $object) { 176 | if ($object->is_modified() || $object->is_new_record()) { 177 | if (!isset($this->options['through']) || !$this->options['through']) 178 | $object->{$this->foreign_key} = $source->{$source->get_primary_key()}; 179 | $object->save(); 180 | } 181 | } 182 | } 183 | 184 | } 185 | ?> 186 | -------------------------------------------------------------------------------- /Zend/Json/Encoder.php: -------------------------------------------------------------------------------- 1 | _cycleCheck = $cycleCheck; 61 | } 62 | 63 | /** 64 | * Use the JSON encoding scheme for the value specified 65 | * 66 | * @param mixed $value The value to be encoded 67 | * @param boolean $cycleCheck Whether or not to check for possible object recursion when encoding 68 | * @return string The encoded value 69 | */ 70 | public static function encode($value, $cycleCheck = false) 71 | { 72 | $encoder = new Zend_Json_Encoder(($cycleCheck) ? true : false); 73 | 74 | return $encoder->_encodeValue($value); 75 | } 76 | 77 | /** 78 | * Recursive driver which determines the type of value to be encoded 79 | * and then dispatches to the appropriate method. $values are either 80 | * - objects (returns from {@link _encodeObject()}) 81 | * - arrays (returns from {@link _encodeArray()}) 82 | * - basic datums (e.g. numbers or strings) (returns from {@link _encodeDatum()}) 83 | * 84 | * @param $value mixed The value to be encoded 85 | * @return string Encoded value 86 | */ 87 | protected function _encodeValue(&$value) 88 | { 89 | if (is_object($value)) { 90 | return $this->_encodeObject($value); 91 | } else if (is_array($value)) { 92 | return $this->_encodeArray($value); 93 | } 94 | 95 | return $this->_encodeDatum($value); 96 | } 97 | 98 | 99 | 100 | /** 101 | * Encode an object to JSON by encoding each of the public properties 102 | * 103 | * A special property is added to the JSON object called '__className' 104 | * that contains the name of the class of $value. This is used to decode 105 | * the object on the client into a specific class. 106 | * 107 | * @param $value object 108 | * @return string 109 | * @throws Zend_Json_Exception If recursive checks are enabled and the object has been serialized previously 110 | */ 111 | protected function _encodeObject(&$value) 112 | { 113 | if ($this->_cycleCheck) { 114 | if ($this->_wasVisited($value)) { 115 | throw new Zend_Json_Exception( 116 | 'Cycles not supported in JSON encoding, cycle introduced by ' 117 | . 'class "' . get_class($value) . '"' 118 | ); 119 | } 120 | 121 | $this->_visited[] = $value; 122 | } 123 | 124 | $props = ''; 125 | foreach (get_object_vars($value) as $name => $propValue) { 126 | if (isset($propValue)) { 127 | $props .= ',' 128 | . $this->_encodeValue($name) 129 | . ':' 130 | . $this->_encodeValue($propValue); 131 | } 132 | } 133 | 134 | return '{"__className":"' . get_class($value) . '"' 135 | . $props . '}'; 136 | } 137 | 138 | 139 | /** 140 | * Determine if an object has been serialized already 141 | * 142 | * @param mixed $value 143 | * @return boolean 144 | */ 145 | protected function _wasVisited(&$value) 146 | { 147 | if (in_array($value, $this->_visited, true)) { 148 | return true; 149 | } 150 | 151 | return false; 152 | } 153 | 154 | 155 | /** 156 | * JSON encode an array value 157 | * 158 | * Recursively encodes each value of an array and returns a JSON encoded 159 | * array string. 160 | * 161 | * Arrays are defined as integer-indexed arrays starting at index 0, where 162 | * the last index is (count($array) -1); any deviation from that is 163 | * considered an associative array, and will be encoded as such. 164 | * 165 | * @param $array array 166 | * @return string 167 | */ 168 | protected function _encodeArray(&$array) 169 | { 170 | $tmpArray = array(); 171 | 172 | // Check for associative array 173 | if (!empty($array) && (array_keys($array) !== range(0, count($array) - 1))) { 174 | // Associative array 175 | $result = '{'; 176 | foreach ($array as $key => $value) { 177 | $key = (string) $key; 178 | $tmpArray[] = $this->_encodeString($key) 179 | . ':' 180 | . $this->_encodeValue($value); 181 | } 182 | $result .= implode(',', $tmpArray); 183 | $result .= '}'; 184 | } else { 185 | // Indexed array 186 | $result = '['; 187 | $length = count($array); 188 | for ($i = 0; $i < $length; $i++) { 189 | $tmpArray[] = $this->_encodeValue($array[$i]); 190 | } 191 | $result .= implode(',', $tmpArray); 192 | $result .= ']'; 193 | } 194 | 195 | return $result; 196 | } 197 | 198 | 199 | /** 200 | * JSON encode a basic data type (string, number, boolean, null) 201 | * 202 | * If value type is not a string, number, boolean, or null, the string 203 | * 'null' is returned. 204 | * 205 | * @param $value mixed 206 | * @return string 207 | */ 208 | protected function _encodeDatum(&$value) 209 | { 210 | $result = 'null'; 211 | 212 | if (is_int($value) || is_float($value)) { 213 | $result = (string)$value; 214 | } elseif (is_string($value)) { 215 | $result = $this->_encodeString($value); 216 | } elseif (is_bool($value)) { 217 | $result = $value ? 'true' : 'false'; 218 | } 219 | 220 | return $result; 221 | } 222 | 223 | 224 | /** 225 | * JSON encode a string value by escaping characters as necessary 226 | * 227 | * @param $value string 228 | * @return string 229 | */ 230 | protected function _encodeString(&$string) 231 | { 232 | // Escape these characters with a backslash: 233 | // " \ / \n \r \t \b \f 234 | $search = array('\\', "\n", "\t", "\r", "\b", "\f", '"'); 235 | $replace = array('\\\\', '\\n', '\\t', '\\r', '\\b', '\\f', '\"'); 236 | $string = str_replace($search, $replace, $string); 237 | 238 | // Escape certain ASCII characters: 239 | // 0x08 => \b 240 | // 0x0c => \f 241 | $string = str_replace(array(chr(0x08), chr(0x0C)), array('\b', '\f'), $string); 242 | 243 | return '"' . $string . '"'; 244 | } 245 | 246 | 247 | /** 248 | * Encode the constants associated with the ReflectionClass 249 | * parameter. The encoding format is based on the class2 format 250 | * 251 | * @param $cls ReflectionClass 252 | * @return string Encoded constant block in class2 format 253 | */ 254 | static private function _encodeConstants(ReflectionClass $cls) 255 | { 256 | $result = "constants : {"; 257 | $constants = $cls->getConstants(); 258 | 259 | $tmpArray = array(); 260 | if (!empty($constants)) { 261 | foreach ($constants as $key => $value) { 262 | $tmpArray[] = "$key: " . self::encode($value); 263 | } 264 | 265 | $result .= implode(', ', $tmpArray); 266 | } 267 | 268 | return $result . "}"; 269 | } 270 | 271 | 272 | /** 273 | * Encode the public methods of the ReflectionClass in the 274 | * class2 format 275 | * 276 | * @param $cls ReflectionClass 277 | * @return string Encoded method fragment 278 | * 279 | */ 280 | static private function _encodeMethods(ReflectionClass $cls) 281 | { 282 | $methods = $cls->getMethods(); 283 | $result = 'methods:{'; 284 | 285 | $started = false; 286 | foreach ($methods as $method) { 287 | if (! $method->isPublic() || !$method->isUserDefined()) { 288 | continue; 289 | } 290 | 291 | if ($started) { 292 | $result .= ','; 293 | } 294 | $started = true; 295 | 296 | $result .= '' . $method->getName(). ':function('; 297 | 298 | if ('__construct' != $method->getName()) { 299 | $parameters = $method->getParameters(); 300 | $paramCount = count($parameters); 301 | $argsStarted = false; 302 | 303 | $argNames = "var argNames=["; 304 | foreach ($parameters as $param) { 305 | if ($argsStarted) { 306 | $result .= ','; 307 | } 308 | 309 | $result .= $param->getName(); 310 | 311 | if ($argsStarted) { 312 | $argNames .= ','; 313 | } 314 | 315 | $argNames .= '"' . $param->getName() . '"'; 316 | 317 | $argsStarted = true; 318 | } 319 | $argNames .= "];"; 320 | 321 | $result .= "){" 322 | . $argNames 323 | . 'var result = ZAjaxEngine.invokeRemoteMethod(' 324 | . "this, '" . $method->getName() 325 | . "',argNames,arguments);" 326 | . 'return(result);}'; 327 | } else { 328 | $result .= "){}"; 329 | } 330 | } 331 | 332 | return $result . "}"; 333 | } 334 | 335 | 336 | /** 337 | * Encode the public properties of the ReflectionClass in the class2 338 | * format. 339 | * 340 | * @param $cls ReflectionClass 341 | * @return string Encode properties list 342 | * 343 | */ 344 | static private function _encodeVariables(ReflectionClass $cls) 345 | { 346 | $properties = $cls->getProperties(); 347 | $propValues = get_class_vars($cls->getName()); 348 | $result = "variables:{"; 349 | $cnt = 0; 350 | 351 | $tmpArray = array(); 352 | foreach ($properties as $prop) { 353 | if (! $prop->isPublic()) { 354 | continue; 355 | } 356 | 357 | $tmpArray[] = $prop->getName() 358 | . ':' 359 | . self::encode($propValues[$prop->getName()]); 360 | } 361 | $result .= implode(',', $tmpArray); 362 | 363 | return $result . "}"; 364 | } 365 | 366 | /** 367 | * Encodes the given $className into the class2 model of encoding PHP 368 | * classes into JavaScript class2 classes. 369 | * NOTE: Currently only public methods and variables are proxied onto 370 | * the client machine 371 | * 372 | * @param $className string The name of the class, the class must be 373 | * instantiable using a null constructor 374 | * @param $package string Optional package name appended to JavaScript 375 | * proxy class name 376 | * @return string The class2 (JavaScript) encoding of the class 377 | * @throws Zend_Json_Exception 378 | */ 379 | static public function encodeClass($className, $package = '') 380 | { 381 | $cls = new ReflectionClass($className); 382 | if (! $cls->isInstantiable()) { 383 | throw new Zend_Json_Exception("$className must be instantiable"); 384 | } 385 | 386 | return "Class.create('$package$className',{" 387 | . self::_encodeConstants($cls) ."," 388 | . self::_encodeMethods($cls) ."," 389 | . self::_encodeVariables($cls) .'});'; 390 | } 391 | 392 | 393 | /** 394 | * Encode several classes at once 395 | * 396 | * Returns JSON encoded classes, using {@link encodeClass()}. 397 | * 398 | * @param array $classNames 399 | * @param string $package 400 | * @return string 401 | */ 402 | static public function encodeClasses(array $classNames, $package = '') 403 | { 404 | $result = ''; 405 | foreach ($classNames as $className) { 406 | $result .= self::encodeClass($className, $package); 407 | } 408 | 409 | return $result; 410 | } 411 | 412 | } 413 | 414 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ActiveRecord In PHP 2 | 3 | ## Motivation 4 | 5 | I wrote this after having been spoiled by Ruby on Rails’ implementation 6 | of the ActiveRecord pattern, while still needing to work primarily in 7 | PHP. When I started this, there did exist some 8 | ORM options in PHP. 9 | However, I wasn’t satisfied with any one in particular. My goals were to 10 | create an implementation that was very similar to the Rails syntax, easy 11 | to install, and fast. 12 | 13 | ## Requirements 14 | 15 | - PHP5 16 | - Naming of tables and columns that follows the Rails convention. 17 | 18 | 19 | ## Installation 20 | 21 | 1. Create your database and tables, if you haven’t already. (remember 22 | use Rails’ conventions for table and column names) 23 | 2. Download [recent ActiveRecord release][] or 24 | 25 | git clone https://github.com/lukebaker/activerecord-php.git 26 | 27 | 3. Untar into a models/ directory within your project or move checked 28 | out directory activerecord-php/ into your models/ directory. 29 | 4. There should now be a models/activerecord-php/ directory, edit 30 | models/activerecord-php/config.php to your liking. 31 | 5. Run models/activerecord-php/generate.php 32 | 6. This should have have generated model stubs inside your models/ 33 | directory. Edit these model files to tell ActiveRecord about the 34 | relationships between tables. Do not edit \*Base.php files as they 35 | get overwritten every time you run generate.php 36 | 7. Use ActiveRecord, by including the models that you want to use: 37 | 38 | require_once 'models/Post.php'; 39 | 40 | [recent ActiveRecord release]: https://github.com/lukebaker/activerecord-php/tags 41 | 42 | ## Example 43 | 44 | ### Create 45 | 46 | ```php 47 | $p = new Post(array('title' => 'First Post!11!', 'body' => 'This is the body of my post')); 48 | $p->save(); # saves this post to the table 49 | 50 | $p2 = new Post(); 51 | $p2->title = "Second Post"; 52 | $p2->body = "This is the body of the second post"; 53 | $p2->save(); # save yet another post to the db 54 | ``` 55 | 56 | ### Retrieve 57 | 58 | ```php 59 | $p = Post::find(1); # finds the post with an id = 1 60 | $p->title; # title of this post 61 | $p->body; # body of this post 62 | 63 | # returns the 10 most recent posts in an array, assuming you have a column called "timestamp" 64 | $posts = Post::find('all', array('order' => 'timestamp DESC', 'limit' => 10)); 65 | ``` 66 | 67 | ### Update 68 | 69 | ```php 70 | $p = Post::find(1); 71 | $p->title = "Some new title"; 72 | $p->save(); # saves the change to the post 73 | 74 | # alternatively, the following is useful when a form submits an array 75 | $_POST['post'] = array('title' => 'New Title', 'body' => 'New body here!'); 76 | $p = Post::find(1); 77 | $p->update_attributes($_POST['post']); # saves the object with these attributes updated 78 | ``` 79 | 80 | ### Destroy 81 | 82 | ```php 83 | $p = Post::find(1); 84 | $p->destroy(); 85 | ``` 86 | 87 | ### Relationships 88 | 89 | ```php 90 | $p = Post::find(1); 91 | # call to $p->comments results in query to get all comments for this post 92 | # a subsequent call to $p->comments would not result in a query, but use results from previous query 93 | foreach ($p->comments as $comment) { 94 | echo $comment->content; 95 | } 96 | ``` 97 | 98 | ## Documentation 99 | 100 | While this attempts to document most of the features of ActiveRecord, it may not be entirely complete. I've tried to create tests for all pieces of functionality that exist in ActiveRecord. To view and / or run these tests check out the devel/ branch in the Subversion repository. In other words, there may be some functionality that is not documented here but is used in the tests. 101 | 102 | For example purposes, let’s pretend we’re building a blog. You’ll have model classes which are each the model of a database table. Each model class is in a separate file. The stubs of these files are automatically generated for you by generate.php. Every time you update your database schema, you'll have to run generate.php again. It will not overwrite the files you've altered, but will overwrite the \*Base.php files. Once you have the model stubs generated you can use them and work with the tables individually. However, in order to use the relationship specific abilities of ActiveRecord, you’ll need to specify the relationships in your models as outlined below in the Associations section. 103 | 104 | ### Associations 105 | 106 | In ActiveRecord we specify relationships between the tables in the model 107 | classes. There are 3 types of relationships, 1:1, 1:many, and many:many. 108 | 109 | #### 1:1 110 | 111 | In our example, blog posts have a 1:1 relationship with slugs. Here’s 112 | how you’d specify that inside the Post and Slug classes. 113 | 114 | ```php 115 | /* inside Post.php */ 116 | protected $has_one = array('slug'); 117 | 118 | /* inside Slug.php */ 119 | protected $belongs_to = array('post'); 120 | ``` 121 | 122 | In a 1:1 relationship we must specify each side of the relationship 123 | slightly differently so that ActiveRecord knows the “direction” of the 124 | relationship. We use belongs\_to for the model whose table contains the 125 | foreign key (post\_id in this case). The other side of the relationship 126 | uses has\_one. Since an object could have multiple 1:1 relationships, we 127 | use an array to allow for additional tables. Notice the singular use of 128 | slug and post. The code tries to read like English as much as possible, 129 | so later when we do 1:many relationships you’ll plural strings. After 130 | you’ve specified this relationship you can do some extra things with 131 | your models. On every slug and post object you can now do →post and 132 | →slug to get its post and slug respectively as an ActiveRecord object. 133 | Also you set assign a slug or post using this mechanism. Furthermore, a 134 | save will cascade to the relationship. 135 | 136 | ```php 137 | $slug = Slug::find('first'); # SQL query to grab first slug 138 | $slug->post; # an SQL query occurs behind the scenes to find the slug's post 139 | 140 | $p = Post::find('first', array('include' => 'slug')); # SQL join 141 | $p->slug; # no SQL query here because we already got this post's slug in the SQL join in the previous line 142 | 143 | $p = Post::find('first'); 144 | $s = new Slug(array('slug' => 'super-slug')); 145 | $p->slug = $s; # assign a slug to this post 146 | 147 | $p->slug->slug = 'foobar'; 148 | $p->save(); # cascading save (post and slug are saved) 149 | ``` 150 | 151 | #### 1:many 152 | 153 | In our example a post has many comments, but a comment only has one 154 | post. Here’s how you’d specify it in the Post and Comment classes. 155 | 156 | ```php 157 | /* inside Post.php */ 158 | protected $has_many = array('comments'); 159 | 160 | /* inside Comment.php */ 161 | protected $belongs_to = array('post'); 162 | ``` 163 | 164 | Notice, we used plural “comments” for the has\_many and a singular 165 | “post” for belongs\_to. Also notice how the comments table contains the 166 | foreign key (post\_id) and therefore is a belongs\_to relationship. Once 167 | we’ve done this Comment can do the same things as an 1:something 168 | relationship can (see 1:1). Post now has some slight variations to the 169 | features added in a 1:1 relationship. Now when accessing the attribute 170 | comments you’d get an array of comment ActiveRecord objects that belong 171 | to this Post. 172 | 173 | ```php 174 | $p = Post::find('first'); 175 | echo $p->comments[0]->body; 176 | ``` 177 | 178 | You can also get the list of comment ids that belong to this post by 179 | calling →comment\_ids. You can set the ids in a similar fashion. 180 | 181 | ```php 182 | $p = Post::find('first'); 183 | $foo = $p->comment_ids; 184 | # foo is now an array of comment ids that belong to this post 185 | array_pop($foo); # pop off last comment id 186 | array_push($foo, 23); # and another comment id to $foo 187 | 188 | $p->comment_ids = $foo; 189 | /* this will remove the comment we popped off of foo 190 | and add the comment we pushed onto foo to this post 191 | */ 192 | ``` 193 | 194 | You can also push new objects onto the relationships. 195 | 196 | ```php 197 | $c = new Comment(array('author' => 'anon', 'body' => 'first comment!!11')); 198 | $p->comments_push($c); # this call saves the new comment and associates with this post 199 | ``` 200 | 201 | In this example, we might want to have comments destroyed when their 202 | post is destroyed or when they are disassociated with their post. You 203 | can have this happen by specifying the relationship slightly 204 | differently. You can do this on any sort of relationship. Instead have 205 | the following in the Post model. 206 | 207 | ```php 208 | /* inside Post.php */ 209 | protected $has_many = array(array('comments' => array('dependent' => 'destroy'))); 210 | ``` 211 | 212 | #### many:many 213 | 214 | A many:many relationship will have an intermediate table (and therefore 215 | model) which ties two other tables together. In our example, there is a 216 | many:many relationship between posts and categories. Our intermediate 217 | table is categorizations. Here is how that is specified: 218 | 219 | ```php 220 | /* inside Categorization.php */ 221 | protected $belongs_to = array('post', 'category'); 222 | 223 | /* inside Post.php */ 224 | protected $has_many = array( 'categorizations', 225 | array('categories' => array('through' => 'categorizations'))); 226 | 227 | /* inside Category.php */ 228 | protected $has_many = array( 'categorizations', 229 | array('posts' => array('through' => 'categorizations'))); 230 | ``` 231 | 232 | Since the categorizations table contains the foreign keys post\_id and 233 | category\_id, it has a belongs\_to relationship with those. The Post 234 | model has a regular has\_many relationship with categorizations and a 235 | special has\_many relationship with categories. We specify which table 236 | that relationship goes through (categorizations), IOW which table is the 237 | intermediate table of that relationship. The category to post 238 | relationship is specified similarly. Posts and categories can now use 239 | the special has\_many methods documented in the 1:many relationship. 240 | 241 | ### Working With Models 242 | 243 | This section applies to all models regardless of any associations they 244 | may have. 245 | 246 | #### Create 247 | 248 | ```php 249 | $p = new Post(array('title' => 'First Post!11!', 'body' => 'This is the body of my post')); 250 | $p->save(); # saves this post to the table 251 | 252 | $p2 = new Post(); 253 | $p2->title = "Second Post"; 254 | $p2->body = "This is the body of the second post"; 255 | $p2->save(); # save yet another post to the db 256 | ``` 257 | 258 | #### Retrieve 259 | 260 | Retrieving data involves finding the rows you want to look at and 261 | subsequently grabbing the column data as needed. The first parameter for 262 | the find method should be one of the following: 263 | 264 | - an id number 265 | - an array of id numbers 266 | - the string “first” 267 | - the string “all” 268 | 269 | When the first parameter is an id number or the string “first”, the 270 | result will be an ActiveRecord object. Otherwise, it will be an array of 271 | ActiveRecord objects. The find method takes quite a few different 272 | options for its second parameter by using “named parameters” by 273 | accepting an array of key, value pairs. You can pass it the following 274 | keys with sane values: 275 | 276 | - limit 277 | - order 278 | - group 279 | - offset 280 | - select 281 | - conditions 282 | - include (for associations) 283 | 284 | ```php 285 | $p = Post::find(1); # finds the post with an id = 1 286 | $p->title; # title of this post 287 | $p->body; # body of this post 288 | 289 | # returns the 10 most recent posts in an array, assuming you have a column called "timestamp" 290 | $posts = Post::find('all', array('order' => 'timestamp DESC', 'limit' => 10)); 291 | ``` 292 | 293 | #### Update 294 | 295 | ```php 296 | $p = Post::find(1); 297 | $p->title = "Some new title"; 298 | $p->save(); # saves the change to the post 299 | 300 | # alternatively, the following is useful when a form submits an array 301 | $_POST['post'] = array('title' => 'New Title', 'body' => 'New body here!'); 302 | $p = Post::find(1); 303 | $p->update_attributes($_POST['post']); # saves the object with these attributes updated 304 | ``` 305 | 306 | #### Destroy 307 | 308 | ```php 309 | $p = Post::find(1); 310 | $p->destroy(); 311 | ``` 312 | 313 | #### Hooks 314 | 315 | The following hooks are available, just define the method of the same 316 | name in the model that you want to use them: 317 | 318 | - before\_save 319 | - before\_create 320 | - after\_create 321 | - before\_update 322 | - after\_update 323 | - after\_save 324 | - before\_destroy 325 | - after\_destroy 326 | 327 | #### Escaping Query Values 328 | 329 | ActiveRecord will do proper escaping of query values passed to where 330 | possible. However, it can’t do proper quoting when you do something like 331 | the following. 332 | 333 | ```php 334 | $p = Post::find('first', array('conditions' => "title = {$_GET['title']}")); 335 | ``` 336 | 337 | Instead you can use the quote static method to quote that value like so. 338 | 339 | ```php 340 | $title = ActiveRecord::quote($_GET['title']); 341 | $p = Post::find('first', array('conditions' => "title = $title")); 342 | ``` 343 | 344 | #### Manual Queries 345 | 346 | Occasionally, though hopefully rarely, you may need to do specify some 347 | queries by hand. You can use the query static method. This returns an 348 | associative array with all the rows in it. 349 | 350 | ```php 351 | ActiveRecord::query("SELECT COUNT(*) FROM bar as b1, bar as b2 where b2.id != b1.id"); 352 | ``` 353 | 354 | ### Table Structure For Example 355 | 356 | ```sql 357 | -- 358 | -- Table structure for table `categories` 359 | -- 360 | 361 | CREATE TABLE `categories` ( 362 | `id` int(11) NOT NULL AUTO_INCREMENT, 363 | `name` varchar(255) DEFAULT NULL, 364 | PRIMARY KEY (`id`) 365 | ) TYPE=MyISAM; 366 | 367 | -- 368 | -- Table structure for table `categorizations` 369 | -- 370 | 371 | CREATE TABLE `categorizations` ( 372 | `id` int(11) NOT NULL AUTO_INCREMENT, 373 | `post_id` int(11) DEFAULT NULL, 374 | `category_id` int(11) DEFAULT NULL, 375 | PRIMARY KEY (`id`) 376 | ) TYPE=MyISAM; 377 | 378 | -- 379 | -- Table structure for table `comments` 380 | -- 381 | 382 | CREATE TABLE `comments` ( 383 | `id` int(11) NOT NULL AUTO_INCREMENT, 384 | `author` varchar(255) DEFAULT NULL, 385 | `body` text, 386 | `post_id` int(11) DEFAULT NULL, 387 | PRIMARY KEY (`id`) 388 | ) TYPE=MyISAM; 389 | 390 | -- 391 | -- Table structure for table `posts` 392 | -- 393 | 394 | CREATE TABLE `posts` ( 395 | `id` int(11) NOT NULL AUTO_INCREMENT, 396 | `title` varchar(255) DEFAULT NULL, 397 | `body` text, 398 | PRIMARY KEY (`id`) 399 | ) TYPE=MyISAM; 400 | 401 | -- 402 | -- Table structure for table `slugs` 403 | -- 404 | 405 | CREATE TABLE `slugs` ( 406 | `id` int(11) NOT NULL AUTO_INCREMENT, 407 | `slug` varchar(255) DEFAULT NULL, 408 | `post_id` int(11) NOT NULL, 409 | PRIMARY KEY (`id`) 410 | ) TYPE=MyISAM; 411 | ``` 412 | -------------------------------------------------------------------------------- /Zend/Json/Decoder.php: -------------------------------------------------------------------------------- 1 | _source = $source; 112 | $this->_sourceLength = strlen($source); 113 | $this->_token = self::EOF; 114 | $this->_offset = 0; 115 | 116 | // Normalize and set $decodeType 117 | if (!in_array($decodeType, array(Zend_Json::TYPE_ARRAY, Zend_Json::TYPE_OBJECT))) 118 | { 119 | $decodeType = Zend_Json::TYPE_ARRAY; 120 | } 121 | $this->_decodeType = $decodeType; 122 | 123 | // Set pointer at first token 124 | $this->_getNextToken(); 125 | } 126 | 127 | /** 128 | * Decode a JSON source string 129 | * 130 | * Decodes a JSON encoded string. The value returned will be one of the 131 | * following: 132 | * - integer 133 | * - float 134 | * - boolean 135 | * - null 136 | * - StdClass 137 | * - array 138 | * - array of one or more of the above types 139 | * 140 | * By default, decoded objects will be returned as associative arrays; to 141 | * return a StdClass object instead, pass {@link Zend_Json::TYPE_OBJECT} to 142 | * the $objectDecodeType parameter. 143 | * 144 | * Throws a Zend_Json_Exception if the source string is null. 145 | * 146 | * @static 147 | * @access public 148 | * @param string $source String to be decoded 149 | * @param int $objectDecodeType How objects should be decoded; should be 150 | * either or {@link Zend_Json::TYPE_ARRAY} or 151 | * {@link Zend_Json::TYPE_OBJECT}; defaults to TYPE_ARRAY 152 | * @return mixed 153 | * @throws Zend_Json_Exception 154 | */ 155 | public static function decode($source = null, $objectDecodeType = Zend_Json::TYPE_ARRAY) 156 | { 157 | if (null === $source) { 158 | throw new Zend_Json_Exception('Must specify JSON encoded source for decoding'); 159 | } elseif (!is_string($source)) { 160 | throw new Zend_Json_Exception('Can only decode JSON encoded strings'); 161 | } 162 | 163 | $decoder = new self($source, $objectDecodeType); 164 | 165 | return $decoder->_decodeValue(); 166 | } 167 | 168 | 169 | /** 170 | * Recursive driving rountine for supported toplevel tops 171 | * 172 | * @return mixed 173 | */ 174 | protected function _decodeValue() 175 | { 176 | switch ($this->_token) { 177 | case self::DATUM: 178 | $result = $this->_tokenValue; 179 | $this->_getNextToken(); 180 | return($result); 181 | break; 182 | case self::LBRACE: 183 | return($this->_decodeObject()); 184 | break; 185 | case self::LBRACKET: 186 | return($this->_decodeArray()); 187 | break; 188 | default: 189 | return null; 190 | break; 191 | } 192 | } 193 | 194 | /** 195 | * Decodes an object of the form: 196 | * { "attribute: value, "attribute2" : value,...} 197 | * 198 | * If ZJsonEnoder or ZJAjax was used to encode the original object 199 | * then a special attribute called __className which specifies a class 200 | * name that should wrap the data contained within the encoded source. 201 | * 202 | * Decodes to either an array or StdClass object, based on the value of 203 | * {@link $_decodeType}. If invalid $_decodeType present, returns as an 204 | * array. 205 | * 206 | * @return array|StdClass 207 | */ 208 | protected function _decodeObject() 209 | { 210 | $members = array(); 211 | $tok = $this->_getNextToken(); 212 | 213 | while ($tok && $tok != self::RBRACE) { 214 | if ($tok != self::DATUM || ! is_string($this->_tokenValue)) { 215 | throw new Zend_Json_Exception('Missing key in object encoding: ' . $this->_source); 216 | } 217 | 218 | $key = $this->_tokenValue; 219 | $tok = $this->_getNextToken(); 220 | 221 | if ($tok != self::COLON) { 222 | throw new Zend_Json_Exception('Missing ":" in object encoding: ' . $this->_source); 223 | } 224 | 225 | $tok = $this->_getNextToken(); 226 | $members[$key] = $this->_decodeValue(); 227 | $tok = $this->_token; 228 | 229 | if ($tok == self::RBRACE) { 230 | break; 231 | } 232 | 233 | if ($tok != self::COMMA) { 234 | throw new Zend_Json_Exception('Missing "," in object encoding: ' . $this->_source); 235 | } 236 | 237 | $tok = $this->_getNextToken(); 238 | } 239 | 240 | switch ($this->_decodeType) { 241 | case Zend_Json::TYPE_OBJECT: 242 | // Create new StdClass and populate with $members 243 | $result = new StdClass(); 244 | foreach ($members as $key => $value) { 245 | $result->$key = $value; 246 | } 247 | break; 248 | case Zend_Json::TYPE_ARRAY: 249 | default: 250 | $result = $members; 251 | break; 252 | } 253 | 254 | $this->_getNextToken(); 255 | return $result; 256 | } 257 | 258 | /** 259 | * Decodes a JSON array format: 260 | * [element, element2,...,elementN] 261 | * 262 | * @return array 263 | */ 264 | protected function _decodeArray() 265 | { 266 | $result = array(); 267 | $starttok = $tok = $this->_getNextToken(); // Move past the '[' 268 | $index = 0; 269 | 270 | while ($tok && $tok != self::RBRACKET) { 271 | $result[$index++] = $this->_decodeValue(); 272 | 273 | $tok = $this->_token; 274 | 275 | if ($tok == self::RBRACKET || !$tok) { 276 | break; 277 | } 278 | 279 | if ($tok != self::COMMA) { 280 | throw new Zend_Json_Exception('Missing "," in array encoding: ' . $this->_source); 281 | } 282 | 283 | $tok = $this->_getNextToken(); 284 | } 285 | 286 | $this->_getNextToken(); 287 | return($result); 288 | } 289 | 290 | 291 | /** 292 | * Removes whitepsace characters from the source input 293 | */ 294 | protected function _eatWhitespace() 295 | { 296 | if (preg_match( 297 | '/([\t\b\f\n\r ])*/s', 298 | $this->_source, 299 | $matches, 300 | PREG_OFFSET_CAPTURE, 301 | $this->_offset) 302 | && $matches[0][1] == $this->_offset) 303 | { 304 | $this->_offset += strlen($matches[0][0]); 305 | } 306 | } 307 | 308 | 309 | /** 310 | * Retrieves the next token from the source stream 311 | * 312 | * @return int Token constant value specified in class definition 313 | */ 314 | protected function _getNextToken() 315 | { 316 | $this->_token = self::EOF; 317 | $this->_tokenValue = null; 318 | $this->_eatWhitespace(); 319 | 320 | if ($this->_offset >= $this->_sourceLength) { 321 | return(self::EOF); 322 | } 323 | 324 | $str = $this->_source; 325 | $str_length = $this->_sourceLength; 326 | $i = $this->_offset; 327 | $start = $i; 328 | 329 | switch ($str{$i}) { 330 | case '{': 331 | $this->_token = self::LBRACE; 332 | break; 333 | case '}': 334 | $this->_token = self::RBRACE; 335 | break; 336 | case '[': 337 | $this->_token = self::LBRACKET; 338 | break; 339 | case ']': 340 | $this->_token = self::RBRACKET; 341 | break; 342 | case ',': 343 | $this->_token = self::COMMA; 344 | break; 345 | case ':': 346 | $this->_token = self::COLON; 347 | break; 348 | case '"': 349 | $result = ''; 350 | do { 351 | $i++; 352 | if ($i >= $str_length) { 353 | break; 354 | } 355 | 356 | $chr = $str{$i}; 357 | if ($chr == '\\') { 358 | $i++; 359 | if ($i >= $str_length) { 360 | break; 361 | } 362 | $chr = $str{$i}; 363 | switch ($chr) { 364 | case '"' : 365 | $result .= '"'; 366 | break; 367 | case '\\': 368 | $result .= '\\'; 369 | break; 370 | case '/' : 371 | $result .= '/'; 372 | break; 373 | case 'b' : 374 | $result .= chr(8); 375 | break; 376 | case 'f' : 377 | $result .= chr(12); 378 | break; 379 | case 'n' : 380 | $result .= chr(10); 381 | break; 382 | case 'r' : 383 | $result .= chr(13); 384 | break; 385 | case 't' : 386 | $result .= chr(9); 387 | break; 388 | default: 389 | throw new Zend_Json_Exception("Illegal escape " 390 | . "sequence '" . $chr . "'"); 391 | } 392 | } elseif ($chr == '"') { 393 | break; 394 | } else { 395 | $result .= $chr; 396 | } 397 | } while ($i < $str_length); 398 | 399 | $this->_token = self::DATUM; 400 | //$this->_tokenValue = substr($str, $start + 1, $i - $start - 1); 401 | $this->_tokenValue = $result; 402 | break; 403 | case 't': 404 | if (($i+ 3) < $str_length && substr($str, $start, 4) == "true") { 405 | $this->_token = self::DATUM; 406 | } 407 | $this->_tokenValue = true; 408 | $i += 3; 409 | break; 410 | case 'f': 411 | if (($i+ 4) < $str_length && substr($str, $start, 5) == "false") { 412 | $this->_token = self::DATUM; 413 | } 414 | $this->_tokenValue = false; 415 | $i += 4; 416 | break; 417 | case 'n': 418 | if (($i+ 3) < $str_length && substr($str, $start, 4) == "null") { 419 | $this->_token = self::DATUM; 420 | } 421 | $this->_tokenValue = NULL; 422 | $i += 3; 423 | break; 424 | } 425 | 426 | if ($this->_token != self::EOF) { 427 | $this->_offset = $i + 1; // Consume the last token character 428 | return($this->_token); 429 | } 430 | 431 | $chr = $str{$i}; 432 | if ($chr == '-' || $chr == '.' || ($chr >= '0' && $chr <= '9')) { 433 | if (preg_match('/-?([0-9])*(\.[0-9]*)?((e|E)((-|\+)?)[0-9]+)?/s', 434 | $str, $matches, PREG_OFFSET_CAPTURE, $start) && $matches[0][1] == $start) { 435 | 436 | $datum = $matches[0][0]; 437 | 438 | if (is_numeric($datum)) { 439 | if (preg_match('/^0\d+$/', $datum)) { 440 | throw new Zend_Json_Exception("Octal notation not supported by JSON (value: $datum)"); 441 | } else { 442 | $val = intval($datum); 443 | $fVal = floatval($datum); 444 | $this->_tokenValue = ($val == $fVal ? $val : $fVal); 445 | } 446 | } else { 447 | throw new Zend_Json_Exception("Illegal number format: $datum"); 448 | } 449 | 450 | $this->_token = self::DATUM; 451 | $this->_offset = $start + strlen($datum); 452 | } 453 | } else { 454 | throw new Zend_Json_Exception('Illegal Token'); 455 | } 456 | 457 | return($this->_token); 458 | } 459 | } 460 | 461 | -------------------------------------------------------------------------------- /ActiveRecord.php: -------------------------------------------------------------------------------- 1 | assoc_types as $type) { 28 | if (isset($this->$type)) { 29 | $class_name = ActiveRecordInflector::classify($type); 30 | foreach ($this->$type as $assoc) { 31 | $assoc = self::decode_if_json($assoc); 32 | /* handle association sent in as array with options */ 33 | if (is_array($assoc)) { 34 | $key = key($assoc); 35 | $this->$key = new $class_name($this, $key, current($assoc)); 36 | } 37 | else 38 | $this->$assoc = new $class_name($this, $assoc); 39 | } 40 | } 41 | } 42 | /* setup attributes */ 43 | if (is_array($params)) { 44 | foreach ($params as $key => $value) 45 | $this->$key = $value; 46 | $this->is_modified = $is_modified; 47 | $this->new_record = $new_record; 48 | } 49 | } 50 | 51 | function __get($name) { 52 | if (array_key_exists($name, $this->attributes)) 53 | return $this->attributes[$name]; 54 | elseif (array_key_exists($name, $this->associations)) 55 | return $this->associations[$name]->get($this); 56 | elseif (in_array($name, $this->columns)) 57 | return null; 58 | elseif (preg_match('/^(.+?)_ids$/', $name, $matches)) { 59 | /* allow for $p->comment_ids type gets on HasMany associations */ 60 | $assoc_name = ActiveRecordInflector::pluralize($matches[1]); 61 | if ($this->associations[$assoc_name] instanceof HasMany) 62 | return $this->associations[$assoc_name]->get_ids($this); 63 | } 64 | throw new ActiveRecordException("attribute called '$name' doesn't exist", 65 | ActiveRecordException::AttributeNotFound); 66 | } 67 | 68 | function __set($name, $value) { 69 | if ($this->frozen) 70 | throw new ActiveRecordException("Can not update $name as object is frozen.", ActiveRecordException::ObjectFrozen); 71 | 72 | /* allow for $p->comment_ids type sets on HasMany associations */ 73 | if (preg_match('/^(.+?)_ids$/', $name, $matches)) { 74 | $assoc_name = ActiveRecordInflector::pluralize($matches[1]); 75 | } 76 | 77 | if (in_array($name, $this->columns)) { 78 | $this->attributes[$name] = $value; 79 | $this->is_modified = true; 80 | } 81 | elseif ($value instanceof Association) { 82 | /* call from constructor to setup association */ 83 | $this->associations[$name] = $value; 84 | } 85 | elseif (array_key_exists($name, $this->associations)) { 86 | /* call like $comment->post = $mypost */ 87 | $this->associations[$name]->set($value, $this); 88 | } 89 | elseif (isset($assoc_name) 90 | && array_key_exists($assoc_name, $this->associations) 91 | && $this->associations[$assoc_name] instanceof HasMany) { 92 | /* allow for $p->comment_ids type sets on HasMany associations */ 93 | $this->associations[$assoc_name]->set_ids($value, $this); 94 | } 95 | else 96 | throw new ActiveRecordException("attribute called '$name' doesn't exist", 97 | ActiveRecordException::AttributeNotFound); 98 | } 99 | 100 | /* on any ActiveRecord object we can make method calls to a specific assoc. 101 | Example: 102 | $p = Post::find(1); 103 | $p->comments_push($comment); 104 | This calls push([$comment], $p) on the comments association 105 | */ 106 | function __call($name, $args) { 107 | // find longest available association that matches beginning of method 108 | $longest_assoc = ''; 109 | foreach (array_keys($this->associations) as $assoc) { 110 | if (strpos($name, $assoc) === 0 && 111 | strlen($assoc) > strlen($longest_assoc)) { 112 | $longest_assoc = $assoc; 113 | } 114 | } 115 | 116 | if ($longest_assoc !== '') { 117 | list($null, $func) = explode($longest_assoc.'_', $name, 2); 118 | return $this->associations[$longest_assoc]->$func($args, $this); 119 | } 120 | else { 121 | throw new ActiveRecordException("method or association not found for ($name)", ActiveRecordException::MethodOrAssocationNotFound); 122 | } 123 | } 124 | 125 | /* various getters */ 126 | function get_columns() { return $this->columns; } 127 | function get_primary_key() { return $this->primary_key; } 128 | function is_frozen() { return $this->frozen; } 129 | function is_new_record() { return $this->new_record; } 130 | function is_modified() { return $this->is_modified; } 131 | function set_modified($val) { $this->is_modified = $val; } 132 | static function get_query_count() { return self::$query_count; } 133 | 134 | /* Json helper will decode a string as Json if it starts with a [ or { 135 | if the Json::decode fails, we return the original value 136 | */ 137 | static function decode_if_json($json) { 138 | require_once dirname(__FILE__) .DIRECTORY_SEPARATOR. 'Zend' .DIRECTORY_SEPARATOR. 'Json.php'; 139 | if (is_string($json) && preg_match('/^\s*[\{\[]/', $json) != 0) { 140 | try { 141 | $json = Zend_Json::decode($json); 142 | } catch (Zend_Json_Exception $e) { } 143 | } 144 | return $json; 145 | } 146 | 147 | /* 148 | DB specific stuff 149 | */ 150 | static function &get_dbh() { 151 | if (!self::$dbh) { 152 | self::$dbh = call_user_func_array(array(AR_ADAPTER."Adapter", __FUNCTION__), 153 | array(AR_HOST, AR_PORT, AR_DB, AR_USER, AR_PASS, AR_DRIVER)); 154 | } 155 | return self::$dbh; 156 | } 157 | 158 | static function query($query) { 159 | $dbh =& self::get_dbh(); 160 | #var_dump($query); 161 | self::$query_count++; 162 | return call_user_func_array(array(AR_ADAPTER."Adapter", __FUNCTION__), 163 | array($query, $dbh)); 164 | } 165 | 166 | static function quote($string, $type = null) { 167 | $dbh =& self::get_dbh(); 168 | return call_user_func_array(array(AR_ADAPTER.'Adapter', __FUNCTION__), 169 | array($string, $dbh, $type)); 170 | } 171 | 172 | static function last_insert_id($resource = null) { 173 | $dbh =& self::get_dbh(); 174 | return call_user_func_array(array(AR_ADAPTER.'Adapter', __FUNCTION__), 175 | array($dbh, $resource)); 176 | } 177 | 178 | function update_attributes($attributes) { 179 | foreach ($attributes as $key => $value) 180 | $this->$key = $value; 181 | return $this->save(); 182 | } 183 | 184 | function save() { 185 | if (method_exists($this, 'before_save')) 186 | $this->before_save(); 187 | foreach ($this->associations as $name => $assoc) { 188 | if ($assoc instanceOf BelongsTo && $assoc->needs_saving()) { 189 | /* save the object referenced by this association */ 190 | $this->$name->save(); 191 | /* after our save, $this->$name might have new id; 192 | we want to update the foreign key of $this to match; 193 | we update this foreign key already as a side-effect 194 | when calling set() on an association 195 | */ 196 | $this->$name = $this->$name; 197 | } 198 | } 199 | if ($this->new_record) { 200 | if (method_exists($this, 'before_create')) 201 | $this->before_create(); 202 | /* insert new record */ 203 | foreach ($this->columns as $column) { 204 | if ($column == $this->primary_key) continue; 205 | if (is_null($this->$column)) continue; 206 | $columns[] = '`' . $column . '`'; 207 | if (is_null($this->$column)) 208 | $values[] = 'NULL'; 209 | else 210 | $values[] = self::quote($this->$column); 211 | } 212 | $columns = implode(", ", $columns); 213 | $values = implode(", ", $values); 214 | $query = "INSERT INTO {$this->table_name} ($columns) VALUES ($values)"; 215 | $res = self::query($query); 216 | $this->{$this->primary_key} = self::last_insert_id(); 217 | $this->new_record = false; 218 | $this->is_modified = false; 219 | if (method_exists($this, 'after_create')) 220 | $this->after_create(); 221 | } 222 | elseif ($this->is_modified) { 223 | if (method_exists($this, 'before_update')) 224 | $this->before_update(); 225 | /* update existing record */ 226 | $col_vals = array(); 227 | foreach ($this->columns as $column) { 228 | if ($column == $this->primary_key) continue; 229 | if (is_null($this->$column)) continue; 230 | $value = is_null($this->$column) ? 'NULL' : self::quote($this->$column); 231 | $col_vals[] = "`$column` = $value"; 232 | } 233 | $columns_values = implode(", ", $col_vals); 234 | $query = "UPDATE {$this->table_name} SET $columns_values " 235 | . " WHERE {$this->primary_key} = {$this->{$this->primary_key}} " 236 | . " LIMIT 1"; 237 | $res = self::query($query); 238 | $this->new_record = false; 239 | $this->is_modified = false; 240 | if (method_exists($this, 'after_update')) 241 | $this->after_update(); 242 | } 243 | foreach ($this->associations as $name => $assoc) { 244 | if ($assoc instanceOf HasOne && $assoc->needs_saving()) { 245 | /* again sorta weird, this will update foreign key as needed */ 246 | $this->$name = $this->$name; 247 | /* save the object referenced by this association */ 248 | $this->$name->save(); 249 | } 250 | elseif ($assoc instanceOf HasMany && $assoc->needs_saving()) { 251 | $assoc->save_as_needed($this); 252 | } 253 | } 254 | if (method_exists($this, 'after_save')) 255 | $this->after_save(); 256 | } 257 | 258 | function destroy() { 259 | if (method_exists($this, 'before_destroy')) 260 | $this->before_destroy(); 261 | foreach ($this->associations as $name => $assoc) { 262 | $assoc->destroy($this); 263 | } 264 | $query = "DELETE FROM {$this->table_name} " 265 | . "WHERE {$this->primary_key} = {$this->{$this->primary_key}} " 266 | . "LIMIT 1"; 267 | self::query($query); 268 | $this->frozen = true; 269 | if (method_exists($this, 'after_destroy')) 270 | $this->after_destroy(); 271 | return true; 272 | } 273 | 274 | /* transform_row -- transforms a row into its various objects 275 | accepts: row from SQL query (array), lookup array of column names 276 | return: object keyed by table names and real columns names 277 | */ 278 | static function transform_row($row, $col_lookup) { 279 | $object = array(); 280 | foreach ($row as $col_name => $col_value) { 281 | /* set $object["table_name"]["column_name"] = $col_value */ 282 | $object[$col_lookup[$col_name]["table"]][$col_lookup[$col_name]["column"]] = $col_value; 283 | } 284 | return $object; 285 | } 286 | 287 | static function find($class, $id, $options=null) { 288 | $class = str_replace('Base', '', $class); 289 | $query = self::generate_find_query($class, $id, $options); 290 | $rows = self::query($query['query']); 291 | #var_dump($query['query']); 292 | #$objects = self::transform_rows($rows, $query['column_lookup']); 293 | $base_objects = array(); 294 | foreach ($rows as $row) { 295 | /* if we've done a join we have some fancy footwork to do 296 | we're going to process one row at a time. 297 | each row has a "base" object and objects that've been joined. 298 | the base object is whatever class we've been passed as $class. 299 | we only want to create one instance of each unique base object. 300 | as we see more rows we may be re-using an exising base object to 301 | append more join objects to its association. 302 | */ 303 | if (count($query['column_lookup']) > 0) { 304 | $objects = self::transform_row($row, $query['column_lookup']); 305 | $ob_key = md5(serialize($objects[ActiveRecordInflector::tableize($class)])); 306 | /* set cur_object to base object for this row; reusing if possible */ 307 | if (array_key_exists($ob_key, $base_objects)) { 308 | $cur_object = $base_objects[$ob_key]; 309 | } 310 | else { 311 | $cur_object = new $class($objects[ActiveRecordInflector::tableize($class)], false); 312 | $base_objects[$ob_key] = $cur_object; 313 | } 314 | 315 | /* now add association data as needed */ 316 | foreach ($objects as $table_name => $attributes) { 317 | if ($table_name == ActiveRecordInflector::tableize($class)) continue; 318 | foreach ($cur_object->associations as $assoc_name => $assoc) { 319 | if ($table_name == ActiveRecordInflector::pluralize($assoc_name)) 320 | $assoc->populate_from_find($attributes); 321 | } 322 | } 323 | } 324 | else { 325 | $item = new $class($row, false); 326 | array_push($base_objects, $item); 327 | } 328 | } 329 | if (count($base_objects) == 0 && (is_array($id) || is_numeric($id))) 330 | throw new ActiveRecordException("Couldn't find anything.", ActiveRecordException::RecordNotFound); 331 | return (is_array($id) || $id == 'all') ? 332 | array_values($base_objects) : 333 | array_shift($base_objects); 334 | } 335 | 336 | function generate_find_query($class_name, $id, $options=null) { 337 | //$dbh =& $this->get_dbh(); 338 | $item = new $class_name; 339 | $options = self::decode_if_json($options); 340 | 341 | /* first sanitize what we can */ 342 | if (is_array($id)) { 343 | foreach ($id as $k => $v) { 344 | $id[$k] = self::quote($v); 345 | } 346 | } 347 | elseif ($id != 'all' && $id != 'first') { 348 | $id = self::quote($id); 349 | } 350 | /* regex for limit, order, group */ 351 | $regex = '/^[A-Za-z0-9\-_ ,\(\)]+$/'; 352 | if (!isset($options['limit']) || !preg_match($regex, $options['limit'])) 353 | $options['limit'] = ''; 354 | if (!isset($options['order']) || !preg_match($regex, $options['order'])) 355 | $options['order'] = ''; 356 | if (!isset($options['group']) || !preg_match($regex, $options['group'])) 357 | $options['group'] = ''; 358 | if (!isset($options['offset']) || !is_numeric($options['offset'])) 359 | $options['offset'] = ''; 360 | 361 | $select = '*'; 362 | if (is_array($id)) 363 | $where = "{$item->primary_key} IN (" . implode(",", $id) . ")"; 364 | elseif ($id == 'first') 365 | $limit = '1'; 366 | elseif ($id != 'all') 367 | $where = "{$item->table_name}.{$item->primary_key} = $id"; 368 | 369 | if (isset($options['conditions'])) { 370 | $cond = self::convert_conditions_to_where($options['conditions']); 371 | $where = (isset($where) && $where) ? $where . " AND " . $cond : $cond; 372 | } 373 | 374 | if ($options['offset']) 375 | $offset = $options['offset']; 376 | if ($options['limit'] && !isset($limit)) 377 | $limit = $options['limit']; 378 | if (isset($options['select'])) 379 | $select = $options['select']; 380 | $joins = array(); 381 | $tables_to_columns = array(); 382 | $column_lookup = array(); 383 | if (isset($options['include'])) { 384 | array_push($tables_to_columns, 385 | array(ActiveRecordInflector::tableize(get_class($item)) => $item->get_columns())); 386 | $includes = preg_split('/[\s,]+/', $options['include']); 387 | // get join part of query from association and column names 388 | foreach ($includes as $include) { 389 | if (isset($item->associations[$include])) { 390 | list($cols, $join) = $item->associations[$include]->join(); 391 | array_push($joins, $join); 392 | array_push($tables_to_columns, $cols); 393 | } 394 | } 395 | // set the select variable so all column names are unique 396 | $selects = array(); 397 | foreach ($tables_to_columns as $table_key => $columns) { 398 | foreach ($columns as $table => $cols) 399 | foreach ($cols as $key => $col) { 400 | array_push($selects, "$table.`$col` AS t{$table_key}_r$key"); 401 | $column_lookup["t{$table_key}_r{$key}"]["table"] = $table; 402 | $column_lookup["t{$table_key}_r{$key}"]["column"] = $col; 403 | } 404 | } 405 | $select = implode(", ", $selects); 406 | } 407 | // joins (?), include 408 | 409 | $query = "SELECT $select FROM {$item->table_name}"; 410 | $query .= (count($joins) > 0) ? " " . implode(" ", $joins) : ""; 411 | $query .= (isset($where)) ? " WHERE $where" : ""; 412 | $query .= ($options['group']) ? " GROUP BY {$options['group']}" : ""; 413 | $query .= ($options['order']) ? " ORDER BY {$options['order']}" : ""; 414 | $query .= (isset($limit) && $limit) ? " LIMIT $limit" : ""; 415 | $query .= (isset($offset) && $offset) ? " OFFSET $offset" : ""; 416 | return array('query' => $query, 'column_lookup' => $column_lookup); 417 | } 418 | 419 | public static function convert_conditions_to_where($conditions) { 420 | if (is_string($conditions)) { return " ( " .$conditions. " ) "; } 421 | /* handle both normal array with place holders 422 | and associative array */ 423 | if (is_array($conditions)) { 424 | // simple array 425 | if (reset(array_keys($conditions)) === 0 && 426 | end(array_keys($conditions)) === count($conditions) - 1 && 427 | !is_array(end($conditions))) { 428 | $condition = " ( " .array_shift($conditions). " ) "; 429 | foreach ($conditions as $value) { 430 | $value = self::quote($value); 431 | $condition = preg_replace('|\?|', $value, $condition, 1); 432 | } 433 | return $condition; 434 | } 435 | /* array starts with a key of 0 436 | next element can be an associative array of bind variables 437 | or array can continue with bind variables with keys specified */ 438 | elseif (reset(array_keys($conditions)) === 0) { 439 | $condition = " ( " .array_shift($conditions). " ) "; 440 | if (is_array(reset($conditions))) { 441 | $conditions = reset($conditions); 442 | } 443 | 444 | foreach ($conditions as $key => $value) { 445 | $value = self::quote($value); 446 | $condition = preg_replace("|:$key|", $value, $condition, 1); 447 | } 448 | return $condition; 449 | } 450 | // associative array 451 | else { 452 | $condition = " ( "; 453 | $w = array(); 454 | foreach ($conditions as $key => $value) { 455 | if (is_array($value)) { 456 | $w[] = '`' .$key. '` IN ( ' .join(", ", $value). ' )'; 457 | } 458 | elseif (is_null($value)) { 459 | $w[] = '`' .$key. '` is null'; 460 | } 461 | else { 462 | $w[] = '`' .$key. '` = '. self::quote($value); 463 | } 464 | } 465 | return $condition . join(" AND ", $w). " ) "; 466 | } 467 | } 468 | } 469 | 470 | } 471 | 472 | class ActiveRecordException extends Exception { 473 | const RecordNotFound = 0; 474 | const AttributeNotFound = 1; 475 | const UnexpectedClass = 2; 476 | const ObjectFrozen = 3; 477 | const HasManyThroughCantAssociateNewRecords = 4; 478 | const MethodOrAssocationNotFound = 5; 479 | } 480 | 481 | interface DatabaseAdapter { 482 | static function get_dbh($host="localhost", $port="3306", $db=null, $user=null, $password=null, $driver="mysql"); 483 | static function query($query, $dbh=null); 484 | static function quote($string, $dbh=null, $type=null); 485 | static function last_insert_id($dbh=null, $resource=null); 486 | } 487 | 488 | ?> 489 | --------------------------------------------------------------------------------