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