3fa528516f8dbacdd46799ffcb230b299281f6c4
[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 * The database connection to use for read operations.
85 * Can be changed via @see setReadDb.
86 *
87 * @since 1.20
88 * @var integer DB_ enum
89 */
90 protected $readDb = DB_SLAVE;
91
92 /**
93 * Constructor.
94 *
95 * @since 1.20
96 *
97 * @param DBTable $table
98 * @param array|null $fields
99 * @param boolean $loadDefaults
100 */
101 public function __construct( DBTable $table, $fields = null, $loadDefaults = false ) {
102 $this->table = $table;
103
104 if ( !is_array( $fields ) ) {
105 $fields = array();
106 }
107
108 if ( $loadDefaults ) {
109 $fields = array_merge( $this->table->getDefaults(), $fields );
110 }
111
112 $this->setFields( $fields );
113 }
114
115 /**
116 * Load the specified fields from the database.
117 *
118 * @since 1.20
119 *
120 * @param array|null $fields
121 * @param boolean $override
122 * @param boolean $skipLoaded
123 *
124 * @return bool Success indicator
125 */
126 public function loadFields( $fields = null, $override = true, $skipLoaded = false ) {
127 if ( is_null( $this->getId() ) ) {
128 return false;
129 }
130
131 if ( is_null( $fields ) ) {
132 $fields = array_keys( $this->table->getFieldTypes() );
133 }
134
135 if ( $skipLoaded ) {
136 $fields = array_diff( $fields, array_keys( $this->fields ) );
137 }
138
139 if ( count( $fields ) > 0 ) {
140 $result = $this->table->rawSelectRow(
141 $this->table->getPrefixedFields( $fields ),
142 array( $this->table->getPrefixedField( 'id' ) => $this->getId() ),
143 array( 'LIMIT' => 1 )
144 );
145
146 if ( $result !== false ) {
147 $this->setFields( $this->table->getFieldsFromDBResult( $result ), $override );
148 return true;
149 }
150
151 return false;
152 }
153
154 return true;
155 }
156
157 /**
158 * Gets the value of a field.
159 *
160 * @since 1.20
161 *
162 * @param string $name
163 * @param mixed $default
164 *
165 * @throws MWException
166 * @return mixed
167 */
168 public function getField( $name, $default = null ) {
169 if ( $this->hasField( $name ) ) {
170 return $this->fields[$name];
171 } elseif ( !is_null( $default ) ) {
172 return $default;
173 } else {
174 throw new MWException( 'Attempted to get not-set field ' . $name );
175 }
176 }
177
178 /**
179 * Gets the value of a field but first loads it if not done so already.
180 *
181 * @since 1.20
182 *
183 * @param string$name
184 *
185 * @return mixed
186 */
187 public function loadAndGetField( $name ) {
188 if ( !$this->hasField( $name ) ) {
189 $this->loadFields( array( $name ) );
190 }
191
192 return $this->getField( $name );
193 }
194
195 /**
196 * Remove a field.
197 *
198 * @since 1.20
199 *
200 * @param string $name
201 */
202 public function removeField( $name ) {
203 unset( $this->fields[$name] );
204 }
205
206 /**
207 * Returns the objects database id.
208 *
209 * @since 1.20
210 *
211 * @return integer|null
212 */
213 public function getId() {
214 return $this->getField( 'id' );
215 }
216
217 /**
218 * Sets the objects database id.
219 *
220 * @since 1.20
221 *
222 * @param integer|null $id
223 */
224 public function setId( $id ) {
225 return $this->setField( 'id', $id );
226 }
227
228 /**
229 * Gets if a certain field is set.
230 *
231 * @since 1.20
232 *
233 * @param string $name
234 *
235 * @return boolean
236 */
237 public function hasField( $name ) {
238 return array_key_exists( $name, $this->fields );
239 }
240
241 /**
242 * Gets if the id field is set.
243 *
244 * @since 1.20
245 *
246 * @return boolean
247 */
248 public function hasIdField() {
249 return $this->hasField( 'id' )
250 && !is_null( $this->getField( 'id' ) );
251 }
252
253 /**
254 * Sets multiple fields.
255 *
256 * @since 1.20
257 *
258 * @param array $fields The fields to set
259 * @param boolean $override Override already set fields with the provided values?
260 */
261 public function setFields( array $fields, $override = true ) {
262 foreach ( $fields as $name => $value ) {
263 if ( $override || !$this->hasField( $name ) ) {
264 $this->setField( $name, $value );
265 }
266 }
267 }
268
269 /**
270 * Gets the fields => values to write to the table.
271 *
272 * @since 1.20
273 *
274 * @return array
275 */
276 protected function getWriteValues() {
277 $values = array();
278
279 foreach ( $this->table->getFieldTypes() as $name => $type ) {
280 if ( array_key_exists( $name, $this->fields ) ) {
281 $value = $this->fields[$name];
282
283 switch ( $type ) {
284 case 'array':
285 $value = (array)$value;
286 case 'blob':
287 $value = serialize( $value );
288 }
289
290 $values[$this->table->getPrefixedField( $name )] = $value;
291 }
292 }
293
294 return $values;
295 }
296
297 /**
298 * Serializes the object to an associative array which
299 * can then easily be converted into JSON or similar.
300 *
301 * @since 1.20
302 *
303 * @param null|array $fields
304 * @param boolean $incNullId
305 *
306 * @return array
307 */
308 public function toArray( $fields = null, $incNullId = false ) {
309 $data = array();
310 $setFields = array();
311
312 if ( !is_array( $fields ) ) {
313 $setFields = $this->getSetFieldNames();
314 } else {
315 foreach ( $fields as $field ) {
316 if ( $this->hasField( $field ) ) {
317 $setFields[] = $field;
318 }
319 }
320 }
321
322 foreach ( $setFields as $field ) {
323 if ( $incNullId || $field != 'id' || $this->hasIdField() ) {
324 $data[$field] = $this->getField( $field );
325 }
326 }
327
328 return $data;
329 }
330
331 /**
332 * Load the default values, via getDefaults.
333 *
334 * @since 1.20
335 *
336 * @param boolean $override
337 */
338 public function loadDefaults( $override = true ) {
339 $this->setFields( $this->table->getDefaults(), $override );
340 }
341
342 /**
343 * Writes the answer to the database, either updating it
344 * when it already exists, or inserting it when it doesn't.
345 *
346 * @since 1.20
347 *
348 * @return boolean Success indicator
349 */
350 public function save() {
351 if ( $this->hasIdField() ) {
352 return $this->saveExisting();
353 } else {
354 return $this->insert();
355 }
356 }
357
358 /**
359 * Updates the object in the database.
360 *
361 * @since 1.20
362 *
363 * @return boolean Success indicator
364 */
365 protected function saveExisting() {
366 $dbw = wfGetDB( DB_MASTER );
367
368 $success = $dbw->update(
369 $this->table->getDBTable(),
370 $this->getWriteValues(),
371 array( $this->table->getPrefixedField( 'id' ) => $this->getId() ),
372 __METHOD__
373 );
374
375 return $success;
376 }
377
378 /**
379 * Inserts the object into the database.
380 *
381 * @since 1.20
382 *
383 * @return boolean Success indicator
384 */
385 protected function insert() {
386 $dbw = wfGetDB( DB_MASTER );
387
388 $result = $dbw->insert(
389 $this->table->getDBTable(),
390 $this->getWriteValues(),
391 __METHOD__,
392 array( 'IGNORE' )
393 );
394
395 if ( $result ) {
396 $this->setField( 'id', $dbw->insertId() );
397 }
398
399 return $result;
400 }
401
402 /**
403 * Removes the object from the database.
404 *
405 * @since 1.20
406 *
407 * @return boolean Success indicator
408 */
409 public function remove() {
410 $this->beforeRemove();
411
412 $success = $this->table->delete( array( 'id' => $this->getId() ) );
413
414 if ( $success ) {
415 $this->onRemoved();
416 }
417
418 return $success;
419 }
420
421 /**
422 * Gets called before an object is removed from the database.
423 *
424 * @since 1.20
425 */
426 protected function beforeRemove() {
427 $this->loadFields( $this->getBeforeRemoveFields(), false, true );
428 }
429
430 /**
431 * Before removal of an object happens, @see beforeRemove gets called.
432 * This method loads the fields of which the names have been returned by this one (or all fields if null is returned).
433 * This allows for loading info needed after removal to get rid of linked data and the like.
434 *
435 * @since 1.20
436 *
437 * @return array|null
438 */
439 protected function getBeforeRemoveFields() {
440 return array();
441 }
442
443 /**
444 * Gets called after successfull removal.
445 * Can be overriden to get rid of linked data.
446 *
447 * @since 1.20
448 */
449 protected function onRemoved() {
450 $this->setField( 'id', null );
451 }
452
453 /**
454 * Return the names and values of the fields.
455 *
456 * @since 1.20
457 *
458 * @return array
459 */
460 public function getFields() {
461 return $this->fields;
462 }
463
464 /**
465 * Return the names of the fields.
466 *
467 * @since 1.20
468 *
469 * @return array
470 */
471 public function getSetFieldNames() {
472 return array_keys( $this->fields );
473 }
474
475 /**
476 * Sets the value of a field.
477 * Strings can be provided for other types,
478 * so this method can be called from unserialization handlers.
479 *
480 * @since 1.20
481 *
482 * @param string $name
483 * @param mixed $value
484 *
485 * @throws MWException
486 */
487 public function setField( $name, $value ) {
488 $fields = $this->table->getFieldTypes();
489
490 if ( array_key_exists( $name, $fields ) ) {
491 switch ( $fields[$name] ) {
492 case 'int':
493 $value = (int)$value;
494 break;
495 case 'float':
496 $value = (float)$value;
497 break;
498 case 'bool':
499 if ( is_string( $value ) ) {
500 $value = $value !== '0';
501 } elseif ( is_int( $value ) ) {
502 $value = $value !== 0;
503 }
504 break;
505 case 'array':
506 if ( is_string( $value ) ) {
507 $value = unserialize( $value );
508 }
509
510 if ( !is_array( $value ) ) {
511 $value = array();
512 }
513 break;
514 case 'blob':
515 if ( is_string( $value ) ) {
516 $value = unserialize( $value );
517 }
518 break;
519 case 'id':
520 if ( is_string( $value ) ) {
521 $value = (int)$value;
522 }
523 break;
524 }
525
526 $this->fields[$name] = $value;
527 } else {
528 throw new MWException( 'Attempted to set unknown field ' . $name );
529 }
530 }
531
532 /**
533 * Get the database type used for read operations.
534 *
535 * @since 1.20
536 *
537 * @return integer DB_ enum
538 */
539 public function getReadDb() {
540 return $this->readDb;
541 }
542
543 /**
544 * Set the database type to use for read operations.
545 *
546 * @param integer $db
547 *
548 * @since 1.20
549 */
550 public function setReadDb( $db ) {
551 $this->readDb = $db;
552 }
553
554 /**
555 * Add an amount (can be negative) to the specified field (needs to be numeric).
556 *
557 * @since 1.20
558 *
559 * @param string $field
560 * @param integer $amount
561 *
562 * @return boolean Success indicator
563 */
564 public function addToField( $field, $amount ) {
565 if ( $amount == 0 ) {
566 return true;
567 }
568
569 if ( !$this->hasIdField() ) {
570 return false;
571 }
572
573 $absoluteAmount = abs( $amount );
574 $isNegative = $amount < 0;
575
576 $dbw = wfGetDB( DB_MASTER );
577
578 $fullField = $this->table->getPrefixedField( $field );
579
580 $success = $dbw->update(
581 $this->table->getDBTable(),
582 array( "$fullField=$fullField" . ( $isNegative ? '-' : '+' ) . $absoluteAmount ),
583 array( $this->table->getPrefixedField( 'id' ) => $this->getId() ),
584 __METHOD__
585 );
586
587 if ( $success && static::hasField( $field ) ) {
588 static::setField( $field, static::getField( $field ) + $amount );
589 }
590
591 return $success;
592 }
593
594 /**
595 * Return the names of the fields.
596 *
597 * @since 1.20
598 *
599 * @return array
600 */
601 public function getFieldNames() {
602 return array_keys( $this->table->getFieldTypes() );
603 }
604
605 /**
606 * Computes and updates the values of the summary fields.
607 *
608 * @since 1.20
609 *
610 * @param array|string|null $summaryFields
611 */
612 public function loadSummaryFields( $summaryFields = null ) {
613
614 }
615
616 /**
617 * Sets the value for the @see $updateSummaries field.
618 *
619 * @since 1.20
620 *
621 * @param boolean $update
622 */
623 public function setUpdateSummaries( $update ) {
624 $this->updateSummaries = $update;
625 }
626
627 /**
628 * Sets the value for the @see $inSummaryMode field.
629 *
630 * @since 1.20
631 *
632 * @param boolean $summaryMode
633 */
634 public function setSummaryMode( $summaryMode ) {
635 $this->inSummaryMode = $summaryMode;
636 }
637
638 /**
639 * Return if any fields got changed.
640 *
641 * @since 1.20
642 *
643 * @param DBDataObject $object
644 * @param boolean $excludeSummaryFields When set to true, summary field changes are ignored.
645 *
646 * @return boolean
647 */
648 protected function fieldsChanged( DBDataObject $object, $excludeSummaryFields = false ) {
649 foreach ( $this->fields as $name => $value ) {
650 $excluded = $excludeSummaryFields && in_array( $name, $this->table->getSummaryFields() );
651
652 if ( !$excluded && $object->getField( $name ) !== $value ) {
653 return true;
654 }
655 }
656
657 return false;
658 }
659
660 /**
661 * Returns the table this DBDataObject is a row in.
662 *
663 * @since 1.20
664 *
665 * @return DBTable
666 */
667 public function getTable() {
668 return $this->table;
669 }
670
671 }