r111264 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r111263‎ | r111264 | r111265 >
Date:21:34, 11 February 2012
Author:jeroendedauw
Status:reverted (Comments)
Tags:
Comment:
adding DBDataObject class after having some people review it and posting on the list. docs can currently be found at https://www.mediawiki.org/wiki/User:Jeroen_De_Dauw/DBDataObject
Modified paths:
  • /trunk/phase3/includes/AutoLoader.php (modified) (history)
  • /trunk/phase3/includes/DBDataObject.php (added) (history)

Diff [purge]

Index: trunk/phase3/includes/DBDataObject.php
@@ -0,0 +1,1287 @@
 2+<?php
 3+
 4+/**
 5+ * Abstract base class for representing objects that are stored in some DB table.
 6+ * This is basically an ORM-like wrapper around rows in database tables that
 7+ * aims to be both simple and very flexible. It is centered around an associative
 8+ * array of fields and various methods to do common interaction with the database.
 9+ *
 10+ * These methods must be implemented in deriving classes:
 11+ * * getFieldTypes
 12+ *
 13+ * These methods are likely candidates for overriding:
 14+ * * getDefaults
 15+ * * remove
 16+ * * insert
 17+ * * saveExisting
 18+ * * loadSummaryFields
 19+ * * getSummaryFields
 20+ *
 21+ * Deriving classes must register their table and field prefix in $wgDBDataObjects.
 22+ * Syntax: $wgDBDataObjects['DrivingClassName'] = array( 'table' => 'table_name', 'prefix' => 'fieldprefix_' );
 23+ * Example: $wgDBDataObjects['EPOrg'] = array( 'table' => 'ep_orgs', 'prefix' => 'org_' );
 24+ *
 25+ * Main instance methods:
 26+ * * getField(s)
 27+ * * setField(s)
 28+ * * save
 29+ * * remove
 30+ *
 31+ * Main static methods:
 32+ * * select
 33+ * * update
 34+ * * delete
 35+ * * count
 36+ * * has
 37+ * * selectRow
 38+ * * selectFields
 39+ * * selectFieldsRow
 40+ *
 41+ * @since 1.20
 42+ *
 43+ * @file DBDataObject.php
 44+ *
 45+ * @licence GNU GPL v3 or later
 46+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
 47+ */
 48+abstract class DBDataObject {
 49+
 50+ /**
 51+ * The fields of the object.
 52+ * field name (w/o prefix) => value
 53+ *
 54+ * @since 0.1
 55+ * @var array
 56+ */
 57+ protected $fields = array( 'id' => null );
 58+
 59+ /**
 60+ * If the object should update summaries of linked items when changed.
 61+ * For example, update the course_count field in universities when a course in courses is deleted.
 62+ * Settings this to false can prevent needless updating work in situations
 63+ * such as deleting a university, which will then delete all it's courses.
 64+ *
 65+ * @since 0.1
 66+ * @var bool
 67+ */
 68+ protected $updateSummaries = true;
 69+
 70+ /**
 71+ * Indicates if the object is in summary mode.
 72+ * This mode indicates that only summary fields got updated,
 73+ * which allows for optimizations.
 74+ *
 75+ * @since 0.1
 76+ * @var bool
 77+ */
 78+ protected $inSummaryMode = false;
 79+
 80+
 81+ /**
 82+ * The database connection to use for read operations.
 83+ * Can be changed via @see setReadDb.
 84+ *
 85+ * @since 0.2
 86+ * @var integer DB_ enum
 87+ */
 88+ protected static $readDb = DB_SLAVE;
 89+
 90+ /**
 91+ * Returns the name of the database table objects of this type are stored in.
 92+ *
 93+ * @since 0.1
 94+ *
 95+ * @throws MWException
 96+ * @return string
 97+ */
 98+ public static function getDBTable() {
 99+ global $wgDBDataObjects;
 100+ if ( array_key_exists( get_called_class(), $wgDBDataObjects ) ) {
 101+ return $wgDBDataObjects[get_called_class()]['table'];
 102+ }
 103+ else {
 104+ throw new MWException( 'Class "' . get_called_class() . '" not found in $wgDBDataObjects' );
 105+ }
 106+ }
 107+
 108+ /**
 109+ * Gets the db field prefix.
 110+ *
 111+ * @since 0.1
 112+ *
 113+ * @throws MWException
 114+ * @return string
 115+ */
 116+ protected static function getFieldPrefix() {
 117+ global $wgDBDataObjects;
 118+ if ( array_key_exists( get_called_class(), $wgDBDataObjects ) ) {
 119+ return $wgDBDataObjects[get_called_class()]['prefix'];
 120+ }
 121+ else {
 122+ throw new MWException( 'Class "' . get_called_class() . '" not found in $wgDBDataObjects' );
 123+ }
 124+ }
 125+
 126+ /**
 127+ * Returns an array with the fields and their types this object contains.
 128+ * This corresponds directly to the fields in the database, without prefix.
 129+ *
 130+ * field name => type
 131+ *
 132+ * Allowed types:
 133+ * * id
 134+ * * str
 135+ * * int
 136+ * * float
 137+ * * bool
 138+ * * array
 139+ *
 140+ * @since 0.1
 141+ *
 142+ * @throws MWException
 143+ * @return array
 144+ */
 145+ protected static function getFieldTypes() {
 146+ throw new MWException( 'Class did not implement getFieldTypes' );
 147+ }
 148+
 149+ /**
 150+ * Returns a list of default field values.
 151+ * field name => field value
 152+ *
 153+ * @since 0.1
 154+ *
 155+ * @return array
 156+ */
 157+ public static function getDefaults() {
 158+ return array();
 159+ }
 160+
 161+ /**
 162+ * Returns a list of the summary fields.
 163+ * These are fields that cache computed values, such as the amount of linked objects of $type.
 164+ * This is relevant as one might not want to do actions such as log changes when these get updated.
 165+ *
 166+ * @since 0.1
 167+ *
 168+ * @return array
 169+ */
 170+ public static function getSummaryFields() {
 171+ return array();
 172+ }
 173+
 174+ /**
 175+ * Constructor.
 176+ *
 177+ * @since 0.1
 178+ *
 179+ * @param array|null $fields
 180+ * @param boolean $loadDefaults
 181+ */
 182+ public function __construct( $fields = null, $loadDefaults = false ) {
 183+ if ( !is_array( $fields ) ) {
 184+ $fields = array();
 185+ }
 186+
 187+ if ( $loadDefaults ) {
 188+ $fields = array_merge( $this->getDefaults(), $fields );
 189+ }
 190+
 191+ $this->setFields( $fields );
 192+ }
 193+
 194+ /**
 195+ * Load the specified fields from the database.
 196+ *
 197+ * @since 0.1
 198+ *
 199+ * @param array|null $fields
 200+ * @param boolean $override
 201+ * @param boolean $skipLoaded
 202+ *
 203+ * @return Success indicator
 204+ */
 205+ public function loadFields( $fields = null, $override = true, $skipLoaded = false ) {
 206+ if ( is_null( $this->getId() ) ) {
 207+ return false;
 208+ }
 209+
 210+ if ( is_null( $fields ) ) {
 211+ $fields = array_keys( $this->getFieldTypes() );
 212+ }
 213+
 214+ if ( $skipLoaded ) {
 215+ $loadedFields = array_keys( $this->fields );
 216+ $fields = array_filter( $fields, function( $field ) use ( $loadedFields ) {
 217+ return !in_array( $field, $loadedFields );
 218+ } );
 219+ }
 220+
 221+ if ( count( $fields ) > 0 ) {
 222+ $results = $this->rawSelect(
 223+ $this->getPrefixedFields( $fields ),
 224+ array( $this->getPrefixedField( 'id' ) => $this->getId() ),
 225+ array( 'LIMIT' => 1 )
 226+ );
 227+
 228+ foreach ( $results as $result ) {
 229+ $this->setFields( $this->getFieldsFromDBResult( $result ), $override );
 230+ return true;
 231+ }
 232+
 233+ return false;
 234+ }
 235+
 236+ return true;
 237+ }
 238+
 239+ /**
 240+ * Gets the value of a field.
 241+ *
 242+ * @since 0.1
 243+ *
 244+ * @param string $name
 245+ * @param mixed $default
 246+ *
 247+ * @throws MWException
 248+ * @return mixed
 249+ */
 250+ public function getField( $name, $default = null ) {
 251+ if ( $this->hasField( $name ) ) {
 252+ return $this->fields[$name];
 253+ } elseif ( !is_null( $default ) ) {
 254+ return $default;
 255+ } else {
 256+ throw new MWException( 'Attempted to get not-set field ' . $name );
 257+ }
 258+ }
 259+
 260+ /**
 261+ * Gets the value of a field but first loads it if not done so already.
 262+ *
 263+ * @since 0.1
 264+ *
 265+ * @param string$name
 266+ *
 267+ * @return mixed
 268+ */
 269+ public function loadAndGetField( $name ) {
 270+ if ( !$this->hasField( $name ) ) {
 271+ $this->loadFields( array( $name ) );
 272+ }
 273+
 274+ return $this->getField( $name );
 275+ }
 276+
 277+ /**
 278+ * Remove a field.
 279+ *
 280+ * @since 0.1
 281+ *
 282+ * @param string $name
 283+ */
 284+ public function removeField( $name ) {
 285+ unset( $this->fields[$name] );
 286+ }
 287+
 288+ /**
 289+ * Returns the objects database id.
 290+ *
 291+ * @since 0.1
 292+ *
 293+ * @return integer|null
 294+ */
 295+ public function getId() {
 296+ return $this->getField( 'id' );
 297+ }
 298+
 299+ /**
 300+ * Sets the objects database id.
 301+ *
 302+ * @since 0.1
 303+ *
 304+ * @param integer|null $id
 305+ */
 306+ public function setId( $id ) {
 307+ return $this->setField( 'id', $id );
 308+ }
 309+
 310+ /**
 311+ * Gets if a certain field is set.
 312+ *
 313+ * @since 0.1
 314+ *
 315+ * @param string $name
 316+ *
 317+ * @return boolean
 318+ */
 319+ public function hasField( $name ) {
 320+ return array_key_exists( $name, $this->fields );
 321+ }
 322+
 323+ /**
 324+ * Gets if the id field is set.
 325+ *
 326+ * @since 0.1
 327+ *
 328+ * @return boolean
 329+ */
 330+ public function hasIdField() {
 331+ return $this->hasField( 'id' )
 332+ && !is_null( $this->getField( 'id' ) );
 333+ }
 334+
 335+ /**
 336+ * Sets multiple fields.
 337+ *
 338+ * @since 0.1
 339+ *
 340+ * @param array $fields The fields to set
 341+ * @param boolean $override Override already set fields with the provided values?
 342+ */
 343+ public function setFields( array $fields, $override = true ) {
 344+ foreach ( $fields as $name => $value ) {
 345+ if ( $override || !$this->hasField( $name ) ) {
 346+ $this->setField( $name, $value );
 347+ }
 348+ }
 349+ }
 350+
 351+ /**
 352+ * Gets the fields => values to write to the table.
 353+ *
 354+ * @since 0.1
 355+ *
 356+ * @return array
 357+ */
 358+ protected function getWriteValues() {
 359+ $values = array();
 360+
 361+ foreach ( $this->getFieldTypes() as $name => $type ) {
 362+ if ( array_key_exists( $name, $this->fields ) ) {
 363+ $value = $this->fields[$name];
 364+
 365+ switch ( $type ) {
 366+ case 'array':
 367+ $value = (array)$value;
 368+ case 'blob':
 369+ $value = serialize( $value );
 370+ }
 371+
 372+ $values[$this->getFieldPrefix() . $name] = $value;
 373+ }
 374+ }
 375+
 376+ return $values;
 377+ }
 378+
 379+ /**
 380+ * Serializes the object to an associative array which
 381+ * can then easily be converted into JSON or similar.
 382+ *
 383+ * @since 0.1
 384+ *
 385+ * @param null|array $fields
 386+ * @param boolean $incNullId
 387+ *
 388+ * @return array
 389+ */
 390+ public function toArray( $fields = null, $incNullId = false ) {
 391+ $data = array();
 392+ $setFields = array();
 393+
 394+ if ( !is_array( $fields ) ) {
 395+ $setFields = $this->getSetFieldNames();
 396+ } else {
 397+ foreach ( $fields as $field ) {
 398+ if ( $this->hasField( $field ) ) {
 399+ $setFields[] = $field;
 400+ }
 401+ }
 402+ }
 403+
 404+ foreach ( $setFields as $field ) {
 405+ if ( $incNullId || $field != 'id' || $this->hasIdField() ) {
 406+ $data[$field] = $this->getField( $field );
 407+ }
 408+ }
 409+
 410+ return $data;
 411+ }
 412+
 413+ /**
 414+ * Load the default values, via getDefaults.
 415+ *
 416+ * @since 0.1
 417+ *
 418+ * @param boolean $override
 419+ */
 420+ public function loadDefaults( $override = true ) {
 421+ $this->setFields( $this->getDefaults(), $override );
 422+ }
 423+
 424+ /**
 425+ * Writes the answer to the database, either updating it
 426+ * when it already exists, or inserting it when it doesn't.
 427+ *
 428+ * @since 0.1
 429+ *
 430+ * @return boolean Success indicator
 431+ */
 432+ public function save() {
 433+ if ( $this->hasIdField() ) {
 434+ return $this->saveExisting();
 435+ } else {
 436+ return $this->insert();
 437+ }
 438+ }
 439+
 440+ /**
 441+ * Updates the object in the database.
 442+ *
 443+ * @since 0.1
 444+ *
 445+ * @return boolean Success indicator
 446+ */
 447+ protected function saveExisting() {
 448+ $dbw = wfGetDB( DB_MASTER );
 449+
 450+ $success = $dbw->update(
 451+ $this->getDBTable(),
 452+ $this->getWriteValues(),
 453+ array( $this->getFieldPrefix() . 'id' => $this->getId() ),
 454+ __METHOD__
 455+ );
 456+
 457+ return $success;
 458+ }
 459+
 460+ /**
 461+ * Inserts the object into the database.
 462+ *
 463+ * @since 0.1
 464+ *
 465+ * @return boolean Success indicator
 466+ */
 467+ protected function insert() {
 468+ $dbw = wfGetDB( DB_MASTER );
 469+
 470+ $result = $dbw->insert(
 471+ $this->getDBTable(),
 472+ $this->getWriteValues(),
 473+ __METHOD__,
 474+ array( 'IGNORE' )
 475+ );
 476+
 477+ if ( $result ) {
 478+ $this->setField( 'id', $dbw->insertId() );
 479+ }
 480+
 481+ return $result;
 482+ }
 483+
 484+ /**
 485+ * Removes the object from the database.
 486+ *
 487+ * @since 0.1
 488+ *
 489+ * @return boolean Success indicator
 490+ */
 491+ public function remove() {
 492+ $this->beforeRemove();
 493+
 494+ $success = static::delete( array( 'id' => $this->getId() ) );
 495+
 496+ if ( $success ) {
 497+ $this->onRemoved();
 498+ }
 499+
 500+ return $success;
 501+ }
 502+
 503+ /**
 504+ * Gets called before an object is removed from the database.
 505+ *
 506+ * @since 0.1
 507+ */
 508+ protected function beforeRemove() {
 509+ $this->loadFields( $this->getBeforeRemoveFields(), false, true );
 510+ }
 511+
 512+ /**
 513+ * Before removal of an object happens, @see beforeRemove gets called.
 514+ * This method loads the fields of which the names have been returned by this one (or all fields if null is returned).
 515+ * This allows for loading info needed after removal to get rid of linked data and the like.
 516+ *
 517+ * @since 0.1
 518+ *
 519+ * @return array|null
 520+ */
 521+ protected function getBeforeRemoveFields() {
 522+ return array();
 523+ }
 524+
 525+ /**
 526+ * Gets called after successfull removal.
 527+ * Can be overriden to get rid of linked data.
 528+ *
 529+ * @since 0.1
 530+ */
 531+ protected function onRemoved() {
 532+ $this->setField( 'id', null );
 533+ }
 534+
 535+ /**
 536+ * Return the names and values of the fields.
 537+ *
 538+ * @since 0.1
 539+ *
 540+ * @return array
 541+ */
 542+ public function getFields() {
 543+ return $this->fields;
 544+ }
 545+
 546+ /**
 547+ * Return the names of the fields.
 548+ *
 549+ * @since 0.1
 550+ *
 551+ * @return array
 552+ */
 553+ public function getSetFieldNames() {
 554+ return array_keys( $this->fields );
 555+ }
 556+
 557+ /**
 558+ * Sets the value of a field.
 559+ * Strings can be provided for other types,
 560+ * so this method can be called from unserialization handlers.
 561+ *
 562+ * @since 0.1
 563+ *
 564+ * @param string $name
 565+ * @param mixed $value
 566+ *
 567+ * @throws MWException
 568+ */
 569+ public function setField( $name, $value ) {
 570+ $fields = $this->getFieldTypes();
 571+
 572+ if ( array_key_exists( $name, $fields ) ) {
 573+ switch ( $fields[$name] ) {
 574+ case 'int':
 575+ $value = (int)$value;
 576+ break;
 577+ case 'float':
 578+ $value = (float)$value;
 579+ break;
 580+ case 'bool':
 581+ if ( is_string( $value ) ) {
 582+ $value = $value !== '0';
 583+ } elseif ( is_int( $value ) ) {
 584+ $value = $value !== 0;
 585+ }
 586+ break;
 587+ case 'array':
 588+ if ( is_string( $value ) ) {
 589+ $value = unserialize( $value );
 590+ }
 591+
 592+ if ( !is_array( $value ) ) {
 593+ $value = array();
 594+ }
 595+ break;
 596+ case 'blob':
 597+ if ( is_string( $value ) ) {
 598+ $value = unserialize( $value );
 599+ }
 600+ break;
 601+ case 'id':
 602+ if ( is_string( $value ) ) {
 603+ $value = (int)$value;
 604+ }
 605+ break;
 606+ }
 607+
 608+ $this->fields[$name] = $value;
 609+ } else {
 610+ throw new MWException( 'Attempted to set unknown field ' . $name );
 611+ }
 612+ }
 613+
 614+ /**
 615+ * Get a new instance of the class from an array.
 616+ *
 617+ * @since 0.1
 618+ *
 619+ * @param array $data
 620+ * @param boolean $loadDefaults
 621+ *
 622+ * @return DBDataObject
 623+ */
 624+ public static function newFromArray( array $data, $loadDefaults = false ) {
 625+ return new static( $data, $loadDefaults );
 626+ }
 627+
 628+ /**
 629+ * Get the database type used for read operations.
 630+ *
 631+ * @since 0.2
 632+ * @return integer DB_ enum
 633+ */
 634+ public static function getReadDb() {
 635+ return self::$readDb;
 636+ }
 637+
 638+ /**
 639+ * Set the database type to use for read operations.
 640+ *
 641+ * @param integer $db
 642+ *
 643+ * @since 0.2
 644+ */
 645+ public static function setReadDb( $db ) {
 646+ self::$readDb = $db;
 647+ }
 648+
 649+ /**
 650+ * Gets if the object can take a certain field.
 651+ *
 652+ * @since 0.1
 653+ *
 654+ * @param string $name
 655+ *
 656+ * @return boolean
 657+ */
 658+ public static function canHasField( $name ) {
 659+ return array_key_exists( $name, static::getFieldTypes() );
 660+ }
 661+
 662+ /**
 663+ * Takes in a field or array of fields and returns an
 664+ * array with their prefixed versions, ready for db usage.
 665+ *
 666+ * @since 0.1
 667+ *
 668+ * @param array|string $fields
 669+ *
 670+ * @return array
 671+ */
 672+ public static function getPrefixedFields( array $fields ) {
 673+ foreach ( $fields as &$field ) {
 674+ $field = static::getPrefixedField( $field );
 675+ }
 676+
 677+ return $fields;
 678+ }
 679+
 680+ /**
 681+ * Takes in a field and returns an it's prefixed version, ready for db usage.
 682+ * If the field needs to be prefixed for another table, provide an array in the form
 683+ * array( 'tablename', 'fieldname' )
 684+ * Where table name is registered in $wgDBDataObjects.
 685+ *
 686+ * @since 0.1
 687+ *
 688+ * @param string|array $field
 689+ *
 690+ * @return string
 691+ * @throws MWException
 692+ */
 693+ public static function getPrefixedField( $field ) {
 694+ static $prefixes = false;
 695+
 696+ if ( $prefixes === false ) {
 697+ foreach ( $GLOBALS['wgDBDataObjects'] as $classInfo ) {
 698+ $prefixes[$classInfo['table']] = $classInfo['prefix'];
 699+ }
 700+ }
 701+
 702+ if ( is_array( $field ) && count( $field ) > 1 ) {
 703+ if ( array_key_exists( $field[0], $prefixes ) ) {
 704+ $prefix = $prefixes[$field[0]];
 705+ $field = $field[1];
 706+ }
 707+ else {
 708+ throw new MWException( 'Tried to prefix field with unknown table "' . $field[0] . '"' );
 709+ }
 710+ }
 711+ else {
 712+ $prefix = static::getFieldPrefix();
 713+ }
 714+
 715+ return $prefix . $field;
 716+ }
 717+
 718+ /**
 719+ * Takes in an associative array with field names as keys and
 720+ * their values as value. The field names are prefixed with the
 721+ * db field prefix.
 722+ *
 723+ * Field names can also be provided as an array with as first element a table name, such as
 724+ * $conditions = array(
 725+ * array( array( 'tablename', 'fieldname' ), $value ),
 726+ * );
 727+ *
 728+ * @since 0.1
 729+ *
 730+ * @param array $values
 731+ *
 732+ * @return array
 733+ */
 734+ public static function getPrefixedValues( array $values ) {
 735+ $prefixedValues = array();
 736+
 737+ foreach ( $values as $field => $value ) {
 738+ if ( is_integer( $field ) ) {
 739+ if ( is_array( $value ) ) {
 740+ $field = $value[0];
 741+ $value = $value[1];
 742+ }
 743+ else {
 744+ $value = explode( ' ', $value, 2 );
 745+ $value[0] = static::getPrefixedField( $value[0] );
 746+ $prefixedValues[] = implode( ' ', $value );
 747+ continue;
 748+ }
 749+ }
 750+
 751+ $prefixedValues[static::getPrefixedField( $field )] = $value;
 752+ }
 753+
 754+ return $prefixedValues;
 755+ }
 756+
 757+ /**
 758+ * Get an array with fields from a database result,
 759+ * that can be fed directly to the constructor or
 760+ * to setFields.
 761+ *
 762+ * @since 0.1
 763+ *
 764+ * @param object $result
 765+ *
 766+ * @return array
 767+ */
 768+ public static function getFieldsFromDBResult( $result ) {
 769+ $result = (array)$result;
 770+ return array_combine(
 771+ static::unprefixFieldNames( array_keys( $result ) ),
 772+ array_values( $result )
 773+ );
 774+ }
 775+
 776+ /**
 777+ * Takes a field name with prefix and returns the unprefixed equivalent.
 778+ *
 779+ * @since 0.1
 780+ *
 781+ * @param string $fieldName
 782+ *
 783+ * @return string
 784+ */
 785+ public static function unprefixFieldName( $fieldName ) {
 786+ return substr( $fieldName, strlen( static::getFieldPrefix() ) );
 787+ }
 788+
 789+ /**
 790+ * Takes an array of field names with prefix and returns the unprefixed equivalent.
 791+ *
 792+ * @since 0.1
 793+ *
 794+ * @param array $fieldNames
 795+ *
 796+ * @return array
 797+ */
 798+ public static function unprefixFieldNames( array $fieldNames ) {
 799+ return array_map( 'static::unprefixFieldName', $fieldNames );
 800+ }
 801+
 802+ /**
 803+ * Get a new instance of the class from a database result.
 804+ *
 805+ * @since 0.1
 806+ *
 807+ * @param stdClass $result
 808+ *
 809+ * @return DBDataObject
 810+ */
 811+ public static function newFromDBResult( stdClass $result ) {
 812+ return static::newFromArray( static::getFieldsFromDBResult( $result ) );
 813+ }
 814+
 815+ /**
 816+ * Removes the object from the database.
 817+ *
 818+ * @since 0.1
 819+ *
 820+ * @param array $conditions
 821+ *
 822+ * @return boolean Success indicator
 823+ */
 824+ public static function delete( array $conditions ) {
 825+ return wfGetDB( DB_MASTER )->delete(
 826+ static::getDBTable(),
 827+ static::getPrefixedValues( $conditions )
 828+ );
 829+ }
 830+
 831+ /**
 832+ * Add an amount (can be negative) to the specified field (needs to be numeric).
 833+ *
 834+ * @since 0.1
 835+ *
 836+ * @param string $field
 837+ * @param integer $amount
 838+ *
 839+ * @return boolean Success indicator
 840+ */
 841+ public static function addToField( $field, $amount ) {
 842+ if ( $amount == 0 ) {
 843+ return true;
 844+ }
 845+
 846+ if ( !static::hasIdField() ) {
 847+ return false;
 848+ }
 849+
 850+ $absoluteAmount = abs( $amount );
 851+ $isNegative = $amount < 0;
 852+
 853+ $dbw = wfGetDB( DB_MASTER );
 854+
 855+ $fullField = static::getPrefixedField( $field );
 856+
 857+ $success = $dbw->update(
 858+ static::getDBTable(),
 859+ array( "$fullField=$fullField" . ( $isNegative ? '-' : '+' ) . $absoluteAmount ),
 860+ array( static::getPrefixedField( 'id' ) => static::getId() ),
 861+ __METHOD__
 862+ );
 863+
 864+ if ( $success && static::hasField( $field ) ) {
 865+ static::setField( $field, static::getField( $field ) + $amount );
 866+ }
 867+
 868+ return $success;
 869+ }
 870+
 871+ /**
 872+ * Selects the the specified fields of the records matching the provided
 873+ * conditions and returns them as DBDataObject. Field names get prefixed.
 874+ *
 875+ * @since 0.1
 876+ *
 877+ * @param array|string|null $fields
 878+ * @param array $conditions
 879+ * @param array $options
 880+ * @param array $joinConds
 881+ *
 882+ * @return array of self
 883+ */
 884+ public static function select( $fields = null, array $conditions = array(), array $options = array(), array $joinConds = array() ) {
 885+ $result = static::selectFields( $fields, $conditions, $options, $joinConds, false );
 886+
 887+ $objects = array();
 888+
 889+ foreach ( $result as $record ) {
 890+ $objects[] = static::newFromArray( $record );
 891+ }
 892+
 893+ return $objects;
 894+ }
 895+
 896+ /**
 897+ * Selects the the specified fields of the records matching the provided
 898+ * conditions and returns them as associative arrays.
 899+ * Provided field names get prefixed.
 900+ * Returned field names will not have a prefix.
 901+ *
 902+ * When $collapse is true:
 903+ * If one field is selected, each item in the result array will be this field.
 904+ * If two fields are selected, each item in the result array will have as key
 905+ * the first field and as value the second field.
 906+ * If more then two fields are selected, each item will be an associative array.
 907+ *
 908+ * @since 0.1
 909+ *
 910+ * @param array|string|null $fields
 911+ * @param array $conditions
 912+ * @param array $options
 913+ * @param array $joinConds
 914+ * @param boolean $collapse Set to false to always return each result row as associative array.
 915+ *
 916+ * @return array of array
 917+ */
 918+ public static function selectFields( $fields = null, array $conditions = array(), array $options = array(), array $joinConds = array(), $collapse = true ) {
 919+ if ( is_null( $fields ) ) {
 920+ $fields = array_keys( static::getFieldTypes() );
 921+ }
 922+ else {
 923+ $fields = (array)$fields;
 924+ }
 925+
 926+ $tables = array( static::getDBTable() );
 927+ $joinConds = static::getProcessedJoinConds( $joinConds, $tables );
 928+
 929+ $result = static::rawSelect(
 930+ static::getPrefixedFields( $fields ),
 931+ static::getPrefixedValues( $conditions ),
 932+ $options,
 933+ $joinConds,
 934+ $tables
 935+ );
 936+
 937+ $objects = array();
 938+
 939+ foreach ( $result as $record ) {
 940+ $objects[] = static::getFieldsFromDBResult( $record );
 941+ }
 942+
 943+ if ( $collapse ) {
 944+ if ( count( $fields ) === 1 ) {
 945+ $objects = array_map( function( $object ) { return array_shift( $object ); } , $objects );
 946+ }
 947+ elseif ( count( $fields ) === 2 ) {
 948+ $o = array();
 949+
 950+ foreach ( $objects as $object ) {
 951+ $o[array_shift( $object )] = array_shift( $object );
 952+ }
 953+
 954+ $objects = $o;
 955+ }
 956+ }
 957+
 958+ return $objects;
 959+ }
 960+
 961+ /**
 962+ * Process the join conditions. This includes prefixing table and field names,
 963+ * and adding of needed tables.
 964+ *
 965+ * @since 0.1
 966+ *
 967+ * @param array $joinConds Join conditions without prefixes and fields in array rather then string with equals sign.
 968+ * @param array $tables List of tables to which the extra needed ones get added.
 969+ *
 970+ * @return array Join conditions ready to be fed to MediaWikis native select function.
 971+ */
 972+ protected static function getProcessedJoinConds( array $joinConds, array &$tables ) {
 973+ $conds = array();
 974+
 975+ foreach ( $joinConds as $table => $joinCond ) {
 976+ if ( !in_array( $table, $tables ) ) {
 977+ $tables[] = $table;
 978+ }
 979+
 980+ $cond = array( $joinCond[0], array() );
 981+
 982+ foreach ( $joinCond[1] as $joinCondPart ) {
 983+ $parts = array(
 984+ static::getPrefixedField( $joinCondPart[0] ),
 985+ static::getPrefixedField( $joinCondPart[1] ),
 986+ );
 987+
 988+ if ( !in_array( $joinCondPart[0][0], $tables ) ) {
 989+ $tables[] = $joinCondPart[0][0];
 990+ }
 991+
 992+ if ( !in_array( $joinCondPart[1][0], $tables ) ) {
 993+ $tables[] = $joinCondPart[1][0];
 994+ }
 995+
 996+ $cond[1][] = implode( '=', $parts );
 997+ }
 998+
 999+ $conds[$table] = $cond;
 1000+ }
 1001+
 1002+ return $conds;
 1003+ }
 1004+
 1005+ /**
 1006+ * Selects the the specified fields of the first matching record.
 1007+ * Field names get prefixed.
 1008+ *
 1009+ * @since 0.1
 1010+ *
 1011+ * @param array|string|null $fields
 1012+ * @param array $conditions
 1013+ * @param array $options
 1014+ * @param array $joinConds
 1015+ *
 1016+ * @return EPBObject|false
 1017+ */
 1018+ public static function selectRow( $fields = null, array $conditions = array(), array $options = array(), array $joinConds = array() ) {
 1019+ $options['LIMIT'] = 1;
 1020+
 1021+ $objects = static::select( $fields, $conditions, $options, $joinConds );
 1022+
 1023+ return count( $objects ) > 0 ? $objects[0] : false;
 1024+ }
 1025+
 1026+ /**
 1027+ * Selects the the specified fields of the first record matching the provided
 1028+ * conditions and returns it as an associative array, or false when nothing matches.
 1029+ * This method makes use of selectFields and expects the same parameters and
 1030+ * returns the same results (if there are any, if there are none, this method returns false).
 1031+ * @see DBDataObject::selectFields
 1032+ *
 1033+ * @since 0.1
 1034+ *
 1035+ * @param array|string|null $fields
 1036+ * @param array $conditions
 1037+ * @param array $options
 1038+ * @param array $joinConds
 1039+ * @param boolean $collapse Set to false to always return each result row as associative array.
 1040+ *
 1041+ * @return mixed|array|false
 1042+ */
 1043+ public static function selectFieldsRow( $fields = null, array $conditions = array(), array $options = array(), array $joinConds = array(), $collapse = true ) {
 1044+ $options['LIMIT'] = 1;
 1045+
 1046+ $objects = static::selectFields( $fields, $conditions, $options, $joinConds, $collapse );
 1047+
 1048+ return count( $objects ) > 0 ? $objects[0] : false;
 1049+ }
 1050+
 1051+ /**
 1052+ * Returns if there is at least one record matching the provided conditions.
 1053+ * Condition field names get prefixed.
 1054+ *
 1055+ * @since 0.1
 1056+ *
 1057+ * @param array $conditions
 1058+ *
 1059+ * @return boolean
 1060+ */
 1061+ public static function has( array $conditions = array() ) {
 1062+ return static::selectRow( array( 'id' ), $conditions ) !== false;
 1063+ }
 1064+
 1065+ /**
 1066+ * Returns the amount of matching records.
 1067+ * Condition field names get prefixed.
 1068+ *
 1069+ * @since 0.1
 1070+ *
 1071+ * @param array $conditions
 1072+ * @param array $options
 1073+ *
 1074+ * @return integer
 1075+ */
 1076+ public static function count( array $conditions = array(), array $options = array() ) {
 1077+ $res = static::rawSelect(
 1078+ array( 'COUNT(*) AS rowcount' ),
 1079+ static::getPrefixedValues( $conditions ),
 1080+ $options
 1081+ )->fetchObject();
 1082+
 1083+ return $res->rowcount;
 1084+ }
 1085+
 1086+ /**
 1087+ * Selects the the specified fields of the records matching the provided
 1088+ * conditions. Field names do NOT get prefixed.
 1089+ *
 1090+ * @since 0.1
 1091+ *
 1092+ * @param array $fields
 1093+ * @param array $conditions
 1094+ * @param array $options
 1095+ * @param array $joinConds
 1096+ * @param array $tables
 1097+ *
 1098+ * @return ResultWrapper
 1099+ */
 1100+ public static function rawSelect( array $fields, array $conditions = array(), array $options = array(), array $joinConds = array(), array $tables = null ) {
 1101+ if ( is_null( $tables ) ) {
 1102+ $tables = static::getDBTable();
 1103+ }
 1104+
 1105+ $dbr = wfGetDB( static::getReadDb() );
 1106+
 1107+ return $dbr->select(
 1108+ $tables,
 1109+ $fields,
 1110+ count( $conditions ) == 0 ? '' : $conditions,
 1111+ __METHOD__,
 1112+ $options,
 1113+ $joinConds
 1114+ );
 1115+ }
 1116+
 1117+ /**
 1118+ * Update the records matching the provided conditions by
 1119+ * setting the fields that are keys in the $values param to
 1120+ * their corresponding values.
 1121+ *
 1122+ * @since 0.1
 1123+ *
 1124+ * @param array $values
 1125+ * @param array $conditions
 1126+ *
 1127+ * @return boolean Success indicator
 1128+ */
 1129+ public static function update( array $values, array $conditions = array() ) {
 1130+ $dbw = wfGetDB( DB_MASTER );
 1131+
 1132+ return $dbw->update(
 1133+ static::getDBTable(),
 1134+ static::getPrefixedValues( $values ),
 1135+ static::getPrefixedValues( $conditions ),
 1136+ __METHOD__
 1137+ );
 1138+ }
 1139+
 1140+ /**
 1141+ * Return the names of the fields.
 1142+ *
 1143+ * @since 0.1
 1144+ *
 1145+ * @return array
 1146+ */
 1147+ public static function getFieldNames() {
 1148+ return array_keys( static::getFieldTypes() );
 1149+ }
 1150+
 1151+ /**
 1152+ * Returns an array with the fields and their descriptions.
 1153+ *
 1154+ * field name => field description
 1155+ *
 1156+ * @since 0.1
 1157+ *
 1158+ * @return array
 1159+ */
 1160+ public static function getFieldDescriptions() {
 1161+ return array();
 1162+ }
 1163+
 1164+ /**
 1165+ * Get API parameters for the fields supported by this object.
 1166+ *
 1167+ * @since 0.1
 1168+ *
 1169+ * @param boolean $requireParams
 1170+ * @param boolean $setDefaults
 1171+ *
 1172+ * @return array
 1173+ */
 1174+ public static function getAPIParams( $requireParams = false, $setDefaults = false ) {
 1175+ $typeMap = array(
 1176+ 'id' => 'integer',
 1177+ 'int' => 'integer',
 1178+ 'float' => 'NULL',
 1179+ 'str' => 'string',
 1180+ 'bool' => 'integer',
 1181+ 'array' => 'string',
 1182+ 'blob' => 'string',
 1183+ );
 1184+
 1185+ $params = array();
 1186+ $defaults = static::getDefaults();
 1187+
 1188+ foreach ( static::getFieldTypes() as $field => $type ) {
 1189+ if ( $field == 'id' ) {
 1190+ continue;
 1191+ }
 1192+
 1193+ $hasDefault = array_key_exists( $field, $defaults );
 1194+
 1195+ $params[$field] = array(
 1196+ ApiBase::PARAM_TYPE => $typeMap[$type],
 1197+ ApiBase::PARAM_REQUIRED => $requireParams && !$hasDefault
 1198+ );
 1199+
 1200+ if ( $type == 'array' ) {
 1201+ $params[$field][ApiBase::PARAM_ISMULTI] = true;
 1202+ }
 1203+
 1204+ if ( $setDefaults && $hasDefault ) {
 1205+ $default = is_array( $defaults[$field] ) ? implode( '|', $defaults[$field] ) : $defaults[$field];
 1206+ $params[$field][ApiBase::PARAM_DFLT] = $default;
 1207+ }
 1208+ }
 1209+
 1210+ return $params;
 1211+ }
 1212+
 1213+ /**
 1214+ * Computes and updates the values of the summary fields.
 1215+ *
 1216+ * @since 0.1
 1217+ *
 1218+ * @param array|string|null $summaryFields
 1219+ */
 1220+ public function loadSummaryFields( $summaryFields = null ) {
 1221+
 1222+ }
 1223+
 1224+ /**
 1225+ * Computes the values of the summary fields of the objects matching the provided conditions.
 1226+ *
 1227+ * @since 0.1
 1228+ *
 1229+ * @param array|string|null $summaryFields
 1230+ * @param array $conditions
 1231+ */
 1232+ public static function updateSummaryFields( $summaryFields = null, array $conditions = array() ) {
 1233+ self::setReadDb( DB_MASTER );
 1234+
 1235+ foreach ( self::select( null, $conditions ) as /* DBDataObject */ $item ) {
 1236+ $item->loadSummaryFields( $summaryFields );
 1237+ $item->setSummaryMode( true );
 1238+ $item->saveExisting();
 1239+ }
 1240+
 1241+ self::setReadDb( DB_SLAVE );
 1242+ }
 1243+
 1244+ /**
 1245+ * Sets the value for the @see $updateSummaries field.
 1246+ *
 1247+ * @since 0.1
 1248+ *
 1249+ * @param boolean $update
 1250+ */
 1251+ public function setUpdateSummaries( $update ) {
 1252+ $this->updateSummaries = $update;
 1253+ }
 1254+
 1255+ /**
 1256+ * Sets the value for the @see $inSummaryMode field.
 1257+ *
 1258+ * @since 0.1
 1259+ *
 1260+ * @param boolean $update
 1261+ */
 1262+ public function setSummaryMode( $summaryMode ) {
 1263+ $this->inSummaryMode = $summaryMode;
 1264+ }
 1265+
 1266+ /**
 1267+ * Return if any fields got changed.
 1268+ *
 1269+ * @since 0.1
 1270+ *
 1271+ * @param DBDataObject $object
 1272+ * @param boolean $excludeSummaryFields When set to true, summary field changes are ignored.
 1273+ *
 1274+ * @return boolean
 1275+ */
 1276+ protected function fieldsChanged( DBDataObject $object, $excludeSummaryFields = false ) {
 1277+ foreach ( $this->fields as $name => $value ) {
 1278+ $excluded = $excludeSummaryFields && in_array( $name, $this->getSummaryFields() );
 1279+
 1280+ if ( !$excluded && $object->getField( $name ) !== $value ) {
 1281+ return true;
 1282+ }
 1283+ }
 1284+
 1285+ return false;
 1286+ }
 1287+
 1288+}
