MDL-26155 string: Implemented moodle_string class
authorSam Hemelryk <sam@moodle.com>
Sun, 4 Dec 2011 23:07:54 +0000 (12:07 +1300)
committerSam Hemelryk <sam@moodle.com>
Mon, 2 Jan 2012 20:04:58 +0000 (09:04 +1300)
lib/moodlelib.php
lib/simpletest/testmoodlelib.php

index 61b4b44..81cbc0e 100644 (file)
@@ -6321,7 +6321,8 @@ class core_string_manager implements string_manager {
         $string = $string[$identifier];
 
         if ($a !== NULL) {
-            if (is_object($a) or is_array($a)) {
+            // Process array's and objects (except lang_strings)
+            if (is_array($a) or (is_object($a) && !($a instanceof lang_string))) {
                 $a = (array)$a;
                 $search = array();
                 $replace = array();
@@ -6330,8 +6331,8 @@ class core_string_manager implements string_manager {
                         // we do not support numeric keys - sorry!
                         continue;
                     }
-                    if (is_object($value) or is_array($value)) {
-                        // we support just string as value
+                    if (is_array($value) or (is_object($value) && !($value instanceof lang_string))) {
+                        // we support just string or lang_string as value
                         continue;
                     }
                     $search[]  = '{$a->'.$key.'}';
@@ -6859,6 +6860,20 @@ class install_string_manager implements string_manager {
  * As a last resort, should the identifier fail to map to a string
  * the returned string will be [[ $identifier ]]
  *
+ * In Moodle 2.3 there is a new argument to this function $lazyload.
+ * Setting $lazyload to true causes get_string to return a lang_string object
+ * rather than the string itself. The fetching of the string is then put off until
+ * the string object is first used. The object can be used by calling it's out
+ * method or by casting the object to a string, either directly e.g.
+ *     (string)$stringobject
+ * or indirectly by using the string within another string or echoing it out e.g.
+ *     echo $stringobject
+ *     return "<p>{$stringobject}</p>";
+ * It is worth noting that using $lazyload and attempting to use the string as an
+ * array key will cause a fatal error as objects cannot be used as array keys.
+ * But you should never do that anyway!
+ * For more information {@see lang_string}
+ *
  * @param string $identifier The key identifier for the localized string
  * @param string $component The module where the key identifier is stored,
  *      usually expressed as the filename in the language pack without the
@@ -6866,17 +6881,29 @@ class install_string_manager implements string_manager {
  *      If none is specified then moodle.php is used.
  * @param string|object|array $a An object, string or number that can be used
  *      within translation strings
+ * @param bool $lazyload If set to true a string object is returned instead of
+ *      the string itself. The string then isn't calculated until it is first used.
  * @return string The localized string.
  */
-function get_string($identifier, $component = '', $a = NULL) {
+function get_string($identifier, $component = '', $a = NULL, $lazyload = false) {
     global $CFG;
 
+    // If the lazy load argument has been supplied return a lang_string object
+    // instead.
+    // We need to make sure it is true (and a bool) as you will see below there
+    // used to be a forth argument at one point.
+    if ($lazyload === true) {
+        return new lang_string($identifier, $component, $a);
+    }
+
     $identifier = clean_param($identifier, PARAM_STRINGID);
     if (empty($identifier)) {
         throw new coding_exception('Invalid string identifier. Most probably some illegal character is part of the string identifier. Please fix your get_string() call and string definition');
     }
 
-    if (func_num_args() > 3) {
+    // There is now a forth argument again, this time it is a boolean however so
+    // we can still check for the old extralocations parameter.
+    if (!is_bool($lazyload) && !empty($lazyload)) {
         debugging('extralocations parameter in get_string() is not supported any more, please use standard lang locations only.');
     }
 
@@ -10432,3 +10459,208 @@ function get_home_page() {
     }
     return HOMEPAGE_SITE;
 }
+
+/**
+ * The lang_string class
+ *
+ * This special class is used to create an object representation of a string request.
+ * It is special because processing doesn't occur until the object is first used.
+ * The class was created especially to aid performance in areas where strings were
+ * required to be generated but were not necessarily used.
+ * As an example the admin tree when generated uses over 1500 strings, of which
+ * normally only 1/3 are ever actually printed at any time.
+ * The performance advantage is achieved by not actually processing strings that
+ * arn't being used, as such reducing the processing required for the page.
+ *
+ * How to use the lang_string class?
+ *     There are two methods of using the lang_string class, first through the
+ *     forth argument of the get_string function, and secondly directly.
+ *     The following are examples of both.
+ * 1. Through get_string calls e.g.
+ *     $string = get_string($identifier, $component, $a, true);
+ *     $string = get_string('yes', 'moodle', null, true);
+ * 2. Direct instantiation
+ *     $string = new lang_string($identifier, $component, $a, $lang);
+ *     $string = new lang_string('yes');
+ *
+ * How do I use a lang_string object?
+ *     The lang_string object makes use of a magic __toString method so that you
+ *     are able to use the object exactly as you would use a string in most cases.
+ *     This means you are able to collect it into a variable and then directly
+ *     echo it, or concatenate it into another string, or similar.
+ *     The other thing you can do is manually get the string by calling the
+ *     lang_strings out method e.g.
+ *         $string = new lang_string('yes');
+ *         $string->out();
+ *     Also worth noting is that the out method can take one argument, $lang which
+ *     allows the developer to change the language on the fly.
+ *
+ * When should I use a lang_string object?
+ *     The lang_string object is designed to be used in any situation where a
+ *     string may not be needed, but needs to be generated.
+ *     The admin tree is a good example of where lang_string objects should be
+ *     used.
+ *     A more practical example would be any class that requries strings that may
+ *     not be printed (after all classes get renderer by renderers and who knows
+ *     what they will do ;))
+ *
+ * When should I not use a lang_string object?
+ *     Don't use lang_strings when you are going to use a string immediately.
+ *     There is no need as it will be processed immediately and there will be no
+ *     advantage, and in fact perhaps a negative hit as a class has to be
+ *     instantiated for a lang_string object, however get_string won't require
+ *     that.
+ *
+ * Limitations:
+ * 1. You cannot use a lang_string object as an array offset. Doing so will
+ *     result in PHP throwing an error. (You can use it as an object property!)
+ *
+ * @package    moodlecore
+ *
+ * @copyright  2011 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class lang_string {
+
+    /** @var string The strings identifier */
+    protected $identifier;
+    /** @var string The strings component. Default '' */
+    protected $component = '';
+    /** @var array|stdClass Any arguments required for the string. Default null */
+    protected $a = null;
+    /** @var string The language to use when processing the string. Default null */
+    protected $lang = null;
+
+    /** @var string The processed string (once processed) */
+    protected $string = null;
+
+    /**
+     * A special boolean. If set to true then the object has been woken up and
+     * cannot be regenerated. If this is set then $this->string MUST be used.
+     * @var bool
+     */
+    protected $forcedstring = false;
+
+    /**
+     * Constructs a lang_string object
+     *
+     * This function should do as little processing as possible to ensure the best
+     * performance for strings that won't be used.
+     *
+     * @param string $identifier The strings identifier
+     * @param string $component The strings component
+     * @param stdClass|array $a Any arguments the string requires
+     * @param string $lang The language to use when processing the string.
+     */
+    public function __construct($identifier, $component = '', $a = null, $lang = null) {
+        $this->identifier = $identifier;
+        $this->component = $component;
+        $this->lang = $lang;
+
+        // We MUST duplicate $a to ensure that it if it changes by reference those
+        // changes are not carried across.
+        // To do this we always ensure $a or its properties/values are strings
+        // and that any properties/values that arn't convertable are forgotten.
+        if (!empty($a)) {
+            if (is_scalar($a)) {
+                $this->a = $a;
+            } else if ($a instanceof lang_string) {
+                $this->a = $a->out();
+            } else if (is_object($a)) {
+                $this->a = new stdClass;
+                foreach (get_object_vars($a) as $key => $value) {
+                    // Make sure conversion errors don't get displayed (results in '')
+                    $this->a->$key = @(string)$value;
+                }
+            } else if (is_array($a)) {
+                $this->a = array();
+                foreach ($a as $key => $value) {
+                    // Make sure conversion errors don't get displayed (results in '')
+                    $this->a[$key] = @(string)$value;
+                }
+            }
+        }
+    }
+
+    /**
+     * Processes the string.
+     *
+     * This function actually processes the string, stores it in the string property
+     * and then returns it.
+     * You will notice that this function is VERY similar to the get_string method.
+     * That is because it is pretty much doing the same thing.
+     * However as this function is an upgrade it isn't as tolerant to backwards
+     * compatability.
+     *
+     * @return string
+     */
+    protected function get_string() {
+        global $CFG;
+
+        // Check if we need to process the string
+        if ($this->string === null) {
+            // Check the quality of the identifier.
+            if (clean_param($this->identifier, PARAM_STRINGID) == '') {
+                throw new coding_exception('Invalid string identifier. Most probably some illegal character is part of the string identifier. Please fix your get_string() call and string definition');
+            }
+
+            // Process the string
+            $this->string = get_string_manager()->get_string($this->identifier, $this->component, $this->a, $this->lang);
+            // Debugging feature lets you display string identifier and component
+            if (isset($CFG->debugstringids) && $CFG->debugstringids && optional_param('strings', 0, PARAM_INT)) {
+                $this->string .= ' {' . $this->identifier . '/' . $this->component . '}';
+            }
+        }
+        // Return the string
+        return $this->string;
+    }
+
+    /**
+     * Returns the string
+     *
+     * @param string $lang The langauge to use when processing the string
+     * @return string
+     */
+    public function out($lang = null) {
+        if ($lang !== null && $lang != $this->lang && ($this->lang == null && $lang != current_language())) {
+            if ($this->forcedstring) {
+                debugging('lang_string objects that have been serialised and unserialised cannot be printed in another language. ('.$this->lang.' used)', DEBUG_DEVELOPER);
+                return $this->get_string();
+            }
+            $translatedstring = new lang_string($this->identifier, $this->component, $this->a, $lang);
+            return $translatedstring->out();
+        }
+        return $this->get_string();
+    }
+
+    /**
+     * Magic __toString method for printing a string
+     *
+     * @return string
+     */
+    public function __toString() {
+        return $this->get_string();
+    }
+
+    /**
+     * Magic __set_state method used for var_export
+     *
+     * @return string
+     */
+    public function __set_state() {
+        return $this->get_string();
+    }
+
+    /**
+     * Prepares the lang_string for sleep and stores only the forcedstring and
+     * string properties... the string cannot be regenerated so we need to ensure
+     * it is generated for this.
+     *
+     * @return string
+     */
+    public function __sleep() {
+        $this->get_string();
+        $this->forcedstring = true;
+        return array('forcedstring', 'string', 'lang');
+    }
+}
\ No newline at end of file
index 066624e..f620ec5 100644 (file)
@@ -1602,4 +1602,150 @@ class moodlelib_test extends UnitTestCase {
         date_default_timezone_set($systemdefaulttimezone);
         setlocale(LC_TIME, $oldlocale);
     }
-}
+
+    /**
+     * Test get_string and most importantly the implementation of the lang_string
+     * object.
+     */
+    public function test_get_string() {
+        global $COURSE;
+
+        // Make sure we are using English
+        $originallang = $COURSE->lang;
+        $COURSE->lang = 'en';
+
+        $yes = get_string('yes');
+        $yesexpected = 'Yes';
+        $this->assertIsA($yes, 'string');
+        $this->assertEqual($yes, $yesexpected);
+
+        $yes = get_string('yes', 'moodle');
+        $this->assertIsA($yes, 'string');
+        $this->assertEqual($yes, $yesexpected);
+
+        $yes = get_string('yes', 'core');
+        $this->assertIsA($yes, 'string');
+        $this->assertEqual($yes, $yesexpected);
+
+        $yes = get_string('yes', '');
+        $this->assertIsA($yes, 'string');
+        $this->assertEqual($yes, $yesexpected);
+
+        $yes = get_string('yes', null);
+        $this->assertIsA($yes, 'string');
+        $this->assertEqual($yes, $yesexpected);
+
+        $yes = get_string('yes', null, 1);
+        $this->assertIsA($yes, 'string');
+        $this->assertEqual($yes, $yesexpected);
+
+        $days = 1;
+        $numdays = get_string('numdays', 'core', '1');
+        $numdaysexpected = $days.' days';
+        $this->assertIsA($numdays, 'string');
+        $this->assertEqual($numdays, $numdaysexpected);
+
+        $yes = get_string('yes', null, null, true);
+        $this->assertEqual(get_class($yes), 'lang_string');
+        $this->assertIsA($yes, 'lang_string');
+        $this->assertEqual((string)$yes, $yesexpected);
+
+        // Test using a lang_string object as the $a argument for a normal
+        // get_string call (returning string)
+        $test = new lang_string('yes', null, null, true);
+        $testexpected = get_string('numdays', 'core', get_string('yes'));
+        $testresult = get_string('numdays', null, $test);
+        $this->assertIsA($testresult, 'string');
+        $this->assertEqual($testresult, $testexpected);
+
+        // Test using a lang_string object as the $a argument for an object
+        // get_string call (returning lang_string)
+        $test = new lang_string('yes', null, null, true);
+        $testexpected = get_string('numdays', 'core', get_string('yes'));
+        $testresult = get_string('numdays', null, $test, true);
+        $this->assertEqual(get_class($testresult), 'lang_string');
+        $this->assertIsA($testresult, 'lang_string');
+        $this->assertEqual("$testresult", $testexpected);
+
+        // Make sure that object properties that can't be converted don't cause
+        // errors
+        // Level one: This is as deep as current language processing goes
+        $test = new stdClass;
+        $test->one = 'here';
+        $string = get_string('yes', null, $test, true);
+        $this->assertEqual($string, $yesexpected);
+
+        // Make sure that object properties that can't be converted don't cause
+        // errors.
+        // Level two: Language processing doesn't currently reach this deep.
+        // only immediate scalar properties are worked with.
+        $test = new stdClass;
+        $test->one = new stdClass;
+        $test->one->two = 'here';
+        $string = get_string('yes', null, $test, true);
+        $this->assertEqual($string, $yesexpected);
+
+        // Make sure that object properties that can't be converted don't cause
+        // errors.
+        // Level three: It should never ever go this deep, but we're making sure
+        // it doesn't cause any probs anyway.
+        $test = new stdClass;
+        $test->one = new stdClass;
+        $test->one->two = new stdClass;
+        $test->one->two->three = 'here';
+        $string = get_string('yes', null, $test, true);
+        $this->assertEqual($string, $yesexpected);
+
+        // Make sure that object properties that can't be converted don't cause
+        // errors and check lang_string properties.
+        // Level one: This is as deep as current language processing goes
+        $test = new stdClass;
+        $test->one = new lang_string('yes');
+        $string = get_string('yes', null, $test, true);
+        $this->assertEqual($string, $yesexpected);
+
+        // Make sure that object properties that can't be converted don't cause
+        // errors and check lang_string properties.
+        // Level two: Language processing doesn't currently reach this deep.
+        // only immediate scalar properties are worked with.
+        $test = new stdClass;
+        $test->one = new stdClass;
+        $test->one->two = new lang_string('yes');
+        $string = get_string('yes', null, $test, true);
+        $this->assertEqual($string, $yesexpected);
+
+        // Make sure that object properties that can't be converted don't cause
+        // errors and check lang_string properties.
+        // Level three: It should never ever go this deep, but we're making sure
+        // it doesn't cause any probs anyway.
+        $test = new stdClass;
+        $test->one = new stdClass;
+        $test->one->two = new stdClass;
+        $test->one->two->three = new lang_string('yes');
+        $string = get_string('yes', null, $test, true);
+        $this->assertEqual($string, $yesexpected);
+
+        // Make sure that array properties that can't be converted don't cause
+        // errors
+        $test = array();
+        $test['one'] = new stdClass;
+        $test['one']->two = 'here';
+        $string = get_string('yes', null, $test, true);
+        $this->assertEqual($string, $yesexpected);
+
+        // This is one of the limitations to the lang_string class. It can't be
+        // used as a key
+        $this->expectError('Illegal offset type', 'Array offsets now support objects we can consider making lazy loading the default!.');
+        $array = array(get_string('yes', null, null, true) => 'yes');
+
+        // Same thing but as above except using an object... this is allowed :P
+        $string = get_string('yes', null, null, true);
+        $object = new stdClass;
+        $object->$string = 'Yes';
+        $this->assertEqual($string, $yesexpected);
+        $this->assertEqual($object->$string, $yesexpected);
+
+        // Reset the language
+        $COURSE->lang = $originallang;
+    }
+}
\ No newline at end of file