A Single File PHP ORM
In most frameworks, ORMs like say… Eloquent, do a fair job extracting and abstracting the database into objects programmers use, but also generally come with a super steep learning curve. Back in 2014, I set out to create an abstraction that would turn MySQL tables and rows (relations and records for the versed) into an easily accessible object-oriented code construct with just some basic table assumptions:
- All tables/relations have a unique, primary, incrementing “id” column
- (Optional) For soft-delete, relations have “data” column where “deleted” is an enumerated option or char/varchar type
- (Optional) For columns that establish 1-n relations, the column name matches its associated table name
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 | <?php /** * A good, simple active record class. Extending this class * (correctly) gives children powerful active-record fetching * capability. Without compromising too many OOP principles. * * @author Ryan Rodd * @since 2015-02-18 */ class Record { public $id; public $object; public $attributes; private $attributesDB; private $record = false; private $depth = 3; private $runmax = 25; private $exclude = []; /** * The constructor is reponsible for fetching. And * maybe later, saving... Also declared final so it * can't be overwritten. If we declare depth in construct, * it overrides native/default depth of 3 * @author Ryan Rodd */ public final function __construct($id,$record=false,$depth=0,$exclude=[]) { global $db; /* * Set some vars */ $this->id = $id; $this->record = $record; $this->exclude = array_merge($this->exclude,$exclude); /* * Divine object name from class name if $this->object isn't given */ if(!$this->object) $this->object = strtolower(get_class($this)."s"); /* * Check that we have data and code needed for * this record and no collisions */ if(!$id) throw new Exception(get_class($this)."->constructor: "."Please specify an \$id"); /* * Check that we have data and code needed for * this record and no collisions. If attributes weren't provided, * we will use class vars that validate agains the db table */ if(empty($this->attributes)) $this->attributes = get_object_vars($this); else{ // Turn mixed array into flipped key values // to bring everything to even keel $temp=[]; foreach($this->attributes as $k=>$v) { if(!is_int($k)) $temp[$k] = $v; else $temp[$v] = ''; } $this->attributes = $temp; } $this->attributes = $db->against($this->attributes,$this->object); /* * Check for collisions */ foreach($this->attributes as $attr) if(in_array($attr,["id","object","record","depth","runmax"])) throw new Exception(get_class($this)."->constructor: " ."Cannot create attrib: $attr. Collides with Record class var."); // Check to see that parent has been set. After the root, // all children should be set. Global behaviour deprecated if(!$this->record) { // Set depth if this is the root $this->depth = $depth; $this->record = get_class($this).".".rand(0,pow(2,20)); $GLOBALS[$this->record]=[]; } array_push($GLOBALS[$this->record],get_class($this)); /* * Record maybe in cache, TODO: search cache and return * for now lets go to the database. First parse attributes * for easy database consumption */ foreach($this->attributes as $k=>$attr) { if($k) $this->attributesDB[$k] = "`".$k."`"; } /* * Run the query. Assumes all data in relations have * a unique primary key int column named "id" */ $sql = "SELECT ".implode(",",$this->attributesDB)." FROM ".$this->object." WHERE id={$id};"; if($res=@reset($db->query($sql))) { foreach($this->attributes as $attr=>$ref) { $val = $res[$attr]; // Create and fill obj attribute with DB val $this->{$attr} = $val; $class = @ucwords(($ref)?$ref:$attr); /* * Check to see if a model exists for this data attribute * and include obj details rather than the key val. Check * depth and exlusion classes to prevent runaway recursion */ if(class_exists($class) && $depth !== false && $depth <= $this->depth && $val && sizeof($GLOBALS[$this->record]) < $this->runmax && !in_array($class,$exclude)) { if(get_parent_class($this)!=$class) { /* * Add the new object. If new objects extend Record, * then the recursive build magic continues until depth * is reached */ $this->{$attr} = new $class($val,$this->record,$depth+1,$this->exclude); } else { /* * If this is a child of a parent that extends Record, * Let's restore/instate inherited attributes. Downside is * attributes will be public by default */ $parent = new $class($val,$this->record,$depth+1); foreach(get_object_vars($parent) as $k=>$var) { $this->{$k} = $var; } } } } } /* * Lastly, if our child has an init, lets call it as if * it were a constructor */ if(method_exists($this,"init")) { // Call fetch call_user_func_array([$this,"init"],[$id,$record,$depth,$this->exclude]); } // Clean up the object for data dumps unset($this->attributes,$this->attributesDB); } /** * Also for backward compatibility... but this may stay */ public function delete($hard = false) { global $db; // See if we have a data column if($struct = $db->query("SHOW COLUMNS FROM ".$this->object)) { foreach($struct as $row) $cols[] = $row['Field']; } // Usually, just update data col if(in_array("data",$cols) && !$hard) $db->update(array( "data" => "deleted" ),$this->object,$this->id); else $db->delete($this->id,$this->object); return true; } // Backward compatibility.... where'd this go? public function unCache() {} public function cache() {} } ?> |
Implementation
Lets say we have a database with a users table that has first_name, last_name, and email columns. With the Record class in our codebase, all we have to do is create a class that matches our table name and extends Record. Like so:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | <?php include_once "Record.php"; /* * Object for users */ class User extends Record { public $first_name; public $last_name; public $email; /* * Choose the form of your constructor */ public function init() { // Code in this block will behave like a constructor // and be called automatically. } } ?> |
Result
Now that we’ve followed our assumptions and extended the Record class, our object has some cool features:
- Column names get mapped to class attributes (table.column gets mapped to $table->column)
- Columns with matching table names automatically JOIN that table data, making it available as a child object
Now in controller pattern code we can use the user object like so:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | <?php include_once "User.php"; $user = new User($id); print_r($user); /* will output a data structure like: Array ( [first_name] => Ryan [last_name] => Rodd [email] => my@email.com ) */ ?> |
No doubt, modern ORMs are feature packed, highly developed, and safe to use, but sometimes nothing beats the simplicity of 3 assumptions and a single drop in file.
0 Comments