Property changes on: trunk/phase3/includes/DBDataObject.php
___________________________________________________________________
Added: svn:eol-style
11289 + native
Index: trunk/phase3/includes/AutoLoader.php
@@ -50,6 +50,7 @@
5151 'Cookie' => 'includes/Cookie.php',
5252 'CookieJar' => 'includes/Cookie.php',
5353 'CurlHttpRequest' => 'includes/HttpFunctions.php',
 54+ 'DBDataObject' => 'includes/DBDataObject.php',
5455 'DeferrableUpdate' => 'includes/DeferredUpdates.php',
5556 'DeferredUpdates' => 'includes/DeferredUpdates.php',
5657 'DerivativeRequest' => 'includes/WebRequest.php',

Follow-up revisions

RevisionCommit summaryAuthorDate
r111266Follow up to r111264; update @since tagsjeroendedauw21:49, 11 February 2012
r111291use array_diff instead of self written equivalentjeroendedauw13:52, 12 February 2012
r111295rename canHas to canHavejeroendedauw14:05, 12 February 2012
r111305follow up to r111264 - get rid of wgDBDataObjects as per suggestion by Platon...jeroendedauw15:51, 12 February 2012
r111309Convert rawSelect() into rawSelectRow()...platonides17:36, 12 February 2012
r111310Useless wrapping on an anonymous function. Follow-up r111264platonides17:47, 12 February 2012
r111468follow up to r111264; split up class into one representing a table and one re...jeroendedauw18:11, 14 February 2012

