diff --git a/lib/datalib.php b/lib/datalib.php
index afb18bb07ff..20a2f66f0f6 100644
--- a/lib/datalib.php
+++ b/lib/datalib.php
@@ -1663,29 +1663,334 @@ function user_accesstime_log($courseid=0) {
/// GENERAL HELPFUL THINGS ///////////////////////////////////
/**
- * Dumps a given object's information for debugging purposes
+ * Dumps a given object's information for debugging purposes. (You can actually use this function
+ * to print any type of value such as arrays or simple strings, not just objects.)
*
- * When used in a CLI script, the object's information is written to the standard
- * error output stream. When used in a web script, the object is dumped to a
- * pre-formatted block with the "notifytiny" CSS class.
+ * When used in a web script, the object is dumped in a fancy-formatted div.
*
- * @param mixed $object The data to be printed
- * @return void output is echo'd
+ * When used in a CLI script, the object's information is written to the standard error output
+ * stream.
+ *
+ * When used in an AJAX script, the object's information is dumped to the server error log.
+ *
+ * In text mode, private fields are shown with * and protected with +.
+ *
+ * In web view, formatting is done with Bootstrap classes. You can hover over some items to see
+ * more information, such as value types or access controls, or full field names if the names get
+ * cut off.
+ *
+ * By default, this will recurse to child objects, except where that would result in infinite
+ * recursion. To change that, set $expandclasses to an empty array (= do not recurse) or to a list
+ * of the class names that you would like to expand. You can also set values in this array to a
+ * regular expression beginning with / if you want to match a range of classes.
+ *
+ * @param mixed $item Object, array, or other item to display
+ * @param string[] $expandclasses Optional list of class patterns to recurse to
+ * @param bool $textonly If true, outputs text-only (automatically set for CLI and AJAX)
+ * @param bool $return For internal use - if true, returns value instead of echoing it
+ * @param int $depth For internal use - depth of recursion within print_object call
+ * @param \stdClass[] $done For internal use - array listing already-printed objects
+ * @return string HTML code (or text if CLI) to display, if $return is true, otherwise empty string
*/
-function print_object($object) {
-
- // we may need a lot of memory here
+function print_object($item, array $expandclasses = ['/./'], bool $textonly = false, bool $return = false,
+ int $depth = 0, array $done = []): string {
+ // We may need a lot of memory here.
raise_memory_limit(MEMORY_EXTRA);
- if (CLI_SCRIPT) {
- fwrite(STDERR, print_r($object, true));
- fwrite(STDERR, PHP_EOL);
- } else if (AJAX_SCRIPT) {
- foreach (explode("\n", print_r($object, true)) as $line) {
- error_log($line);
+ // Set text (instead of HTML) mode if in CLI or AJAX script.
+ if (CLI_SCRIPT || AJAX_SCRIPT) {
+ $textonly = true;
+ }
+
+ /**
+ * Gets styling for types of variable.
+ *
+ * @param mixed $item Arbitrary PHP variable (simple primitive type) to display
+ * @return string Bootstrap class for styling the display
+ */
+ $gettypestyle = function($item): string {
+ switch (gettype($item)) {
+ case 'NULL':
+ case 'boolean':
+ return 'font-italic';
+ case 'integer':
+ case 'double':
+ return 'text-primary';
+ case 'string' :
+ return 'text-success';
+ default:
+ return '';
+ }
+ };
+
+ /**
+ * Formats and escapes the text for the contents of a variable.
+ *
+ * @param mixed $item Arbitrary PHP variable (simple primitive type) to display
+ * @return string Contents as text
+ */
+ $getobjectstr = function($item) use($textonly): string {
+ if (is_null($item)) {
+ return 'null';
+ }
+ $objectstr = (string)$item;
+ if (is_string($item)) {
+ // Quotes around strings.
+ $objectstr = "'$objectstr'";
+ } else if (is_bool($item)) {
+ // Show true or false for bools.
+ $objectstr = $item ? 'true' : 'false';
+ } else if (is_float($item)) {
+ // Add 'f' for floats.
+ $objectstr = $item . 'f';
+ }
+ if ($textonly) {
+ return $objectstr;
+ } else {
+ return s($objectstr);
+ }
+ };
+
+ if ($textonly) {
+ $out = '';
+ } else {
+ $notype = false;
+ $cssclass = $gettypestyle($item);
+ if (is_object($item) || is_array($item)) {
+ // For object and array, don't show the title on hover - it makes no sense because
+ // they're big, plus we already show the word 'array' or the object type.
+ $notype = true;
+ // Add a fancy box, with alternating colour, around the object and non-empty array.
+ if (is_object($item) || count($item) > 0) {
+ if (($depth & 1) === 0) {
+ $cssclass .= ' bg-white rounded p-2';
+ } else {
+ $cssclass .= ' bg-light rounded p-2';
+ }
+ }
+ }
+ if ($depth === 0) {
+ // The top-level object being printed has print-object class in case anyone wants to
+ // do extra styling.
+ $cssclass .= ' print-object';
+ }
+ $attributes = [];
+ if (!$notype) {
+ // We show the item type on hover. Note there is no need to include the actual value
+ // in the title attribute here, because the full text will be displayed anyway with
+ // wrapping if needed..
+ $attributes['title'] = gettype($item);
+ }
+ $out = html_writer::start_div($cssclass, $attributes);
+ }
+
+ // Depending on the level of nesting, we allocate a slightly different proportion (ranging
+ // from 2/12 to 5/12) of the available width for the key names.
+ $bsdepth = floor(min(6, $depth) / 2);
+ $bootstrapdt = 'col-sm-' . ($bsdepth + 2);
+ $bootstrapdd = 'col-sm-' . (12 - ($bsdepth + 2));
+
+ // This main code handles objects and arrays.
+ if (is_array($item) || is_object($item)) {
+ if (is_object($item)) {
+ // Object header: class name.
+ if ($textonly) {
+ $out .= '[' . get_class($item) . ']';
+ } else {
+ // Objects display the class name as a badge. Content goes within a
.
+ $badge = html_writer::span(get_class($item), 'badge badge-primary');
+ $out .= html_writer::tag('h5', $badge);
+ $out .= html_writer::start_tag('dl', ['class' => 'row']);
+ $dl = true;
+ }
+ // Record that we have output this object already (to prevent circular refs).
+ $done[] = $item;
+ $object = true;
+ // Cast to array so we can loop through all properties.
+ $item = (array)$item;
+ } else {
+ // Array header: 'array' and a count.
+ $arrayinfo = 'array (' . count($item) . ')';
+ if ($textonly) {
+ $out .= $arrayinfo;
+ } else {
+ // Arrays show the same as objects but the badge is grey.
+ $badge = html_writer::span($arrayinfo, 'badge badge-secondary');
+ // Decide if there will be a tag - only if there is some content.
+ $dl = count($item) > 0;
+ $attributes = [];
+ if (!$dl) {
+ // When there is no content inside the array, don't show bottom margin on heading.
+ $attributes['class'] = 'mb-0';
+ }
+ $out .= html_writer::tag('h5', $badge, $attributes);
+ if ($dl) {
+ $out .= html_writer::start_tag('dl', ['class' => 'row']);
+ }
+ }
+ $object = false;
+ }
+
+ // Properties.
+ foreach ($item as $key => $value) {
+ // Detect private and protected variables.
+ $matches = [];
+ $stringkey = (string)$key;
+ if (preg_match('~^\x00(.*)\x00(.*)$~', $stringkey, $matches)) {
+ $shortkey = $matches[2];
+ $access = $matches[1] == '*' ? 'protected' : 'private';
+ } else {
+ $shortkey = $stringkey;
+ $access = 'public';
+ }
+ if ($textonly) {
+ switch ($access) {
+ case 'protected' :
+ $shortkey = '+' . $shortkey;
+ break;
+ case 'private' :
+ $shortkey = '*' . $shortkey;
+ break;
+ }
+ $out .= PHP_EOL . ' ' . $shortkey . ' = ';
+ } else {
+ switch ($access) {
+ case 'protected':
+ // Protected is in normal font.
+ $bootstrapstyle = ' font-weight-normal';
+ break;
+ case 'private':
+ // Private is italic.
+ $bootstrapstyle = ' font-weight-normal font-italic';
+ break;
+ default:
+ // Public is bold, same for array keys.
+ $bootstrapstyle = '';
+ break;
+ }
+ $attributes = ['class' => $bootstrapdt . ' text-truncate' . $bootstrapstyle];
+ if ($object) {
+ // For an object property, the title is the full text of the key (in case it
+ // gets cut off) and the access modifier.
+ $attributes['title'] = s($shortkey) . ' (' . $access . ')';
+ $objectstr = s($shortkey);
+ } else {
+ // For an array key, the title is the full text of the key (in case it gets
+ // cut off) and the type of the key. Array keys can't have an access modifier.
+ $attributes['title'] = s($shortkey) . ' (' . gettype($key) . ')';
+ // Array keys are styled according to the normal styling for that type.
+ $typestyle = $gettypestyle($key);
+ if ($typestyle) {
+ $attributes['class'] .= ' ' . $typestyle;
+ }
+ // Array keys also use a special object string e.g. 'true' for bool, quoted.
+ $objectstr = $getobjectstr($key);
+ }
+ $out .= html_writer::tag('dt', $objectstr, $attributes);
+ }
+ // Consider how to display the value for this key.
+ $extraclass = '';
+ switch (gettype($value)) {
+ case 'object' :
+ $objclass = get_class($value);
+
+ // See if we printed it further up the tree in which case
+ // it will definitely not be printed (infinite recursion).
+ if (in_array($value, $done)) {
+ if ($textonly) {
+ $display = '[circular reference: ' . $objclass . ']';
+ } else {
+ $display = '[circular reference: ' . s($objclass) . ']';
+ $extraclass = ' text-danger';
+ }
+ break;
+ }
+
+ // Recurse only to specified types.
+ $recurse = false;
+ foreach ($expandclasses as $pattern) {
+ if (substr($pattern, 0, 1) === '/') {
+ // Allow regular expressions beginning with a / symbol.
+ if (preg_match($pattern, $objclass)) {
+ $recurse = true;
+ break;
+ }
+ } else {
+ // Other strings must be exact match.
+ if ($objclass === $pattern) {
+ $recurse = true;
+ break;
+ }
+ }
+ }
+
+ if ($recurse) {
+ // Recursively display the object.
+ $display = print_object($value, $expandclasses, $textonly, true, $depth + 1, $done);
+ if ($textonly) {
+ // Indent by adding spaces after each LF.
+ $display = str_replace(PHP_EOL, PHP_EOL . ' ', $display);
+ }
+ } else {
+ // Do not display the object, just a marker in square breackets.
+ if ($textonly) {
+ $display = '[object: ' . $objclass . ']';
+ } else {
+ $display = '[object: ' . s($objclass) . ']';
+ }
+ }
+ break;
+
+ case 'array' :
+ // Recursively display the array.
+ $display = print_object($value, $expandclasses, $textonly, true, $depth + 1, $done);
+ if ($textonly) {
+ // Indent by adding spaces after each LF.
+ $display = str_replace(PHP_EOL, PHP_EOL . ' ', $display);
+ }
+ break;
+
+ default:
+ // Plain value - recurse to display.
+ $display = print_object($value, [], $textonly, true, $depth + 1);
+ break;
+ }
+ if ($textonly) {
+ $out .= $display;
+ } else {
+ $out .= html_writer::tag('dd', $display, ['class' => $bootstrapdd . $extraclass]);
+ }
+ }
+ if (!$textonly && $dl) {
+ $out .= html_writer::end_tag('dl');
}
} else {
- echo html_writer::tag('pre', s(print_r($object, true)), array('class' => 'notifytiny'));
+ // For things which are not objects or arrays, just convert to string for display.
+ $out .= $getobjectstr($item);
+ }
+
+ if (!$textonly) {
+ $out .= html_writer::end_div();
+ }
+
+ // Display or return result.
+ if ($return) {
+ return $out;
+ } else {
+ if (CLI_SCRIPT) {
+ fwrite(STDERR, $out);
+ fwrite(STDERR, PHP_EOL);
+ } else if (AJAX_SCRIPT) {
+ foreach (explode(PHP_EOL, $out) as $line) {
+ error_log($line);
+ }
+ } else {
+ if ($textonly) {
+ $out = html_writer::tag('pre', s($out));
+ }
+ echo $out . "\n";
+ }
+ return '';
}
}
diff --git a/lib/upgrade.txt b/lib/upgrade.txt
index 9bc46bed14e..aaf06976fee 100644
--- a/lib/upgrade.txt
+++ b/lib/upgrade.txt
@@ -66,6 +66,8 @@ information provided here is intended especially for developers.
that need to perform processing when a question is deleted should observe the \core\event\question_deleted event instead.
* The external function core_grades_get_groups_for_selector is now relocated.
Please use it at core_group_get_groups_for_selector instead.
+* The print_object function (for temporary debugging use only) can now be used on objects that have recursive references,
+ and has an improved display and other changes. (New parameters are available; see function documentation.)
=== 4.2 ===