Add data\Model from phalanx
[hoplite.git] / data / model.php
1 <?php
2 // Hoplite
3 // Copyright (c) 2011 Blue Static
4 //
5 // This program is free software: you can redistribute it and/or modify it
6 // under the terms of the GNU General Public License as published by the Free
7 // Software Foundation, either version 3 of the License, or any later version.
8 //
9 // This program is distributed in the hope that it will be useful, but WITHOUT
10 // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 // FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
12 // more details.
13 //
14 // You should have received a copy of the GNU General Public License along with
15 // this program. If not, see <http://www.gnu.org/licenses/>.
16
17 namespace hoplite\data;
18
19 require_once HOPLITE_ROOT . '/base/struct.php';
20
21 /*!
22 A Model represents a single instance of a database row and its relations.
23 It inherits the strict data member policy from base\Struct and maintains
24 a condition by which it fetches and updates data. The role of the Model
25 class is not to validate data, but to persist it. Validation is the job
26 of the Controller (in phalanx, that is a Task object); the Model will
27 persist the data and help access related information.
28
29 This class requires the use of a PDO object.
30 */
31 class Model extends \hoplite\base\Struct
32 {
33 /*! @var PDO The object the model will use when performing operations. */
34 static protected $db = NULL;
35
36 /*! @var string The string prefix to put in front of the table name. */
37 protected $table_prefix = '';
38
39 /*! @var string The name of the database table to which the object belongs. */
40 protected $table = 'table';
41
42 /*! @var string The condition to select this data object by. Parameters should
43 be keyed using :keyname syntax.
44 */
45 protected $condition = 'pkey = :pkey';
46
47 /*! @var string The name of the field(s) that provide the primary key. This
48 can either be a single string or an array for a compound key.
49
50 A word on primary keys: This class assumes that if the primary key is
51 singular (defined as a string), then it is auto-increment. The
52 implications of this are that the set primary key value is igorned on
53 inserts and upates. If you do not want this behavior for a table with a
54 truly singluar primary key, define it as a plural/compound primary key
55 in the Model (defined as an array).
56 */
57 protected $primary_key = 'pkey';
58
59 public function condition() { return $this->condition; }
60 public function set_condition($cond) { $this->condition = $cond; }
61
62 static public function db() { return self::$db; }
63 static public function set_db(\PDO $db) { self::$db = $db; }
64
65 /*!
66 Constructor. This takes in either the value(s) to substitute into the
67 |$this->condition| or NULL to create a new instance of the model.
68 */
69 public function __construct($condition_data = NULL)
70 {
71 if (is_array($condition_data)) {
72 if (!is_array($this->primary_key))
73 throw new ModelException('Cannot create ' . get_class($this) . ' with an array when primary key is singular.');
74
75 foreach ($condition_data as $key => $value)
76 if (in_array($key, $this->primary_key))
77 $this->Set($key, $value);
78 } else if (!is_null($condition_data)) {
79 if (is_array($this->primary_key))
80 throw new ModelException('Cannot create ' . get_class($this) . ' when a singular value is given for a compound primary key.');
81 $this->Set($this->primary_key, $condition_data);
82 }
83
84 $this->table = $this->table_prefix . $this->table;
85 }
86
87 /*! Fetches an object and returns the result based on the |$this->condition|. */
88 public function Fetch()
89 {
90 $stmt = self::$db->Prepare("SELECT * FROM {$this->table} WHERE " . $this->condition());
91 $stmt->Execute($this->_GetSQLParams($stmt));
92 $result = $stmt->FetchObject();
93 if (!$result)
94 throw new ModelException("Could not fetch " . get_class($this));
95 return $result;
96 }
97
98 /*!
99 Fetches an object and stores the result in the model, overwriting existing
100 data values.
101 */
102 public function FetchInto()
103 {
104 $this->SetFrom($this->Fetch());
105 }
106
107 /*!
108 Inserts the new model into the database. This will explicitly filter out
109 primary key information if it is a singular key.
110 */
111 public function Insert()
112 {
113 $data = $this->ToArray();
114 if (!is_array($this->primary_key))
115 unset($data[$this->primary_key]);
116
117 $keys = array_keys($data);
118 $placeholders = array_map(function ($s) { return ":$s"; }, $keys);
119 $stmt = self::$db->Prepare("
120 INSERT INTO {$this->table}
121 (" . implode(', ', $keys) . ")
122 VALUES
123 (" . implode(', ', $placeholders) . ")
124 ");
125 $stmt->Execute($data);
126
127 if (!is_array($this->primary_key))
128 $this->Set($this->primary_key, self::$db->LastInsertID());
129 }
130
131 /*! Updates the database based on the values set in the model. */
132 public function Update()
133 {
134 $updates = array_map(function($s) { return "$s = :$s"; }, array_keys($this->ToArray()));
135 $condition = $this->condition();
136 $stmt = self::$db->Prepare("UPDATE {$this->table} SET " . implode(', ', $updates) . " WHERE $condition");
137 $stmt->Execute($this->_GetSQLParams($stmt));
138 }
139
140 /*! Deletes a record in the database based on the set condition. */
141 public function Delete()
142 {
143 $stmt = self::$db->Prepare("DELETE FROM {$this->table} WHERE " . $this->condition());
144 $stmt->Execute($this->_GetSQLParams($stmt));
145 }
146
147 /*!
148 Returns a subest of |$this->data| that is required to execute a given
149 PDOStatement. This will only work on queries whose parameters are
150 specified using :name syntax. It will filter all values that are not
151 used in the query.
152 */
153 protected function _GetSQLParams(\PDOStatement $stmt)
154 {
155 $matches = array();
156 preg_match_all('/\:([a-z0-9_\-]+)/i', $stmt->queryString, $matches);
157 $params = array();
158 $data = $this->ToArray();
159
160 foreach ($matches[1] as $key)
161 $params[$key] = $data[$key];
162
163 return $params;
164 }
165
166 /*!
167 Returns an array of Model objects of the correct type based on a group
168 condition. If no condition is specified, returns all results in the table.
169 If the |$params| argument is not an array, it will be assumed to be a
170 single value and will be converted to an array.
171 */
172 static public function FetchGroup($condition = '', $params = array())
173 {
174 $class = get_called_class(); // Late static binding.
175 $props = new $class();
176 $sql = "SELECT * FROM {$props->table_prefix}{$props->table}";
177 if ($condition)
178 $sql .= " WHERE $condition";
179 $stmt = self::$db->Prepare($sql);
180
181 if (!is_array($params))
182 $params = array($params);
183 $stmt->Execute($params);
184
185 $results = array();
186 while ($obj = $stmt->FetchObject()) {
187 $model = new $class();
188 $model->SetFrom($obj);
189 $results[] = $model;
190 }
191
192 return $results;
193 }
194 }
195
196 class ModelException extends \Exception {}