Commit | Line | Data |
---|---|---|
26d00fe5 FB |
1 | <?php |
2 | /*************************************************************************** | |
3 | * Copyright (C) 2003-2010 Polytechnique.org * | |
4 | * http://opensource.polytechnique.org/ * | |
5 | * * | |
6 | * This program is free software; you can redistribute it and/or modify * | |
7 | * it under the terms of the GNU General Public License as published by * | |
8 | * the Free Software Foundation; either version 2 of the License, or * | |
9 | * (at your option) any later version. * | |
10 | * * | |
11 | * This program is distributed in the hope that it will be useful, * | |
12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of * | |
13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * | |
14 | * GNU General Public License for more details. * | |
15 | * * | |
16 | * You should have received a copy of the GNU General Public License * | |
17 | * along with this program; if not, write to the Free Software * | |
18 | * Foundation, Inc., * | |
19 | * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * | |
20 | ***************************************************************************/ | |
21 | ||
22 | class PlDBBadValueException extends PlException | |
23 | { | |
24 | public function __construct($value, PlDBTableField $field, $reason) | |
25 | { | |
26 | parent::__construct('Erreur lors de l\'accès à la base de données', | |
27 | 'Illegal value '. (is_null($value) ? '(null)' : '(\'' . $value . '\')') | |
28 | . ' for field (\'' . $field->table->table . '.' . $field->name . '\'): ' | |
29 | . $reason); | |
30 | } | |
31 | } | |
32 | ||
33 | class PlDBNoSuchFieldException extends PlException | |
34 | { | |
35 | public function __construct($field, PlDBTable $table) | |
36 | { | |
37 | parent::__construct('Erreur lors de l\'accès à la base de données', | |
38 | 'No such field ' . $field . ' in table ' . $table->table); | |
39 | } | |
40 | } | |
41 | ||
42 | class PlDBIncompleteEntryDescription extends PlException | |
43 | { | |
44 | public function __construct($field, PlDBTable $table) | |
45 | { | |
46 | parent::__construct('Erreur lors de l\'accès à la base de données', | |
47 | 'The field ' . $field . ' is required to describe an entry in table ' | |
48 | . $table->table); | |
49 | } | |
50 | } | |
51 | ||
52 | class PlDBTableField | |
53 | { | |
54 | public $table; | |
55 | ||
56 | public $name; | |
57 | public $inPrimaryKey; | |
58 | public $inUniqueKey; | |
59 | public $inKey; | |
60 | ||
61 | public $type; | |
62 | public $typeLength; | |
63 | public $typeParameters; | |
64 | ||
65 | public $allowNull; | |
66 | public $defaultValue; | |
67 | public $autoIncrement; | |
68 | ||
69 | public function __construct(array $column) | |
70 | { | |
71 | $this->name = $column['Field']; | |
72 | $this->typeParameters = explode(' ', str_replace(array('(', ')', ',', '\''), ' ', | |
73 | $column['Type'])); | |
74 | $this->type = array_shift($this->typeParameters); | |
75 | if ($this->type == 'enum' || $this->type == 'set') { | |
76 | $this->typeParameters = new PlFlagSet(implode(',', $this->typeParameters)); | |
77 | } else if (ctype_digit($this->typeParameters[0])) { | |
78 | $this->typeLength = intval($this->typeParameters[0]); | |
79 | array_shift($this->typeParameters); | |
80 | } | |
81 | $this->allowNull = ($column['Null'] === 'YES'); | |
82 | $this->autoIncrement = (strpos($column['Extra'], 'auto_increment') !== false); | |
83 | $this->inPrimaryKey = ($column['Key'] === 'PRI'); | |
84 | $this->inUniqueKey = $this->inPrimaryKey || ($column['Key'] === 'UNI'); | |
85 | $this->inKey = $this->inUniqueKey || ($column['Key'] === 'MUL'); | |
86 | ||
87 | try { | |
88 | $this->defaultValue = $this->format($column['Default']); | |
89 | } catch (PlDBBadValueException $e) { | |
90 | $this->defaultValue = null; | |
91 | } | |
92 | } | |
93 | ||
94 | public function format($value, $badNullFallbackToDefault = false) | |
95 | { | |
96 | if (is_null($value)) { | |
97 | if ($this->allowNull || $this->autoIncrement) { | |
98 | return $value; | |
99 | } | |
100 | if ($badNullFallbackToDefault) { | |
101 | return $this->defaultValue; | |
102 | } | |
103 | throw new PlDBBadValueException($value, $this, 'null not allowed'); | |
104 | } else if ($this->type == 'enum') { | |
105 | if (!$this->typeParameters->hasFlag($value)) { | |
106 | throw new PlDBBadValueException($value, $this, 'invalid value for enum ' . $this->typeParameters->flags()); | |
107 | } | |
108 | return $value; | |
109 | } else if ($this->type == 'set') { | |
110 | $value = new PlFlagSet($value); | |
111 | foreach ($value as $flag) { | |
112 | if (!$this->typeParameters->hasFlag($flag)) { | |
113 | throw new PlDBBadValueException($value, $this, 'invalid flag for set ' . $this->typeParameters->flags()); | |
114 | } | |
115 | } | |
116 | return $value; | |
117 | } else if (ends_with($this->type, 'int')) { | |
118 | if (!is_int($value) && !ctype_digit($value)) { | |
119 | throw new PlDBBadValueException($value, $this, 'value is not an integer'); | |
120 | } | |
121 | $value = intval($value); | |
122 | if (count($this->typeParameters) > 0 && $this->typeParameters[0] == 'unsigned') { | |
123 | if ($value < 0) { | |
124 | throw new PlDBBadValueException($value, $this, 'value is negative in an unsigned field'); | |
125 | } | |
126 | } | |
127 | /* TODO: Check bounds */ | |
128 | return $value; | |
129 | } else if ($this->type == 'varchar') { | |
130 | if (strlen($value) > $this->typeLength) { | |
131 | throw new PlDBBadValueException($value, $this, 'value is expected to be at most ' . $this->typeLength . ' characters long, ' . strlen($value) . ' given'); | |
132 | } | |
133 | return $value; | |
134 | } else if ($this->type == 'char') { | |
135 | if (strlen($value) != $this->typeLength) { | |
136 | throw new PlDBBadValueException($value, $this, 'value is expected to be ' . $this->typeLength . ' characters long, ' . strlen($value) . ' given'); | |
137 | } | |
138 | return $value; | |
139 | } | |
140 | /* TODO: Support data and times */ | |
141 | return $value; | |
142 | } | |
143 | } | |
144 | ||
145 | ||
146 | /** This class aims at providing a simple interface to interact with a single | |
147 | * table of a database. It is implemented as a wrapper around XDB. | |
148 | */ | |
149 | class PlDBTable | |
150 | { | |
151 | public $table; | |
152 | ||
153 | private $schema; | |
154 | private $keyFields; | |
155 | private $mutableFields; | |
156 | ||
157 | public function __construct($table) | |
158 | { | |
159 | $this->table = $table; | |
160 | $this->schema(); | |
161 | } | |
162 | ||
163 | private function parseSchema(PlIterator $schema) | |
164 | { | |
165 | $this->schema = array(); | |
166 | $this->keyFields = array(); | |
167 | $this->mutableFields = array(); | |
168 | while ($column = $schema->next()) { | |
169 | $field = new PlDBTableField($column); | |
170 | $this->schema[$field->name] = $field; | |
171 | if ($field->inPrimaryKey) { | |
172 | $this->keyFields[] = $field->name; | |
173 | } else { | |
174 | $this->mutableFields[] = $field->name; | |
175 | } | |
176 | } | |
177 | } | |
178 | ||
179 | ||
180 | private function schema() | |
181 | { | |
182 | if (!$this->schema) { | |
183 | $schema = XDB::iterator('DESCRIBE ' . $this->table); | |
184 | $this->parseSchema($schema); | |
185 | } | |
186 | return $this->schema; | |
187 | } | |
188 | ||
189 | private function field($field) | |
190 | { | |
191 | $schema = $this->schema(); | |
192 | if (!isset($schema[$field])) { | |
193 | throw new PlDBNoSuchFieldException($field, $this); | |
194 | } | |
195 | return $schema[$field]; | |
196 | } | |
197 | ||
198 | public function formatField($field, $value) | |
199 | { | |
200 | return $this->field($field)->format($value); | |
201 | } | |
202 | ||
203 | public function defaultValue($field) | |
204 | { | |
205 | return $this->field($field)->defaultValue; | |
206 | } | |
207 | ||
208 | public function primaryKey(PlDBTableEntry $entry) | |
209 | { | |
210 | $key = array(); | |
211 | foreach ($this->keyFields as $field) { | |
212 | if (!isset($entry->$field)) { | |
213 | throw new PlDBIncompleteEntryDescription($field, $this); | |
214 | } else { | |
215 | $key[] = XDB::escape($this->$field); | |
216 | } | |
217 | } | |
218 | return implode('-', $key); | |
219 | } | |
220 | ||
221 | private function buildKeyCondition(PlDBTableEntry $entry, $allowIncomplete) | |
222 | { | |
223 | $condition = array(); | |
224 | foreach ($this->keyFields as $field) { | |
225 | if (!isset($entry->$field)) { | |
226 | if (!$allowIncomplete) { | |
227 | throw new PlDBIncompleteEntryDescription($field, $this); | |
228 | } | |
229 | } else { | |
230 | $condition[] = XDB::format($field . ' = {?}', $entry->$field); | |
231 | } | |
232 | } | |
233 | return implode(' AND ', $condition); | |
234 | } | |
235 | ||
236 | public function fetchEntry(PlDBTableEntry $entry) | |
237 | { | |
238 | $result = XDB::rawFetchOneAssoc('SELECT * | |
239 | FROM ' . $this->table . ' | |
240 | WHERE ' . $this->buildKeyCondition($entry, false)); | |
241 | if (!$result) { | |
242 | return false; | |
243 | } | |
244 | return $entry->fillFromDBData($result); | |
245 | } | |
246 | ||
247 | public function iterateOnEntry(PlDBTableEntry $entry) | |
248 | { | |
249 | $it = XDB::rawIterator('SELECT * | |
250 | FROM ' . $this->table . ' | |
251 | WHERE ' . $this->buildKeyCondition($entry, true)); | |
252 | return PlIteratorUtils::map($it, array($entry, 'cloneAndFillFromDBData')); | |
253 | } | |
254 | ||
255 | public function updateEntry(PlDBTableEntry $entry) | |
256 | { | |
257 | $values = array(); | |
258 | foreach ($this->mutableFields as $field) { | |
259 | if ($entry->hasChanged($field)) { | |
260 | $values[] = XDB::format($field . ' = {?}', $entry->$field); | |
261 | } | |
262 | } | |
263 | if (count($values) > 0) { | |
264 | XDB::rawExecute('UPDATE ' . $this->table . ' | |
265 | SET ' . implode(', ', $values) . ' | |
266 | WHERE ' . $this->buildKeyCondition($entry, false)); | |
267 | } | |
268 | } | |
269 | ||
270 | public static function get($name) | |
271 | { | |
272 | var_dump('blah'); | |
273 | return new PlDBTable($name); | |
274 | } | |
275 | } | |
276 | ||
277 | class PlDBTableEntry extends PlAbstractIterable | |
278 | { | |
279 | private $table; | |
280 | private $changed; | |
281 | private $fetched = false; | |
282 | private $autoFetch; | |
283 | ||
284 | private $data = array(); | |
285 | ||
286 | public function __construct($table, $autoFetch = false) | |
287 | { | |
288 | if ($table instanceof PlDBTable) { | |
289 | $this->table = $table; | |
290 | } else { | |
291 | $this->table = PlCache::getGlobal('pldbtable_' . $table, array('PlDBTable', 'get'), array($table)); | |
292 | } | |
293 | $this->autoFetch = $autoFetch; | |
294 | $this->changed = new PlFlagSet(); | |
295 | } | |
296 | ||
297 | /** This hook is called when the entry is going to be updated in the db. | |
298 | * | |
299 | * A typical usecase is a class that stores low-level representation of | |
300 | * an object in db and perform a conversion between this low-level representation | |
301 | * and a higher-level representation. | |
302 | * | |
303 | * @return true in case of success | |
304 | */ | |
305 | protected function preSave() | |
306 | { | |
307 | return true; | |
308 | } | |
309 | ||
310 | /** This hook is called when the entry has just been fetched from the db. | |
311 | * | |
312 | * This is the counterpart of @ref preSave and a typical use-case is the conversion | |
313 | * from a high-level representation of the objet to a representation suitable for | |
314 | * storage in the database. | |
315 | * | |
316 | * @return true in case of success. | |
317 | */ | |
318 | protected function postFetch() | |
319 | { | |
320 | return true; | |
321 | } | |
322 | ||
323 | public function __get($field) | |
324 | { | |
325 | if (isset($this->data[$field])) { | |
326 | return $this->data[$field]; | |
327 | } else if (!$this->fetched && $this->autoFetch) { | |
328 | $this->fetch(); | |
329 | if (isset($this->data[$field])) { | |
330 | return $this->data[$field]; | |
331 | } | |
332 | } | |
333 | return $this->table->defaultValue($field); | |
334 | } | |
335 | ||
336 | public function __set($field, $value) | |
337 | { | |
338 | $this->data[$field] = $this->table->formatField($field, $value); | |
339 | $this->changed->addFlag($field); | |
340 | } | |
341 | ||
342 | public function __isset($field) | |
343 | { | |
344 | return isset($this->data[$field]); | |
345 | } | |
346 | ||
347 | public function primaryKey() | |
348 | { | |
349 | $this->table->primaryKey($this); | |
350 | } | |
351 | ||
352 | public function hasChanged($field) | |
353 | { | |
354 | return $this->changed->hasFlag($field); | |
355 | } | |
356 | ||
357 | public function fillFromArray(array $data) | |
358 | { | |
359 | foreach ($data as $field => $value) { | |
360 | $this->$field = $value; | |
361 | } | |
362 | } | |
363 | ||
364 | public function fillFromDBData(array $data) | |
365 | { | |
366 | $this->fillFromArray($data); | |
367 | $this->changed->clear(); | |
368 | return $this->postFetch(); | |
369 | } | |
370 | ||
371 | public function cloneAndFillFromDBData(array $data) | |
372 | { | |
373 | $clone = clone $this; | |
374 | $clone->fillFromDBData($data); | |
375 | return $clone; | |
376 | } | |
377 | ||
378 | public function fetch() | |
379 | { | |
380 | return $this->table->fetchEntry($this); | |
381 | } | |
382 | ||
383 | public function iterate() | |
384 | { | |
385 | return $this->table->iterateOnEntry($this); | |
386 | } | |
387 | ||
388 | public function save() | |
389 | { | |
390 | if (!$this->preSave()) { | |
391 | return false; | |
392 | } | |
393 | $this->table->updateEntry($this); | |
394 | $this->changed->clear(); | |
395 | return true; | |
396 | } | |
397 | } | |
398 | ||
399 | // vim:set et sw=4 sts=4 sws=4 foldmethod=marker enc=utf-8: | |
400 | ?> |