Comments

#Comment by Platonides (talk | contribs)   00:44, 12 February 2012

Your use of an anonymous function in line 215 is PHP 5.3+ only (probably even more due to the use clause).

#Comment by Jeroen De Dauw (talk | contribs)   01:06, 12 February 2012

This class is 5.3 dependent as described in the docs. You can get rid of the anon functions, but not of the late static bindings (unless you want to do evil stuff).

That's the way it is unfortunately - only code that can depend on 5.3 can use this. That means none of core for now, until we ditch 5.2, but most new extensions can happily require 5.3 (esp since 5.3 is now on the cluster).

#Comment by Bawolff (talk | contribs)   01:12, 12 February 2012

I'm really not a fan of putting stuff in core that requires 5.3 until such a time as we have bumped the required version to 5.3. Even if only extensions use the code (which honestly, probably won't stay that way once these classes are in core) it seems rather wrong.

#Comment by Jeroen De Dauw (talk | contribs)   01:43, 12 February 2012

I see your point. This would need clear docs that it requires 5.3 to avoid accidents. Then again, these should be easy to spot. This class could theoretically be put into it's own extension, but the hassle involved with this is probably more work then fixing any misuse of this class if it where to happen when it's in core.

Sort of makes me wonder when we finally will drop 5.2.x support in core though. That version lost official support by the PHP guys long ago (2 years?). This class is one more reason to drop support.

