MaxSem has uploaded a new change for review. ( 
https://gerrit.wikimedia.org/r/391747 )

Change subject: WIP: Rewrite user options support
......................................................................

WIP: Rewrite user options support

Move stuff from User god class, isolate concerns.
Nowhere near done, but serves as a demonstration of a concept.

Change-Id: I2726eee9f6b18b1f922995f91f28a70d01586d74
---
M includes/MediaWikiServices.php
A includes/user/ReadOnlyUserOptions.php
M includes/user/User.php
A includes/user/UserOptions.php
A includes/user/UserOptionsProvider.php
M tests/phpunit/includes/user/UserTest.php
6 files changed, 199 insertions(+), 125 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/mediawiki/core 
refs/changes/47/391747/1

diff --git a/includes/MediaWikiServices.php b/includes/MediaWikiServices.php
index 19b71f1..dec90bc 100644
--- a/includes/MediaWikiServices.php
+++ b/includes/MediaWikiServices.php
@@ -698,6 +698,14 @@
                return $this->getService( 'ExternalStoreFactory' );
        }
 
+       /**
+        * @since 1.31
+        * @return \UserOptionsProvider
+        */
+       public function getUserOptionsProvider() {
+               return $this->getService( 'UserOptionsProvider' );
+       }
+
        
///////////////////////////////////////////////////////////////////////////
        // NOTE: When adding a service getter here, don't forget to add a test
        // case for it in MediaWikiServicesTest::provideGetters() and in
diff --git a/includes/user/ReadOnlyUserOptions.php 
b/includes/user/ReadOnlyUserOptions.php
new file mode 100644
index 0000000..340bdc2
--- /dev/null
+++ b/includes/user/ReadOnlyUserOptions.php
@@ -0,0 +1,21 @@
+<?php
+
+class ReadOnlyUserOptions {
+       protected $data;
+
+       public function __construct( array $data ) {
+               $this->data = $data;
+       }
+
+       public function has( $optionName ) {
+               return isset( $this->data[$optionName] );
+       }
+
+       public function get( $optionName ) {
+               return isset( $this->data[$optionName] ) ? 
$this->data[$optionName] : null;
+       }
+
+       public function getAll() {
+               return $this->data;
+       }
+}
diff --git a/includes/user/User.php b/includes/user/User.php
index 854ebbd..a62ef20 100644
--- a/includes/user/User.php
+++ b/includes/user/User.php
@@ -234,8 +234,8 @@
        private $mGroups;
        /** @var array Associative array of (group name => UserGroupMembership 
object) */
        protected $mGroupMemberships;
-       /** @var array */
-       protected $mOptionOverrides;
+       /** @var UserOptions */
+       private $userOptions;
        // @}
 
        /**
@@ -495,7 +495,6 @@
 
                                $this->loadFromDatabase( self::READ_NORMAL );
                                $this->loadGroups();
-                               $this->loadOptions();
 
                                $data = [];
                                foreach ( self::$mCacheVars as $name ) {
@@ -1320,7 +1319,7 @@
         *                table. Previously you were supposed to pass an array 
of strings
         *                here, but we also need expiry info nowadays, so an 
array of
         *                strings is ignored.
-        *  user_properties   Array with properties out of the user_properties 
table
+        *  user_properties   Deprecated, don't use
         */
        protected function loadFromRow( $row, $data = null ) {
                $all = true;
@@ -1408,7 +1407,8 @@
                                }
                        }
                        if ( isset( $data['user_properties'] ) && is_array( 
$data['user_properties'] ) ) {
-                               $this->loadOptions( $data['user_properties'] );
+                               throw new Exception( 'Passing user_properties 
to ' . __METHOD__
+                                       . ' is not supported anymore' );
                        }
                }
        }
@@ -1569,7 +1569,7 @@
                $this->mEffectiveGroups = null;
                $this->mImplicitGroups = null;
                $this->mGroupMemberships = null;
-               $this->mOptions = null;
+               $this->userOptions = null;
                $this->mOptionsLoaded = false;
                $this->mEditCount = null;
 
@@ -1583,44 +1583,20 @@
         * Combine the language default options with any site-specific options
         * and add the default language variants.
         *
