89765b15ae4e2373d62bbe520bfc0f6e9a5420b9
[lhc/web/wiklou.git] / includes / DBDataObject.php
1 <?php
2
3 /**
4 * Abstract base class for representing objects that are stored in some DB table.
5 * This is basically an ORM-like wrapper around rows in database tables that
6 * aims to be both simple and very flexible. It is centered around an associative
7 * array of fields and various methods to do common interaction with the database.
8 *
9 * These methods must be implemented in deriving classes:
10 * * getDBTable
11 * * getFieldPrefix
12 * * getFieldTypes
13 *
14 * These methods are likely candidates for overriding:
15 * * getDefaults
16 * * remove
17 * * insert
18 * * saveExisting
19 * * loadSummaryFields
20 * * getSummaryFields
21 *
22 * Main instance methods:
23 * * getField(s)
24 * * setField(s)
25 * * save
26 * * remove
27 *
28 * Main static methods:
29 * * select
30 * * update
31 * * delete
32 * * count
33 * * has
34 * * selectRow
35 * * selectFields
36 * * selectFieldsRow
37 *
38 * @since 1.20
39 *
40 * @file DBDataObject.php
41 *
42 * @licence GNU GPL v2 or later
43 * @author Jeroen De Dauw < jeroendedauw@gmail.com >
44 */
45 abstract class DBDataObject {
46
47 /**
48 * The fields of the object.
49 * field name (w/o prefix) => value
50 *
51 * @since 1.20
52 * @var array
53 */
54 protected $fields = array( 'id' => null );
55
56 /**
57 * @since 1.20
58 * @var DBTable
59 */
60 protected $table;
61
62 /**
63 * If the object should update summaries of linked items when changed.
64 * For example, update the course_count field in universities when a course in courses is deleted.
65 * Settings this to false can prevent needless updating work in situations
66 * such as deleting a university, which will then delete all it's courses.
67 *
68 * @since 1.20
69 * @var bool
70 */
71 protected $updateSummaries = true;
72
73 /**
74 * Indicates if the object is in summary mode.
75 * This mode indicates that only summary fields got updated,
76 * which allows for optimizations.
77 *
78 * @since 1.20
79 * @var bool
80 */
81 protected $inSummaryMode = false;
82
83 /**
84 * Constructor.
85 *
86 * @since 1.20
87 *
88 * @param DBTable $table
89 * @param array|null $fields
90 * @param boolean $loadDefaults
91 */
92 public function __construct( DBTable $table, $fields = null, $loadDefaults = false ) {
93 $this->table = $table;
94
95 if ( !is_array( $fields ) ) {
96 $fields = array();
97 }
98
99 if ( $loadDefaults ) {
100 $fields = array_merge( $this->table->getDefaults(), $fields );
101 }
102
103 $this->setFields( $fields );
104 }
105
106 /**
107 * Load the specified fields from the database.
108 *
109 * @since 1.20
110 *
111 * @param array|null $fields
112 * @param boolean $override
113 * @param boolean $skipLoaded
114 *
115 * @return bool Success indicator
116 */
117 public function loadFields( $fields = null, $override = true, $skipLoaded = false ) {
118 if ( is_null( $this->getId() ) ) {
119 return false;
120 }
121
122 if ( is_null( $fields ) ) {
123 $fields = array_keys( $this->table->getFieldTypes() );
124 }
125
126 if ( $skipLoaded ) {
127 $fields = array_diff( $fields, array_keys( $this->fields ) );
128 }
129
130 if ( count( $fields ) > 0 ) {
131 $result = $this->table->rawSelectRow(
132 $this->table->getPrefixedFields( $fields ),
133 array( $this->table->getPrefixedField( 'id' ) => $this->getId() ),
134 array( 'LIMIT' => 1 )
135 );
136
137 if ( $result !== false ) {
138 $this->setFields( $this->table->getFieldsFromDBResult( $result ), $override );
139 return true;
140 }
141
142 return false;
143 }
144
145 return true;
146 }
147
148 /**
149 * Gets the value of a field.
150 *
151 * @since 1.20
152 *
153 * @param string $name
154 * @param mixed $default
155 *
156 * @throws MWException
157 * @return mixed
158 */
159 public function getField( $name, $default = null ) {
160 if ( $this->hasField( $name ) ) {
161 return $this->fields[$name];
162 } elseif ( !is_null( $default ) ) {
163 return $default;
164 } else {
165 throw new MWException( 'Attempted to get not-set field ' . $name );
166 }
167 }
168
169 /**
170 * Gets the value of a field but first loads it if not done so already.
171 *
172 * @since 1.20
173 *
174 * @param string$name
175 *
176 * @return mixed
177 */
178 public function loadAndGetField( $name ) {
179 if ( !$this->hasField( $name ) ) {
180 $this->loadFields( array( $name ) );
181 }
182
183 return $this->getField( $name );
184 }
185
186 /**
187 * Remove a field.
188 *
189 * @since 1.20
190 *
191 * @param string $name
192 */
193 public function removeField( $name ) {
194 unset( $this->fields[$name] );
195 }
196
197 /**
198 * Returns the objects database id.
199 *
200 * @since 1.20
201 *
202 * @return integer|null
203 */
204 public function getId() {
205 return $this->getField( 'id' );
206 }
207
208 /**
209 * Sets the objects database id.
210 *
211 * @since 1.20
212 *
213 * @param integer|null $id
214 */
215 public function setId( $id ) {
216 return $this->setField( 'id', $id );
217 }
218
219 /**
220 * Gets if a certain field is set.
221 *
222 * @since 1.20
223 *
224 * @param string $name
225 *
226 * @return boolean
227 */
228 public function hasField( $name ) {
229 return array_key_exists( $name, $this->fields );
230 }
231
232 /**
233 * Gets if the id field is set.
234 *
235 * @since 1.20
236 *
237 * @return boolean
238 */
239 public function hasIdField() {
240 return $this->hasField( 'id' )
241 && !is_null( $this->getField( 'id' ) );
242 }
243
244 /**
245 * Sets multiple fields.
246 *
247 * @since 1.20
248 *
249 * @param array $fields The fields to set
250 * @param boolean $override Override already set fields with the provided values?
251 */
252 public function setFields( array $fields, $override = true ) {
253 foreach ( $fields as $name => $value ) {
254 if ( $override || !$this->hasField( $name ) ) {
255 $this->setField( $name, $value );
256 }
257 }
258 }
259
260 /**
261 * Gets the fields => values to write to the table.
262 *
263 * @since 1.20
264 *
265 * @return array
266 */
267 protected function getWriteValues() {
268 $values = array();
269
270 foreach ( $this->table->getFieldTypes() as $name => $type ) {
271 if ( array_key_exists( $name, $this->fields ) ) {
272 $value = $this->fields[$name];
273
274 switch ( $type ) {
275 case 'array':
276 $value = (array)$value;
277 case 'blob':
278 $value = serialize( $value );
279 }
280
281 $values[$this->table->getPrefixedField( $name )] = $value;
282 }
283 }
284
285 return $values;
286 }
287
288 /**
289 * Serializes the object to an associative array which
290 * can then easily be converted into JSON or similar.
291 *
292 * @since 1.20
293 *
294 * @param null|array $fields
295 * @param boolean $incNullId
296 *
297 * @return array
298 */
299 public function toArray( $fields = null, $incNullId = false ) {
300 $data = array();
301 $setFields = array();
302
303 if ( !is_array( $fields ) ) {
304 $setFields = $this->getSetFieldNames();
305 } else {
306 foreach ( $fields as $field ) {
307 if ( $this->hasField( $field ) ) {
308 $setFields[] = $field;
309 }
310 }
311 }
312
313 foreach ( $setFields as $field ) {
314 if ( $incNullId || $field != 'id' || $this->hasIdField() ) {
315 $data[$field] = $this->getField( $field );
316 }
317 }
318
319 return $data;
320 }
321
322 /**
323 * Load the default values, via getDefaults.
324 *
325 * @since 1.20
326 *
327 * @param boolean $override
328 */
329 public function loadDefaults( $override = true ) {
330 $this->setFields( $this->table->getDefaults(), $override );
331 }
332
333 /**
334 * Writes the answer to the database, either updating it
335 * when it already exists, or inserting it when it doesn't.
336 *
337 * @since 1.20
338 *
339 * @return boolean Success indicator
340 */
341 public function save() {
342 if ( $this->hasIdField() ) {
343 return $this->saveExisting();
344 } else {
345 return $this->insert();
346 }
347 }
348
349 /**
350 * Updates the object in the database.
351 *
352 * @since 1.20
353 *
354 * @return boolean Success indicator
355 */
356 protected function saveExisting() {
357 $dbw = wfGetDB( DB_MASTER );
358
359 $success = $dbw->update(
360 $this->table->getDBTable(),
361 $this->getWriteValues(),
362 array( $this->table->getPrefixedField( 'id' ) => $this->getId() ),
363 __METHOD__
364 );
365
366 return $success;
367 }
368
369 /**
370 * Inserts the object into the database.
371 *
372 * @since 1.20
373 *
374 * @return boolean Success indicator
375 */
376 protected function insert() {
377 $dbw = wfGetDB( DB_MASTER );
378
379 $result = $dbw->insert(
380 $this->table->getDBTable(),
381 $this->getWriteValues(),
382 __METHOD__,
383 array( 'IGNORE' )
384 );
385
386 if ( $result ) {
387 $this->setField( 'id', $dbw->insertId() );
388 }
389
390 return $result;
391 }
392
393 /**
394 * Removes the object from the database.
395 *
396 * @since 1.20
397 *
398 * @return boolean Success indicator
399 */
400 public function remove() {
401 $this->beforeRemove();
402
403 $success = $this->table->delete( array( 'id' => $this->getId() ) );
404
405 if ( $success ) {
406 $this->onRemoved();
407 }
408
409 return $success;
410 }
411
412 /**
413 * Gets called before an object is removed from the database.
414 *
415 * @since 1.20
416 */
417 protected function beforeRemove() {
418 $this->loadFields( $this->getBeforeRemoveFields(), false, true );
419 }
420
421 /**
422 * Before removal of an object happens, @see beforeRemove gets called.
423 * This method loads the fields of which the names have been returned by this one (or all fields if null is returned).
424 * This allows for loading info needed after removal to get rid of linked data and the like.
425 *
426 * @since 1.20
427 *
428 * @return array|null
429 */
430 protected function getBeforeRemoveFields() {
431 return array();
432 }
433
434 /**
435 * Gets called after successfull removal.
436 * Can be overriden to get rid of linked data.
437 *
438 * @since 1.20
439 */
440 protected function onRemoved() {
441 $this->setField( 'id', null );
442 }
443
444 /**
445 * Return the names and values of the fields.
446 *
447 * @since 1.20
448 *
449 * @return array
450 */
451 public function getFields() {
452 return $this->fields;
453 }
454
455 /**
456 * Return the names of the fields.
457 *
458 * @since 1.20
459 *
460 * @return array
461 */
462 public function getSetFieldNames() {
463 return array_keys( $this->fields );
464 }
465
466 /**
467 * Sets the value of a field.
468 * Strings can be provided for other types,
469 * so this method can be called from unserialization handlers.
470 *
471 * @since 1.20
472 *
473 * @param string $name
474 * @param mixed $value
475 *
476 * @throws MWException
477 */
478 public function setField( $name, $value ) {
479 $fields = $this->table->getFieldTypes();
480
481 if ( array_key_exists( $name, $fields ) ) {
482 switch ( $fields[$name] ) {
483 case 'int':
484 $value = (int)$value;
485 break;
486 case 'float':
487 $value = (float)$value;
488 break;
489 case 'bool':
490 if ( is_string( $value ) ) {
491 $value = $value !== '0';
492 } elseif ( is_int( $value ) ) {
493 $value = $value !== 0;
494 }
495 break;
496 case 'array':
497 if ( is_string( $value ) ) {
498 $value = unserialize( $value );
499 }
500
501 if ( !is_array( $value ) ) {
502 $value = array();
503 }
504 break;
505 case 'blob':
506 if ( is_string( $value ) ) {
507 $value = unserialize( $value );
508 }
509 break;
510 case 'id':
511 if ( is_string( $value ) ) {
512 $value = (int)$value;
513 }
514 break;
515 }
516
517 $this->fields[$name] = $value;
518 } else {
519 throw new MWException( 'Attempted to set unknown field ' . $name );
520 }
521 }
522
523 /**
524 * Add an amount (can be negative) to the specified field (needs to be numeric).
525 *
526 * @since 1.20
527 *
528 * @param string $field
529 * @param integer $amount
530 *
531 * @return boolean Success indicator
532 */
533 public function addToField( $field, $amount ) {
534 if ( $amount == 0 ) {
535 return true;
536 }
537
538 if ( !$this->hasIdField() ) {
539 return false;
540 }
541
542 $absoluteAmount = abs( $amount );
543 $isNegative = $amount < 0;
544
545 $dbw = wfGetDB( DB_MASTER );
546
547 $fullField = $this->table->getPrefixedField( $field );
548
549 $success = $dbw->update(
550 $this->table->getDBTable(),
551 array( "$fullField=$fullField" . ( $isNegative ? '-' : '+' ) . $absoluteAmount ),
552 array( $this->table->getPrefixedField( 'id' ) => $this->getId() ),
553 __METHOD__
554 );
555
556 if ( $success && $this->hasField( $field ) ) {
557 $this->setField( $field, $this->getField( $field ) + $amount );
558 }
559
560 return $success;
561 }
562
563 /**
564 * Return the names of the fields.
565 *
566 * @since 1.20
567 *
568 * @return array
569 */
570 public function getFieldNames() {
571 return array_keys( $this->table->getFieldTypes() );
572 }
573
574 /**
575 * Computes and updates the values of the summary fields.
576 *
577 * @since 1.20
578 *
579 * @param array|string|null $summaryFields
580 */
581 public function loadSummaryFields( $summaryFields = null ) {
582
583 }
584
585 /**
586 * Sets the value for the @see $updateSummaries field.
587 *
588 * @since 1.20
589 *
590 * @param boolean $update
591 */
592 public function setUpdateSummaries( $update ) {
593 $this->updateSummaries = $update;
594 }
595
596 /**
597 * Sets the value for the @see $inSummaryMode field.
598 *
599 * @since 1.20
600 *
601 * @param boolean $summaryMode
602 */
603 public function setSummaryMode( $summaryMode ) {
604 $this->inSummaryMode = $summaryMode;
605 }
606
607 /**
608 * Return if any fields got changed.
609 *
610 * @since 1.20
611 *
612 * @param DBDataObject $object
613 * @param boolean $excludeSummaryFields When set to true, summary field changes are ignored.
614 *
615 * @return boolean
616 */
617 protected function fieldsChanged( DBDataObject $object, $excludeSummaryFields = false ) {
618 foreach ( $this->fields as $name => $value ) {
619 $excluded = $excludeSummaryFields && in_array( $name, $this->table->getSummaryFields() );
620
621 if ( !$excluded && $object->getField( $name ) !== $value ) {
622 return true;
623 }
624 }
625
626 return false;
627 }
628
629 /**
630 * Returns the table this DBDataObject is a row in.
631 *
632 * @since 1.20
633 *
634 * @return DBTable
635 */
636 public function getTable() {
637 return $this->table;
638 }
639
640 }