#Comment by Platonides (talk | contribs)   14:44, 12 February 2012

By the late static binding, do you mean anything else than the get_called_class()? That function can be emulated very inefficiently by using debug_backtrace()

#Comment by 😂 (talk | contribs)   15:01, 12 February 2012

The uses of static:: are LSB. Although I remain convinced they're a hack to work around poor initial design decisions 90% of the time.

#Comment by Jeroen De Dauw (talk | contribs)   15:08, 12 February 2012

If you think this class falls in this 90%, please suggest how to get similar functionality without LSB so we can make this compatible with 5.2. I don't think you can do this in a nice way, but would be glad to be proven wrong about this :)

#Comment by 😂 (talk | contribs)   15:11, 12 February 2012

I'm not sure what the utility of making so much of it static is. Why not instantiate classes and then call ->delete() etc on the objects.

#Comment by Jeroen De Dauw (talk | contribs)   15:20, 12 February 2012

An instance represents a row. So if you want to do an operation on a table as a whole, it makes no sense to make an instance. I think it even does not make sense if you ignore that first argument. Why would you create an instance of an object if there is absolutely no need to?

$instance = new MyClass();
$instance->someNonInstanceBehaviour();
MyClass::someNonInstanceBehaviour();

The later one looks more sane to me.

#Comment by 😂 (talk | contribs)   15:53, 12 February 2012