+        * @deprecated since 1.31, use UserOptionsProvider
+        *
         * @return array Array of String options
         */
        public static function getDefaultOptions() {
-               global $wgNamespacesToBeSearchedDefault, $wgDefaultUserOptions, 
$wgContLang, $wgDefaultSkin;
+               $provider = 
MediaWikiServices::getInstance()->getUserOptionsProvider();
 
-               static $defOpt = null;
-               static $defOptLang = null;
-
-               if ( $defOpt !== null && $defOptLang === $wgContLang->getCode() 
) {
-                       // $wgContLang does not change (and should not change) 
mid-request,
-                       // but the unit tests change it anyway, and expect this 
method to
-                       // return values relevant to the current $wgContLang.
-                       return $defOpt;
-               }
-
-               $defOpt = $wgDefaultUserOptions;
-               // Default language setting
-               $defOptLang = $wgContLang->getCode();
-               $defOpt['language'] = $defOptLang;
-               foreach ( LanguageConverter::$languagesWithVariants as 
$langCode ) {
-                       $defOpt[$langCode == $wgContLang->getCode() ? 'variant' 
: "variant-$langCode"] = $langCode;
-               }
-
-               // NOTE: don't use SearchEngineConfig::getSearchableNamespaces 
here,
-               // since extensions may change the set of searchable namespaces 
depending
-               // on user groups/permissions.
-               foreach ( $wgNamespacesToBeSearchedDefault as $nsnum => $val ) {
-                       $defOpt['searchNs' . $nsnum] = (bool)$val;
-               }
-               $defOpt['skin'] = Skin::normalizeKey( $wgDefaultSkin );
-
-               Hooks::run( 'UserGetDefaultOptions', [ &$defOpt ] );
-
-               return $defOpt;
+               return $provider->getDefaultOptions()->getAll();
        }
 
        /**
         * Get a given default option value.
+        *
+        * @deprecated since 1.31, use UserOptionsProvider
         *
         * @param string $opt Name of option to retrieve
         * @return string Default option value
@@ -2890,7 +2866,6 @@
         */
        public function getOption( $oname, $defaultOverride = null, 
$ignoreHidden = false ) {
                global $wgHiddenPrefs;
-               $this->loadOptions();
 
                # We want 'disabled' preferences to always behave as the 
default value for
                # users, even if they have set the option explicitly in their 
settings (ie they
@@ -2908,6 +2883,16 @@
                }
        }
 
