A Solid Single File PHP ORM

Published by Ryan on

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
191
192
193
194
195
196
197
198
199
200
201
202
<?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.");

        /*
         * Create a unique lookup tree for this instance in global space.
                 * DEPRECATED
         */

        $GLOBALS["r2increm"]++;


        // Check to see that parent has been set. After the root,
        // all children should be set.
        if(!$this->record) {
            // Set depth if this is the root
            $this->depth  = $depth;
            $this->record = get_class($this).".".$GLOBALS["r2increm"].".".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,
                         * Lets 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 compaitibility.... 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
21
22
23
<?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
15
16
<?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.

Categories: Back End

Leave a Reply

Your email address will not be published. Required fields are marked *