Not necessarily more sane, just lazier. Instantiating classes to call a method is a basic tenet of OOP. Static should be used for things that truly should be called from outside of the class on a non-instance of it (such as factory methods or utility functions). Making it static to save a line of code at the expense of making the code more complex isn't a win in my book.

#Comment by Jeroen De Dauw (talk | contribs)   16:09, 12 February 2012

More complex? You have static methods instead of non-static ones. That's not more complex.

What would you propose doing instead? Having some singleton like pattern and calling these methods on an instance of the class? This is what I am doing in the Contest extension because it needed to be PHP 5.2 compat, and I think it's rather evil to have methods that should be static be non-static like that.

> Static should be used for things that truly should be called from outside of the class on a non-instance of it

I guess this is our main point of disagreement. Why do you think this is the case? And how does your reasoning fir in with things such as Linker::link, which has been made static?

> Not necessarily more sane, just lazier

This is about doing the right thing. Not about saving a line of code. No need to accuse me of being lazy.

#Comment by 😂 (talk | contribs)   16:14, 12 February 2012

No need for a singleton either, just instantiate it when you need it.

Linker::link() is a utility function, so is one of the "ok for static" cases I mentioned above.

#Comment by Jeroen De Dauw (talk | contribs)   16:21, 12 February 2012