+       public function getOptionsObject() {
+               if ( !$this->userOptions ) {
+                       $this->userOptions = MediaWikiServices::getInstance()
+                               ->getUserOptionsProvider()
+                               ->getUserOptions( $this, $this->queryFlagsUsed 
);
+               }
+
+               return $this->userOptions;
+       }
+
        /**
         * Get all user's options
         *
@@ -2918,8 +2903,8 @@
         */
        public function getOptions( $flags = 0 ) {
                global $wgHiddenPrefs;
-               $this->loadOptions();
-               $options = $this->mOptions;
+
+               $options = $this->getOptionsObject()->getAll();
 
                # We want 'disabled' preferences to always behave as the 
default value for
                # users, even if they have set the option explicitly in their 
settings (ie they
@@ -2976,8 +2961,6 @@
         * @param mixed $val New value to set
         */
        public function setOption( $oname, $val ) {
-               $this->loadOptions();
-
                // Explicitly NULL values should refer to defaults
                if ( is_null( $val ) ) {
                        $val = self::getDefaultOption( $oname );
@@ -3082,9 +3065,8 @@
         * @return array The key => kind mapping data
         */
        public function getOptionKinds( IContextSource $context, $options = 
null ) {
-               $this->loadOptions();
                if ( $options === null ) {
-                       $options = $this->mOptions;
+                       $options = $this->getOptionsObject()->getAll();
                }
 
                $prefs = Preferences::getPreferences( $this, $context );
@@ -5265,93 +5247,11 @@
        }
 
        /**
-        * Load the user options either from cache, the database or an array
-        *
-        * @param array $data Rows for the current user out of the 
user_properties table
-        */
-       protected function loadOptions( $data = null ) {
-               global $wgContLang;
-
-               $this->load();
-
-               if ( $this->mOptionsLoaded ) {
-                       return;
-               }
-
-               $this->mOptions = self::getDefaultOptions();
-
-               if ( !$this->getId() ) {
-                       // For unlogged-in users, load language/variant options 
from request.
-                       // There's no need to do it for logged-in users: they 
can set preferences,
-                       // and handling of page content is done by 
$pageLang->getPreferredVariant() and such,
-                       // so don't override user's choice (especially when the 
user chooses site default).
-                       $variant = $wgContLang->getDefaultVariant();
-                       $this->mOptions['variant'] = $variant;
-                       $this->mOptions['language'] = $variant;
-                       $this->mOptionsLoaded = true;
-                       return;
-               }
-
-               // Maybe load from the object
-               if ( !is_null( $this->mOptionOverrides ) ) {
-                       wfDebug( "User: loading options for user " . 
$this->getId() . " from override cache.\n" );
-                       foreach ( $this->mOptionOverrides as $key => $value ) {
-                               $this->mOptions[$key] = $value;
-                       }
-               } else {
-                       if ( !is_array( $data ) ) {
-                               wfDebug( "User: loading options for user " . 
$this->getId() . " from database.\n" );
-                               // Load from database
-                               $dbr = ( $this->queryFlagsUsed & 
self::READ_LATEST )
-                                       ? wfGetDB( DB_MASTER )
-                                       : wfGetDB( DB_REPLICA );
-
-                               $res = $dbr->select(
-                                       'user_properties',
-                                       [ 'up_property', 'up_value' ],
-                                       [ 'up_user' => $this->getId() ],
-                                       __METHOD__
-                               );
-
-                               $this->mOptionOverrides = [];
-                               $data = [];
-                               foreach ( $res as $row ) {
-                                       // Convert '0' to 0. PHP's boolean 
conversion considers them both
-                                       // false, but e.g. JavaScript considers 
the former as true.
-                                       // @todo: T54542 Somehow determine the 
desired type (string/int/bool)
-                                       //  and convert all values here.
-                                       if ( $row->up_value === '0' ) {
-                                               $row->up_value = 0;
-                                       }
-                                       $data[$row->up_property] = 
$row->up_value;
-                               }
-                       }
-
-                       // Convert the email blacklist from a new line 
delimited string
-                       // to an array of ids.
-                       if ( isset( $data['email-blacklist'] ) && 
$data['email-blacklist'] ) {
-                               $data['email-blacklist'] = array_map( 'intval', 
explode( "\n", $data['email-blacklist'] ) );
-                       }
-
-                       foreach ( $data as $property => $value ) {
-                               $this->mOptionOverrides[$property] = $value;
-                               $this->mOptions[$property] = $value;
-                       }
-               }
-
-               $this->mOptionsLoaded = true;
-
-               Hooks::run( 'UserLoadOptions', [ $this, &$this->mOptions ] );
-       }
-
-       /**
         * Saves the non-default options for this user, as previously set e.g. 
via
         * setOption(), in the database's "user_properties" (preferences) table.
         * Usually used via saveSettings().
         */
        protected function saveOptions() {
-               $this->loadOptions();
-
                // Not using getOptions(), to keep hidden preferences in 
database
                $saveOptions = $this->mOptions;
 
diff --git a/includes/user/UserOptions.php b/includes/user/UserOptions.php
new file mode 100644
index 0000000..376df27
--- /dev/null
+++ b/includes/user/UserOptions.php
@@ -0,0 +1,31 @@
+<?php
+
+class UserOptions extends ReadOnlyUserOptions {
+       protected $saveCallback;
+       protected $changed = false;
+
+       public function __construct( array $data, callback $saveCallback ) {
+               parent::__construct( $data );
+               $this->saveCallback = $saveCallback;
+       }
+
+       public function isChanged() {
+               return $this->changed;
+       }
+
+       public function set( $optionName, $value ) {
+               $this->changed = true;
+               $this->data[$optionName] = $value;
+       }
+
+       public function setMulti( array $values ) {
+               $this->changed = true;
+               $this->data = $values + $this->data;
+       }
+
+       public function save() {
+               $callback = $this->saveCallback;
+               $callback();
+               $this->changed = false;
+       }
+}
diff --git a/includes/user/UserOptionsProvider.php 
b/includes/user/UserOptionsProvider.php
new file mode 100644
index 0000000..83b2cbd
--- /dev/null
+++ b/includes/user/UserOptionsProvider.php
@@ -0,0 +1,113 @@
+<?php
+
+use Wikimedia\Rdbms\LoadBalancer;
+
+class UserOptionsProvider implements IDBAccessObject {
+       private $defaultOptions = [];
+       /** @var LoadBalancer */
+       private $loadBalancer;
+
+       public function __construct( LoadBalancer $loadBalancer ) {
+               $this->loadBalancer = $loadBalancer;
+       }
+
+       public function getUserOptions( User $user, $flags ) {
+               $data = $this->getOptionsInternal( $user, $flags );
+               /** @var UserOptions $options */
+               $options = null;
+               $options = new UserOptions( $data, function() use ( $user, 
&$options ) {
+                       $this->saveOptions( $user, $options );
+               } );
+
+               return $options;
+       }
+
+       public function getDefaultOptions() {
+               global $wgNamespacesToBeSearchedDefault, $wgDefaultUserOptions, 
$wgContLang, $wgDefaultSkin;
+
+               if ( $this->defaultOptions ) {
+                       return $this->defaultOptions;
+               }
+
+               $options = $wgDefaultUserOptions;
+               $options['language'] = $wgContLang->getCode();
+               foreach ( LanguageConverter::$languagesWithVariants as 
$langCode ) {
+                       $options[$langCode == $wgContLang->getCode() ? 
'variant' : "variant-$langCode"] = $langCode;
+               }
+
+               // NOTE: don't use SearchEngineConfig::getSearchableNamespaces 
here,
+               // since extensions may change the set of searchable namespaces 
depending
+               // on user groups/permissions.
+               foreach ( $wgNamespacesToBeSearchedDefault as $nsnum => $val ) {
+                       $options['searchNs' . $nsnum] = (bool)$val;
+               }
+               $options['skin'] = Skin::normalizeKey( $wgDefaultSkin );
+
+               Hooks::run( 'UserGetDefaultOptions', [ &$options ] );
+
+               $this->defaultOptions = new ReadOnlyUserOptions( $options );
+
+               return $this->defaultOptions;
+       }
+
+       private function saveOptions( User $user, UserOptions $options ) {
+
+       }
+
+       protected function getOptionsInternal( User $user, $flags ) {
+               global $wgContLang;
+
+               $options = $this->getDefaultOptions()->getAll();
+
+               if ( !$user->getId() ) {
+                       // For unlogged-in users, load language/variant options 
from request.
+                       // There's no need to do it for logged-in users: they 
can set preferences,
+                       // and handling of page content is done by 
$pageLang->getPreferredVariant() and such,
+                       // so don't override user's choice (especially when the 
user chooses site default).
+                       $variant = $wgContLang->getDefaultVariant();
+                       $options['variant'] = $variant;
+                       $options['language'] = $variant;
+                       return $options;
+               }
+
+               // Load from database
+               $dbType = ( $flags & self::READ_LATEST )
+                       ? DB_MASTER
+                       : DB_REPLICA;
+
+               $dbr = $this->loadBalancer->getConnection( $dbType );
+
+               $res = $dbr->select(
+                       'user_properties',
+                       [ 'up_property', 'up_value' ],
+                       [ 'up_user' => $user->getId() ],
+                       __METHOD__
+               );
+
+               $data = [];
+               foreach ( $res as $row ) {
+                       // Convert '0' to 0. PHP's boolean conversion considers 
them both
+                       // false, but e.g. JavaScript considers the former as 
true.
+                       // @todo: T54542 Somehow determine the desired type 
(string/int/bool)
+                       //  and convert all values here.
+                       if ( $row->up_value === '0' ) {
+                               $row->up_value = 0;
+                       }
+                       $data[$row->up_property] = $row->up_value;
+               }
+
+               // Convert the email blacklist from a new line delimited string
+               // to an array of ids.
+               if ( isset( $data['email-blacklist'] ) && 
$data['email-blacklist'] ) {
+                       $data['email-blacklist'] = array_map( 'intval', 
explode( "\n", $data['email-blacklist'] ) );
+               }
+
+               foreach ( $data as $property => $value ) {
+                       $options[$property] = $value;
+               }
+
+               Hooks::run( 'UserLoadOptions', [ $user, &$options ] );
+
+               return $options;
+       }
+}
diff --git a/tests/phpunit/includes/user/UserTest.php 
b/tests/phpunit/includes/user/UserTest.php
index 2721c18..869743c 100644
--- a/tests/phpunit/includes/user/UserTest.php
+++ b/tests/phpunit/includes/user/UserTest.php
@@ -363,6 +363,7 @@
         * T39963
         * Make sure defaults are loaded when setOption is called.
         * @covers User::loadOptions
+        * @fixme: coverage
         */
        public function testAnonOptions() {
                global $wgDefaultUserOptions;

-- 
To view, visit https://gerrit.wikimedia.org/r/391747
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: I2726eee9f6b18b1f922995f91f28a70d01586d74
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/core
Gerrit-Branch: master
Gerrit-Owner: MaxSem <[email protected]>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to