diff --git a/lib/classes/component.php b/lib/classes/component.php index e7f3eb4b324..1e63ce80bee 100644 --- a/lib/classes/component.php +++ b/lib/classes/component.php @@ -390,6 +390,7 @@ class core_component { $keyclasses = [ \core\exception\moodle_exception::class, \core\output\bootstrap_renderer::class, + \core\lang_string::class, ]; foreach ($keyclasses as $classname) { if (!array_key_exists($classname, $cache['classmap'])) { diff --git a/lib/classes/lang_string.php b/lib/classes/lang_string.php new file mode 100644 index 00000000000..5f09a9b5cb8 --- /dev/null +++ b/lib/classes/lang_string.php @@ -0,0 +1,280 @@ +. + +namespace core; + +use core\exception\coding_exception; +use stdClass; + +/** + * 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 + * aren'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 requires strings that may + * not be printed (after all classes get rendered 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 core + * @copyright 2011 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class lang_string { + + /** @var string The strings component. Default '' */ + protected ?string $component = ''; + + /** @var array|stdClass Any arguments required for the string. Default null */ + protected mixed $a = null; + + /** @var string The processed string (once processed) */ + protected ?string $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 bool $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|null $component The strings component + * @param mixed $a Any arguments the string requires + * @param string|null $lang The language to use when processing the string. + * @throws coding_exception + */ + public function __construct( + /** @var string The strings identifier */ + protected readonly string $identifier, + ?string $component = '', + mixed $a = null, + /** @var string The language to use when processing the string*/ + protected readonly ?string $lang = null, + ) { + if (empty($component)) { + $component = 'moodle'; + } + + $this->component = $component; + + // 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 ($a !== null) { + if (is_scalar($a)) { + $this->a = $a; + } else if ($a instanceof lang_string) { + $this->a = $a->out(); + } else if (is_object($a) || is_array($a)) { + $a = (array)$a; + $this->a = []; + foreach ($a as $key => $value) { + // Make sure conversion errors don't get displayed (results in ''). + if (is_array($value)) { + $this->a[$key] = ''; + } else if (is_object($value)) { + if (method_exists($value, '__toString')) { + $this->a[$key] = $value->__toString(); + } else { + $this->a[$key] = ''; + } + } else { + $this->a[$key] = (string)$value; + } + } + } + } + + if (debugging(false, DEBUG_DEVELOPER)) { + 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 check your string definition'); + } + if (!empty($this->component) && clean_param($this->component, PARAM_COMPONENT) == '') { + throw new coding_exception('Invalid string compontent. Please check your string definition'); + } + if (!get_string_manager()->string_exists($this->identifier, $this->component)) { + debugging('String does not exist. Please check your string definition for '.$this->identifier.'/'.$this->component, + DEBUG_DEVELOPER); + } + } + } + + /** + * 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 + * compatibility. + * + * @return string + * @throws coding_exception + */ + protected function get_string(): string { + global $CFG; + + // Check if we need to process the string. + if ($this->string === null) { + // Check the quality of the identifier. + if ($CFG->debugdeveloper && 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 check your string definition', DEBUG_DEVELOPER); + } + + // 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): string { + if ($lang !== null && $lang != $this->lang && ($this->lang == null && $lang != current_language())) { + if ($this->forcedstring) { + debugging('lang_string objects that have been used 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 + * + * @param array $array + * @return self + */ + public static function __set_state(array $array): self { + $tmp = new lang_string($array['identifier'], $array['component'], $array['a'], $array['lang']); + $tmp->string = $array['string']; + $tmp->forcedstring = $array['forcedstring']; + return $tmp; + } + + /** + * 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 array + */ + public function __sleep() { + $this->get_string(); + $this->forcedstring = true; + return ['forcedstring', 'string', 'lang']; + } + + /** + * Returns the identifier. + * + * @return string + */ + public function get_identifier(): string { + return $this->identifier; + } + + /** + * Returns the component. + * + * @return string + */ + public function get_component(): string { + return $this->component; + } +} + +// Alias this class to the old name. +// This file will be autoloaded by the legacyclasses autoload system. +// In future all uses of this class will be corrected and the legacy references will be removed. +class_alias(lang_string::class, \lang_string::class); diff --git a/lib/db/legacyclasses.php b/lib/db/legacyclasses.php index 75076d95c32..e1568603b03 100644 --- a/lib/db/legacyclasses.php +++ b/lib/db/legacyclasses.php @@ -27,10 +27,15 @@ defined('MOODLE_INTERNAL') || die(); // Like other files in the db directory this file uses an array. -// The old class name is the key, the path to the file containing the class is the vlaue. +// The old class name is the key, the path to the file containing the class is the value. // The array must be called $legacyclasses. $legacyclasses = [ \bootstrap_renderer::class => 'output/bootstrap_renderer.php', + + // Core API. + \lang_string::class => 'lang_string.php', + + // Exception API. \coding_exception::class => 'exception/coding_exception.php', \file_serving_exception::class => 'exception/file_serving_exception.php', \invalid_dataroot_permissions::class => 'exception/invalid_dataroot_permissions.php', diff --git a/lib/moodlelib.php b/lib/moodlelib.php index a68bb235f0f..d3c5d020fe0 100644 --- a/lib/moodlelib.php +++ b/lib/moodlelib.php @@ -10183,256 +10183,6 @@ function unserialize_object(string $input): stdClass { return (object) $instance; } -/** - * 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 core - * @category string - * @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|mixed $a Any arguments the string requires - * @param string $lang The language to use when processing the string. - * @throws coding_exception - */ - public function __construct($identifier, $component = '', $a = null, $lang = null) { - if (empty($component)) { - $component = 'moodle'; - } - - $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 ($a !== null) { - if (is_scalar($a)) { - $this->a = $a; - } else if ($a instanceof lang_string) { - $this->a = $a->out(); - } else if (is_object($a) or is_array($a)) { - $a = (array)$a; - $this->a = array(); - foreach ($a as $key => $value) { - // Make sure conversion errors don't get displayed (results in ''). - if (is_array($value)) { - $this->a[$key] = ''; - } else if (is_object($value)) { - if (method_exists($value, '__toString')) { - $this->a[$key] = $value->__toString(); - } else { - $this->a[$key] = ''; - } - } else { - $this->a[$key] = (string)$value; - } - } - } - } - - if (debugging(false, DEBUG_DEVELOPER)) { - 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 check your string definition'); - } - if (!empty($this->component) && clean_param($this->component, PARAM_COMPONENT) == '') { - throw new coding_exception('Invalid string compontent. Please check your string definition'); - } - if (!get_string_manager()->string_exists($this->identifier, $this->component)) { - debugging('String does not exist. Please check your string definition for '.$this->identifier.'/'.$this->component, DEBUG_DEVELOPER); - } - } - } - - /** - * 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 - * compatibility. - * - * @return string - * @throws coding_exception - */ - 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 ($CFG->debugdeveloper && 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 check your string definition', DEBUG_DEVELOPER); - } - - // 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 used 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 - * - * @param array $array - * @return self - */ - public static function __set_state(array $array): self { - $tmp = new lang_string($array['identifier'], $array['component'], $array['a'], $array['lang']); - $tmp->string = $array['string']; - $tmp->forcedstring = $array['forcedstring']; - return $tmp; - } - - /** - * 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 array - */ - public function __sleep() { - $this->get_string(); - $this->forcedstring = true; - return array('forcedstring', 'string', 'lang'); - } - - /** - * Returns the identifier. - * - * @return string - */ - public function get_identifier() { - return $this->identifier; - } - - /** - * Returns the component. - * - * @return string - */ - public function get_component() { - return $this->component; - } -} - /** * Get human readable name describing the given callable. * diff --git a/lib/tests/moodlelib_test.php b/lib/tests/moodlelib_test.php index fd9a80cb3e6..6a3ce1cbeae 100644 --- a/lib/tests/moodlelib_test.php +++ b/lib/tests/moodlelib_test.php @@ -16,8 +16,6 @@ namespace core; -use lang_string; - /** * Unit tests for (some of) ../moodlelib.php. * @@ -2115,13 +2113,13 @@ class moodlelib_test extends \advanced_testcase { $leadingbackslash = (version_compare(PHP_VERSION, '8.2.0', '>=')) ? '\\' : ''; $expected1 = << 'no', +{$leadingbackslash}core\lang_string::__set_state(array( 'component' => 'moodle', 'a' => NULL, - 'lang' => NULL, 'string' => NULL, 'forcedstring' => false, + 'identifier' => 'no', + 'lang' => NULL, )) EOF; @@ -4656,7 +4654,7 @@ EOT; ], 'method_of_object' => [ [new lang_string('parentlanguage', 'core_langconfig'), 'my_foobar_method'], - 'lang_string::my_foobar_method', + 'core\lang_string::my_foobar_method', ], 'function_as_literal' => [ 'my_foobar_callback',