So you would make the getDBTable, getFieldPrefix, getFieldTypes and similar methods non-static? And then create an instance of the class to call them and get the completely not instance related info they return? This seems evil. The info belongs to the class, not one instance.

#Comment by Platonides (talk | contribs)   19:07, 12 February 2012

Create two classes, one representing the table, and a second one for the rows (which would hardly need to be overriden).

#Comment by MaxSem (talk | contribs)   13:00, 12 February 2012

Resetting to fixme, I'm not convinced that making some parts of the core require a different PHP version is wise.

#Comment by Jeroen De Dauw (talk | contribs)   13:34, 12 February 2012

I don't see the harm done really. This is an utility included in core that needs a different version. If it's not used by anything in core, it's not really part of it in a way.

Either way, I want some clarifications about the PHP 5.3 policies we want to have, so will post to the list.

#Comment by 😂 (talk | contribs)   13:58, 12 February 2012

canHasField() should be renamed to a real verb phrase -- maybe canHaveField()

#Comment by Jeroen De Dauw (talk | contribs)   14:06, 12 February 2012

r111295 :)

#Comment by Platonides (talk | contribs)   14:52, 12 February 2012

I'm not convinced about this class. For one, why use $wgDBDataObjects if each object will need to get its own class anyway?

It's easier to override both getDBTable() and getFieldPrefix()


