Revert to arbitrarily old point before initial remote branch creation to help clean up
[lhc/web/wiklou.git] / includes / db / ORMRow.php
1 <?php
2 /**
3 * Abstract base class for representing objects that are stored in some DB table.
4 * This is basically an ORM-like wrapper around rows in database tables that
5 * aims to be both simple and very flexible. It is centered around an associative
6 * array of fields and various methods to do common interaction with the database.
7 *
8 * This program is free software; you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation; either version 2 of the License, or
11 * (at your option) any later version.
12 *
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public License along
19 * with this program; if not, write to the Free Software Foundation, Inc.,
20 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
21 * http://www.gnu.org/copyleft/gpl.html
22 *
23 * These methods are likely candidates for overriding:
24 * * getDefaults
25 * * remove
26 * * insert
27 * * saveExisting
28 * * loadSummaryFields
29 * * getSummaryFields
30 *
31 * Main instance methods:
32 * * getField(s)
33 * * setField(s)
34 * * save
35 * * remove
36 *
37 * Main static methods:
38 * * select
39 * * update
40 * * delete
41 * * count
42 * * has
43 * * selectRow
44 * * selectFields
45 * * selectFieldsRow
46 *
47 * @since 1.20
48 *
49 * @file ORMRow.php
50 *
51 * @licence GNU GPL v2 or later
52 * @author Jeroen De Dauw < jeroendedauw@gmail.com >
53 */
54
55 abstract class ORMRow {
56
57 /**
58 * The fields of the object.
59 * field name (w/o prefix) => value
60 *
61 * @since 1.20
62 * @var array
63 */
64 protected $fields = array( 'id' => null );
65
66 /**
67 * @since 1.20
68 * @var ORMTable
69 */
70 protected $table;
71
72 /**
73 * If the object should update summaries of linked items when changed.
74 * For example, update the course_count field in universities when a course in courses is deleted.
75 * Settings this to false can prevent needless updating work in situations
76 * such as deleting a university, which will then delete all it's courses.
77 *
78 * @since 1.20
79 * @var bool
80 */
81 protected $updateSummaries = true;
82
83 /**
84 * Indicates if the object is in summary mode.
85 * This mode indicates that only summary fields got updated,
86 * which allows for optimizations.
87 *
88 * @since 1.20
89 * @var bool
90 */
91 protected $inSummaryMode = false;
92
93 /**
94 * Constructor.
95 *
96 * @since 1.20
97 *
98 * @param ORMTable $table
99 * @param array|null $fields
100 * @param boolean $loadDefaults
101 */
102 public function __construct( ORMTable $table, $fields = null, $loadDefaults = false ) {
103 $this->table = $table;
104
105 if ( !is_array( $fields ) ) {
106 $fields = array();
107 }
108
109 if ( $loadDefaults ) {
110 $fields = array_merge( $this->table->getDefaults(), $fields );
111 }
112
113 $this->setFields( $fields );
114 }
115
116 /**
117 * Load the specified fields from the database.
118 *
119 * @since 1.20
120 *
121 * @param array|null $fields
122 * @param boolean $override
123 * @param boolean $skipLoaded
124 *
125 * @return bool Success indicator
126 */
127 public function loadFields( $fields = null, $override = true, $skipLoaded = false ) {
128 if ( is_null( $this->getId() ) ) {
129 return false;
130 }
131
132 if ( is_null( $fields ) ) {
133 $fields = array_keys( $this->table->getFields() );
134 }
135
136 if ( $skipLoaded ) {
137 $fields = array_diff( $fields, array_keys( $this->fields ) );
138 }
139
140 if ( !empty( $fields ) ) {
141 $result = $this->table->rawSelectRow(
142 $this->table->getPrefixedFields( $fields ),
143 array( $this->table->getPrefixedField( 'id' ) => $this->getId() ),
144 array( 'LIMIT' => 1 )
145 );
146
147 if ( $result !== false ) {
148 $this->setFields( $this->table->getFieldsFromDBResult( $result ), $override );
149 return true;
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 $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->getFields() 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 * @param string|null $functionName
349 *
350 * @return boolean Success indicator
351 */
352 public function save( $functionName = null ) {
353 if ( $this->hasIdField() ) {
354 return $this->saveExisting( $functionName );
355 } else {
356 return $this->insert( $functionName );
357 }
358 }
359
360 /**
361 * Updates the object in the database.
362 *
363 * @since 1.20
364 *
365 * @param string|null $functionName
366 *
367 * @return boolean Success indicator
368 */
369 protected function saveExisting( $functionName = null ) {
370 $dbw = wfGetDB( DB_MASTER );
371
372 $success = $dbw->update(
373 $this->table->getName(),
374 $this->getWriteValues(),
375 $this->table->getPrefixedValues( $this->getUpdateConditions() ),
376 is_null( $functionName ) ? __METHOD__ : $functionName
377 );
378
379 return $success;
380 }
381
382 /**
383 * Returns the WHERE considtions needed to identify this object so
384 * it can be updated.
385 *
386 * @since 1.20
387 *
388 * @return array
389 */
390 protected function getUpdateConditions() {
391 return array( 'id' => $this->getId() );
392 }
393
394 /**
395 * Inserts the object into the database.
396 *
397 * @since 1.20
398 *
399 * @param string|null $functionName
400 * @param array|null $options
401 *
402 * @return boolean Success indicator
403 */
404 protected function insert( $functionName = null, array $options = null ) {
405 $dbw = wfGetDB( DB_MASTER );
406
407 $result = $dbw->insert(
408 $this->table->getName(),
409 $this->getWriteValues(),
410 is_null( $functionName ) ? __METHOD__ : $functionName,
411 is_null( $options ) ? array( 'IGNORE' ) : $options
412 );
413
414 if ( $result ) {
415 $this->setField( 'id', $dbw->insertId() );
416 }
417
418 return $result;
419 }
420
421 /**
422 * Removes the object from the database.
423 *
424 * @since 1.20
425 *
426 * @return boolean Success indicator
427 */
428 public function remove() {
429 $this->beforeRemove();
430
431 $success = $this->table->delete( array( 'id' => $this->getId() ) );
432
433 if ( $success ) {
434 $this->onRemoved();
435 }
436
437 return $success;
438 }
439
440 /**
441 * Gets called before an object is removed from the database.
442 *
443 * @since 1.20
444 */
445 protected function beforeRemove() {
446 $this->loadFields( $this->getBeforeRemoveFields(), false, true );
447 }
448
449 /**
450 * Before removal of an object happens, @see beforeRemove gets called.
451 * This method loads the fields of which the names have been returned by this one (or all fields if null is returned).
452 * This allows for loading info needed after removal to get rid of linked data and the like.
453 *
454 * @since 1.20
455 *
456 * @return array|null
457 */
458 protected function getBeforeRemoveFields() {
459 return array();
460 }
461
462 /**
463 * Gets called after successfull removal.
464 * Can be overriden to get rid of linked data.
465 *
466 * @since 1.20
467 */
468 protected function onRemoved() {
469 $this->setField( 'id', null );
470 }
471
472 /**
473 * Return the names and values of the fields.
474 *
475 * @since 1.20
476 *
477 * @return array
478 */
479 public function getFields() {
480 return $this->fields;
481 }
482
483 /**
484 * Return the names of the fields.
485 *
486 * @since 1.20
487 *
488 * @return array
489 */
490 public function getSetFieldNames() {
491 return array_keys( $this->fields );
492 }
493
494 /**
495 * Sets the value of a field.
496 * Strings can be provided for other types,
497 * so this method can be called from unserialization handlers.
498 *
499 * @since 1.20
500 *
501 * @param string $name
502 * @param mixed $value
503 *
504 * @throws MWException
505 */
506 public function setField( $name, $value ) {
507 $fields = $this->table->getFields();
508
509 if ( array_key_exists( $name, $fields ) ) {
510 switch ( $fields[$name] ) {
511 case 'int':
512 $value = (int)$value;
513 break;
514 case 'float':
515 $value = (float)$value;
516 break;
517 case 'bool':
518 if ( is_string( $value ) ) {
519 $value = $value !== '0';
520 } elseif ( is_int( $value ) ) {
521 $value = $value !== 0;
522 }
523 break;
524 case 'array':
525 if ( is_string( $value ) ) {
526 $value = unserialize( $value );
527 }
528
529 if ( !is_array( $value ) ) {
530 $value = array();
531 }
532 break;
533 case 'blob':
534 if ( is_string( $value ) ) {
535 $value = unserialize( $value );
536 }
537 break;
538 case 'id':
539 if ( is_string( $value ) ) {
540 $value = (int)$value;
541 }
542 break;
543 }
544
545 $this->fields[$name] = $value;
546 } else {
547 throw new MWException( 'Attempted to set unknown field ' . $name );
548 }
549 }
550
551 /**
552 * Add an amount (can be negative) to the specified field (needs to be numeric).
553 *
554 * @since 1.20
555 *
556 * @param string $field
557 * @param integer $amount
558 *
559 * @return boolean Success indicator
560 */
561 public function addToField( $field, $amount ) {
562 if ( $amount == 0 ) {
563 return true;
564 }
565
566 if ( !$this->hasIdField() ) {
567 return false;
568 }
569
570 $absoluteAmount = abs( $amount );
571 $isNegative = $amount < 0;
572
573 $dbw = wfGetDB( DB_MASTER );
574
575 $fullField = $this->table->getPrefixedField( $field );
576
577 $success = $dbw->update(
578 $this->table->getName(),
579 array( "$fullField=$fullField" . ( $isNegative ? '-' : '+' ) . $absoluteAmount ),
580 array( $this->table->getPrefixedField( 'id' ) => $this->getId() ),
581 __METHOD__
582 );
583
584 if ( $success && $this->hasField( $field ) ) {
585 $this->setField( $field, $this->getField( $field ) + $amount );
586 }
587
588 return $success;
589 }
590
591 /**
592 * Return the names of the fields.
593 *
594 * @since 1.20
595 *
596 * @return array
597 */
598 public function getFieldNames() {
599 return array_keys( $this->table->getFields() );
600 }
601
602 /**
603 * Computes and updates the values of the summary fields.
604 *
605 * @since 1.20
606 *
607 * @param array|string|null $summaryFields
608 */
609 public function loadSummaryFields( $summaryFields = null ) {
610
611 }
612
613 /**
614 * Sets the value for the @see $updateSummaries field.
615 *
616 * @since 1.20
617 *
618 * @param boolean $update
619 */
620 public function setUpdateSummaries( $update ) {
621 $this->updateSummaries = $update;
622 }
623
624 /**
625 * Sets the value for the @see $inSummaryMode field.
626 *
627 * @since 1.20
628 *
629 * @param boolean $summaryMode
630 */
631 public function setSummaryMode( $summaryMode ) {
632 $this->inSummaryMode = $summaryMode;
633 }
634
635 /**
636 * Return if any fields got changed.
637 *
638 * @since 1.20
639 *
640 * @param ORMRow $object
641 * @param boolean|array $excludeSummaryFields
642 * When set to true, summary field changes are ignored.
643 * Can also be an array of fields to ignore.
644 *
645 * @return boolean
646 */
647 protected function fieldsChanged( ORMRow $object, $excludeSummaryFields = false ) {
648 $exclusionFields = array();
649
650 if ( $excludeSummaryFields !== false ) {
651 $exclusionFields = is_array( $excludeSummaryFields ) ? $excludeSummaryFields : $this->table->getSummaryFields();
652 }
653
654 foreach ( $this->fields as $name => $value ) {
655 $excluded = $excludeSummaryFields && in_array( $name, $exclusionFields );
656
657 if ( !$excluded && $object->getField( $name ) !== $value ) {
658 return true;
659 }
660 }
661
662 return false;
663 }
664
665 /**
666 * Returns the table this ORMRow is a row in.
667 *
668 * @since 1.20
669 *
670 * @return ORMTable
671 */
672 public function getTable() {
673 return $this->table;
674 }
675
676 }