Then there's the way in how it tries to load half-columns. I assume each instance corresponds to a table row? If a system needs to load the same row twice, it's very likely that someone was coded wrong. I'm not saying we shouldn't support that, but I'd log such thing.

#Comment by Jeroen De Dauw (talk | contribs)   15:15, 12 February 2012

> It's easier to override both getDBTable() and getFieldPrefix()

This is actually what it originally did. The reason why I changed it is gone now, so this could easily be changed back. This really is a implementation detail that does not affect the rest of the code at all, so judging the class on that seems odd :)

> I assume each instance corresponds to a table row?

Yeah it does.

> Then there's the way in how it tries to load half-columns.

Sorry, I'm not following you here. half-columns? Are you referring to the fact that you can choose to load only a subset of the fields?

> If a system needs to load the same row twice, it's very likely that someone was coded wrong.

Load it twice? Huh? It does not need to do this...

#Comment by Platonides (talk | contribs)   16:38, 12 February 2012

I wasn't clear. I was refering to how you can load a subset, then if you need a non-loaded field, go filtering with that anonymous function. That second load shouldn't happen in good code (though it's not bad to take the possibility into account).

#Comment by Jeroen De Dauw (talk | contribs)   16:45, 12 February 2012

Yes, I agree, this should not happen. AFAIK this is not happening anywhere in the Education Program extension, which makes heavy use of this class, so it can definitely be avoided. The idea is that you load all fields you need when the object gets constructed. If you then try to get a field that you did not load, it will throw an exception. There are cases where it might be impossible to know in advance what you will need though. For this the class allows for loading additional things later on (with the option to not re-load already loaded data). I don't see any way to avoid doing 2 selects in such cases. If you have an idea that works, I'd be happy to hear it :)

#Comment by Jeroen De Dauw (talk | contribs)   15:27, 12 February 2012

I'll have to correct myself there. The original reason for having $wgDBDataObjects is not gone. It's there to also be able to automatically add prefixes for tables that do not have their own class in joins (typically tables that just link stuff). I'm actually not convinced myself that this join code is any good. Where all other code in this class makes stuff easier, this is one place where it just adds hassle. So I'm thinking of just throwing that out, after which the class can return to using overriding of the methods you mentioned.

#Comment by Jeroen De Dauw (talk | contribs)   15:52, 12 February 2012

Gone in r111305

#Comment by 😂 (talk | contribs)   15:14, 12 February 2012

Unrelated to other comment threads, but definitely needs some unit tests if it's kept.

#Comment by Jeroen De Dauw (talk | contribs)   15:16, 12 February 2012

+1. I will write these if this can stay in core.

#Comment by Hashar (talk | contribs)   17:27, 12 February 2012

In the long run, It would be ten time easier to reuses an existing ORM such as doctrine or propel (from symphony). I do not think we want that kind of home-brewed ORM :

  • it adds to our maintenance / code review queue
  • it is yet another barrier to new developers
  • it will cause bug
  • it lacks a design document

Finally I am unsure which needs it is intended to fulfill, much less if it is going to be able to fulfill the perceived needs at this time.

I would personally bury that somewhere. Start by writing the needs we have, evaluate existing solutions and then if and only if none of them fit our needs, start writing our own ORM.

#Comment by Jeroen De Dauw (talk | contribs)   17:36, 12 February 2012

> reuses an existing ORM such as doctrine or propel

> it adds to our maintenance / code review queue

You'd need to adapt these to work with our existing DB abstraction layer or throw the later out altogether. This does NOT decrease maintenance or review costs.

> it is yet another barrier to new developers

You can make this argument about every utility added. Really, this is like asking why we got Html when we had Xml. You are not forced to use this and can just stick with the regular db access functions. To be fair though, this might actually be easier to use for new developers.

> it will cause bug

Yeah, that happens with code and can be listed as a potential side effect of every addition.

> it lacks a design document

It's better documented then most of our current code.

> Finally I am unsure which needs it is intended to fulfill

Did you read the docs? It lists the reasons? It avoids a lot of work, avoids a lot of code duplication and increases consistency. I wrote the class because I felt the need to have such an utility and have used it to great effect in my last 5 extensions.

#Comment by Platonides (talk | contribs)   17:45, 12 February 2012

I don't like getReadDb() / setReadDb(). I would have expected getReadDb() to return a DB object. And setReadDB is used only in updateSummaryFields() to switch a parameter used two functions down in the stack.

#Comment by Jeroen De Dauw (talk | contribs)   17:50, 12 February 2012

What would you propose as alternative?

These functions are needed to set the type of database used to read so you can set it to DB_MASTER in cases where you'd otherwise get bitten by replag. This normally used from outside of the class, so has little to do with updateSummaryFields which happens to also use it.

#Comment by Jeroen De Dauw (talk | contribs)   20:47, 12 February 2012

(unindent, reply to Special:Code/MediaWiki/111264#c30852)

That's an interesting idea. I could live with having these methods non-static in a class representing a table. I'm not exactly sure how the split would work. Would you have a table class and a row class for each table? If so, the table classes would not hold a lot of stuff, they would just hold these methods plus something that associates their corresponding row class (unless you have some kind of table registration system, in which case you could have one single table class which knows what to use if you specify which table you are talking about and return an object of the right row class).

I'd love to see more ideas on how this should be done.

#Comment by Platonides (talk | contribs)   21:40, 12 February 2012

Move all static methods to a table class. The static::foo() calls become $this->tableClass->foo()

You interface just with the table class and the objects it returns. The base row class could usually be the used one for many applications.

#Comment by Jeroen De Dauw (talk | contribs)   21:48, 12 February 2012

Sure. We could do that, but this does not answer my questions. Do we have 2 classes per table? How is the table class linked to the row one?

#Comment by Platonides (talk | contribs)   23:05, 12 February 2012

Theoretically yes. In practise, I'd like that the row class would be reusable, and in most classes you don't need to extend it.

The way the parent knows the name of the class to create, it can be a (const) variable or returned by a method.

#Comment by Jeroen De Dauw (talk | contribs)   23:23, 12 February 2012

> and in most classes you don't need to extend it.

In ALL cases for every extension I used this for, it would need to be extended, in most cases very non-trivially. After all, if you have a class Course that represents a row in the courses table, all course logic goes there right. So I don't think you'd gain much advantage by doing this, although there is of course no reason to go enforce deriving. Either way, I don't think this is an issue.

What I think is silly is that the table classes will often end up just holding the methods returning table specific data. I can't think of any table specific logic I'd put there (everything that can be derived from the info the class holds can be abstracted out to the parent class).

So good things of this approach:

  • No need for LSB AFAICS
  • Nicer separation between table and row specific functionality.

Things I like less:

  • Two classes needed for almost all types instead of one.
  • Table classes will likely just hold three or so methods returning table info.

In a scenario where PHP 5.3 could be used everywhere, I don't think the advantages would be worth the disadvantages, so I'm hesitant to make such changes.

#Comment by Platonides (talk | contribs)   22:57, 13 February 2012

The parent table could instead provide variables overriden in the child, that could make the overriding a bit shorter.

I prefer that separation between table/rows. If rows will hold a lot of logic, well, so be it. Two classes per table.

#Comment by Tim Starling (talk | contribs)   07:00, 13 February 2012

The use of static variables to store database connection objects, and indeed heavy use of static variables generally, is not acceptable. It's basically equivalent to using global variables, except for the syntax. It's the opposite to the direction we are heading with state architecture in MediaWiki.

#Comment by Jeroen De Dauw (talk | contribs)   13:44, 13 February 2012

> The use of static variables to store database connection objects

There is one static field readDb, which is clearly documented as storing an integer (element of DB_ enum), not a database connection object.

> heavy use of static variables generally

Like I said, there is one such field, and it's not part of the core functionality (so how does this count as heavy use?), so can be killed, or probably better, be replaced by an additional optional argument for all methods that read from the db.

#Comment by Jeroen De Dauw (talk | contribs)   21:51, 14 February 2012

Well, I split up the class and got rid of the late static binding stuff (see follow up revs). There might still be some PHP 5.3 stuff left, but I'll get rid of that. So it would seem the concerns expressed here are all addressed, so this should be changed to resolved I think.

#Comment by 😂 (talk | contribs)   21:54, 14 February 2012

That count() function is kind of scary. There's a reason DatabaseBase::estimateRowCount() exists, since a COUNT(*) query on large tables is bad.

#Comment by Jeroen De Dauw (talk | contribs)   22:25, 14 February 2012

It's not bad on small tables. But you are right, I ought to add a warning in the method docs.

#Comment by Jeroen De Dauw (talk | contribs)   22:27, 14 February 2012

Status & tagging log