From 860f59b13cf6618cf502820846dbae09db297a6e Mon Sep 17 00:00:00 2001 From: Mike Churchward Date: Tue, 5 Sep 2017 16:36:01 -0400 Subject: [PATCH 1/2] MDL-60029 core_user: Added new api function to reduce db calls. --- lib/myprofilelib.php | 23 +- user/lib.php | 43 ++-- user/profile/field/checkbox/field.class.php | 33 --- user/profile/field/menu/field.class.php | 9 +- user/profile/lib.php | 239 ++++++++++++-------- 5 files changed, 185 insertions(+), 162 deletions(-) diff --git a/lib/myprofilelib.php b/lib/myprofilelib.php index 54ea27c5cfc..2a95a140fbf 100644 --- a/lib/myprofilelib.php +++ b/lib/myprofilelib.php @@ -367,19 +367,16 @@ function core_myprofile_navigation(core_user\output\myprofile\tree $tree, $user, $tree->add_node($node); } - if ($categories = $DB->get_records('user_info_category', null, 'sortorder ASC')) { - foreach ($categories as $category) { - if ($fields = $DB->get_records('user_info_field', array('categoryid' => $category->id), 'sortorder ASC')) { - foreach ($fields as $field) { - require_once($CFG->dirroot.'/user/profile/field/'.$field->datatype.'/field.class.php'); - $newfield = 'profile_field_'.$field->datatype; - $formfield = new $newfield($field->id, $user->id); - if ($formfield->is_visible() and !$formfield->is_empty()) { - $node = new core_user\output\myprofile\node('contact', 'custom_field_' . $formfield->field->shortname, - format_string($formfield->field->name), null, null, $formfield->display_data()); - $tree->add_node($node); - } - } + $categories = profile_get_user_fields_with_data_by_category($user->id); + foreach ($categories as $categoryid => $fields) { + foreach ($fields as $field) { + require_once($CFG->dirroot.'/user/profile/field/'.$field->datatype.'/field.class.php'); + $newfield = 'profile_field_'.$field->datatype; + $formfield = new $newfield($field->id, $user->id, $field); + if ($formfield->is_visible() and !$formfield->is_empty()) { + $node = new core_user\output\myprofile\node('contact', 'custom_field_' . $formfield->field->shortname, + format_string($formfield->field->name), null, null, $formfield->display_data()); + $tree->add_node($node); } } } diff --git a/user/lib.php b/user/lib.php index 5074fc2f796..e9bbef3c757 100644 --- a/user/lib.php +++ b/user/lib.php @@ -331,34 +331,31 @@ function user_get_user_details($user, $course = null, array $userfields = array( $userdetails['fullname'] = fullname($user); if (in_array('customfields', $userfields)) { - $fields = $DB->get_recordset_sql("SELECT f.* - FROM {user_info_field} f - JOIN {user_info_category} c - ON f.categoryid=c.id - ORDER BY c.sortorder ASC, f.sortorder ASC"); + $categories = profile_get_user_fields_with_data_by_category($user->id); $userdetails['customfields'] = array(); - foreach ($fields as $field) { - require_once($CFG->dirroot.'/user/profile/field/'.$field->datatype.'/field.class.php'); - $newfield = 'profile_field_'.$field->datatype; - $formfield = new $newfield($field->id, $user->id); - if ($formfield->is_visible() and !$formfield->is_empty()) { + foreach ($categories as $categoryid => $fields) { + foreach ($fields as $field) { + require_once($CFG->dirroot . '/user/profile/field/' . $field->datatype . '/field.class.php'); + $newfield = 'profile_field_' . $field->datatype; + $formfield = new $newfield($field->id, $user->id, $field); + if ($formfield->is_visible() and !$formfield->is_empty()) { - // TODO: Part of MDL-50728, this conditional coding must be moved to - // proper profile fields API so they are self-contained. - // We only use display_data in fields that require text formatting. - if ($field->datatype == 'text' or $field->datatype == 'textarea') { - $fieldvalue = $formfield->display_data(); - } else { - // Cases: datetime, checkbox and menu. - $fieldvalue = $formfield->data; + // TODO: Part of MDL-50728, this conditional coding must be moved to + // proper profile fields API so they are self-contained. + // We only use display_data in fields that require text formatting. + if ($field->datatype == 'text' or $field->datatype == 'textarea') { + $fieldvalue = $formfield->display_data(); + } else { + // Cases: datetime, checkbox and menu. + $fieldvalue = $formfield->data; + } + + $userdetails['customfields'][] = + array('name' => $formfield->field->name, 'value' => $fieldvalue, + 'type' => $field->datatype, 'shortname' => $formfield->field->shortname); } - - $userdetails['customfields'][] = - array('name' => $formfield->field->name, 'value' => $fieldvalue, - 'type' => $field->datatype, 'shortname' => $formfield->field->shortname); } } - $fields->close(); // Unset customfields if it's empty. if (empty($userdetails['customfields'])) { unset($userdetails['customfields']); diff --git a/user/profile/field/checkbox/field.class.php b/user/profile/field/checkbox/field.class.php index 7e08b368277..d6d9943157c 100644 --- a/user/profile/field/checkbox/field.class.php +++ b/user/profile/field/checkbox/field.class.php @@ -30,39 +30,6 @@ */ class profile_field_checkbox extends profile_field_base { - /** - * Constructor method. - * Pulls out the options for the checkbox from the database and sets the - * the corresponding key for the data if it exists - * - * @param int $fieldid - * @param int $userid - */ - public function __construct($fieldid=0, $userid=0) { - global $DB; - // First call parent constructor. - parent::__construct($fieldid, $userid); - - if (!empty($this->field)) { - $datafield = $DB->get_field('user_info_data', 'data', array('userid' => $this->userid, 'fieldid' => $this->fieldid)); - if ($datafield !== false) { - $this->data = $datafield; - } else { - $this->data = $this->field->defaultdata; - } - } - } - - /** - * Old syntax of class constructor. Deprecated in PHP7. - * - * @deprecated since Moodle 3.1 - */ - public function profile_field_checkbox($fieldid=0, $userid=0) { - debugging('Use of class name as constructor is deprecated', DEBUG_DEVELOPER); - self::__construct($fieldid, $userid); - } - /** * Add elements for editing the profile field value. * @param moodleform $mform diff --git a/user/profile/field/menu/field.class.php b/user/profile/field/menu/field.class.php index e662c490ba7..db63d099fd1 100644 --- a/user/profile/field/menu/field.class.php +++ b/user/profile/field/menu/field.class.php @@ -43,10 +43,11 @@ class profile_field_menu extends profile_field_base { * * @param int $fieldid * @param int $userid + * @param object $fielddata */ - public function __construct($fieldid = 0, $userid = 0) { + public function __construct($fieldid = 0, $userid = 0, $fielddata = null) { // First call parent constructor. - parent::__construct($fieldid, $userid); + parent::__construct($fieldid, $userid, $fielddata); // Param 1 for menu type is the options. if (isset($this->field->param1)) { @@ -78,9 +79,9 @@ class profile_field_menu extends profile_field_base { * * @deprecated since Moodle 3.1 */ - public function profile_field_menu($fieldid=0, $userid=0) { + public function profile_field_menu($fieldid=0, $userid=0, $fielddata=null) { debugging('Use of class name as constructor is deprecated', DEBUG_DEVELOPER); - self::__construct($fieldid, $userid); + self::__construct($fieldid, $userid, $fielddata); } /** diff --git a/user/profile/lib.php b/user/profile/lib.php index 36a63e20d34..f3a394973e0 100644 --- a/user/profile/lib.php +++ b/user/profile/lib.php @@ -60,13 +60,14 @@ class profile_field_base { * Constructor method. * @param int $fieldid id of the profile from the user_info_field table * @param int $userid id of the user for whom we are displaying data + * @param object $fielddata optional data for the field object. */ - public function __construct($fieldid=0, $userid=0) { + public function __construct($fieldid=0, $userid=0, $fielddata=null) { global $USER; $this->set_fieldid($fieldid); $this->set_userid($userid); - $this->load_data(); + $this->load_data($fielddata); } /** @@ -289,36 +290,67 @@ class profile_field_base { } /** - * Accessor method: Load the field record and user data associated with the - * object's fieldid and userid + * Accessor method: Load the field record and user data associated with the object's fieldid and userid. + * If $fielddata is provided, it must be an object structure as follows: + * - $fielddata->field The user_info_field record for this field. + * - $fielddata->data The data column from user_info_data for this user. + * - $fielddata->dataformat The dataformat column from user_info_data for this user. + * * @internal This method should not generally be overwritten by child classes. + * @param object $fielddata Optional data for the field object. */ - public function load_data() { + public function load_data($fielddata = null) { global $DB; - // Load the field object. - if (($this->fieldid == 0) or (!($field = $DB->get_record('user_info_field', array('id' => $this->fieldid))))) { - $this->field = null; - $this->inputname = ''; - } else { + if (($fielddata !== null) && ($this->validate_fielddata($fielddata))) { + // If the field data was passed in, use it and return. + // Note that $fielddata->data will be in both $this->field->data and $this->data, but since PHP copies objects + // by reference, this adds no extra memory strain. + $this->field = $fielddata; + $this->inputname = 'profile_field_'.$fielddata->shortname; + $this->data = $fielddata->data; + $this->dataformat = $fielddata->dataformat; + + } else if (($this->fieldid != 0) && ($field = $DB->get_record('user_info_field', ['id' => $this->fieldid]))) { + // If no data is passed in, and the field id is known, get the data from the database. $this->field = $field; $this->inputname = 'profile_field_'.$field->shortname; - } - - if (!empty($this->field)) { - $params = array('userid' => $this->userid, 'fieldid' => $this->fieldid); - if ($data = $DB->get_record('user_info_data', $params, 'data, dataformat')) { + // Check for instance data and include it if present. Otherwise set to default. + if ($data = $DB->get_record('user_info_data', ['userid' => $this->userid, 'fieldid' => $this->fieldid], + 'data, dataformat')) { $this->data = $data->data; $this->dataformat = $data->dataformat; } else { $this->data = $this->field->defaultdata; $this->dataformat = FORMAT_HTML; } + } else { + $this->field = null; + $this->inputname = ''; $this->data = null; + $this->dataformat = null; } } + /** + * Validate that fielddata parameter used by 'load_data' is structured properly. + * @param object $fielddata + * @return bool + * @throws coding_exception + */ + private function validate_fielddata($fielddata) { + $properties = ['id', 'shortname', 'name', 'datatype', 'description', 'descriptionformat', 'categoryid', 'sortorder', + 'required', 'locked', 'visible', 'forceunique', 'signup', 'defaultdata', 'defaultdataformat', 'param1', 'param2', + 'param3', 'param4', 'param5', 'data', 'dataformat']; + foreach ($properties as $property) { + if (!property_exists($fielddata, $property)) { + throw new \coding_exception('The \'' . $property . '\' property must be set.'); + } + } + return true; + } + /** * Check if the field data is visible to the current user * @internal This method should not generally be overwritten by child classes. @@ -411,20 +443,60 @@ class profile_field_base { } } +/** + * Returns an array of all custom field records with any defined data (or empty data), for the specified user id. + * @param int $userid + * @return array + */ +function profile_get_user_fields_with_data($userid) { + global $DB; + + // Join any user info data present with each user info field for the user object. + $sql = 'SELECT uif.*, uid.data, uid.dataformat '; + $sql .= 'FROM {user_info_field} uif '; + $sql .= 'LEFT JOIN {user_info_data} uid ON uif.id = uid.fieldid AND uid.userid = :userid '; + return $DB->get_records_sql($sql, ['userid' => $userid]); +} + +/** + * Returns an array of all custom field records with any defined data (or empty data), for the specified user id, by category. + * @param int $userid + * @return array + */ +function profile_get_user_fields_with_data_by_category($userid) { + global $DB; + + // Join any user info data present with each user info field for the user object. + $sql = 'SELECT uif.*, uic.name AS categoryname, uid.data, uid.dataformat '; + $sql .= 'FROM {user_info_field} uif '; + $sql .= 'INNER JOIN {user_info_category} uic ON uif.categoryid = uic.id '; + $sql .= 'LEFT JOIN {user_info_data} uid ON uif.id = uid.fieldid AND uid.userid = :userid '; + $sql .= 'ORDER BY uic.sortorder ASC, uif.sortorder ASC '; + $fields = $DB->get_records_sql($sql, ['userid' => $userid]); + $catid = 0; + $data = []; + foreach ($fields as $field) { + if ($field->categoryid != $catid) { + $catid = $field->categoryid; + } + $data[$catid][] = $field; + } + return $data; +} + /** * Loads user profile field data into the user object. * @param stdClass $user */ function profile_load_data($user) { - global $CFG, $DB; + global $CFG; - if ($fields = $DB->get_records('user_info_field')) { - foreach ($fields as $field) { - require_once($CFG->dirroot.'/user/profile/field/'.$field->datatype.'/field.class.php'); - $newfield = 'profile_field_'.$field->datatype; - $formfield = new $newfield($field->id, $user->id); - $formfield->edit_load_user_data($user); - } + $fields = profile_get_user_fields_with_data($user->id); + foreach ($fields as $field) { + require_once($CFG->dirroot.'/user/profile/field/'.$field->datatype.'/field.class.php'); + $newfield = 'profile_field_'.$field->datatype; + $formfield = new $newfield($field->id, $user->id, $field); + $formfield->edit_load_user_data($user); } } @@ -440,28 +512,24 @@ function profile_definition($mform, $userid = 0) { // If user is "admin" fields are displayed regardless. $update = has_capability('moodle/user:update', context_system::instance()); - if ($categories = $DB->get_records('user_info_category', null, 'sortorder ASC')) { - foreach ($categories as $category) { - if ($fields = $DB->get_records('user_info_field', array('categoryid' => $category->id), 'sortorder ASC')) { + $categories = profile_get_user_fields_with_data_by_category($userid); + foreach ($categories as $categoryid => $fields) { + // Check first if *any* fields will be displayed. + $display = false; + foreach ($fields as $field) { + if ($field->visible != PROFILE_VISIBLE_NONE) { + $display = true; + } + } - // Check first if *any* fields will be displayed. - $display = false; - foreach ($fields as $field) { - if ($field->visible != PROFILE_VISIBLE_NONE) { - $display = true; - } - } - - // Display the header and the fields. - if ($display or $update) { - $mform->addElement('header', 'category_'.$category->id, format_string($category->name)); - foreach ($fields as $field) { - require_once($CFG->dirroot.'/user/profile/field/'.$field->datatype.'/field.class.php'); - $newfield = 'profile_field_'.$field->datatype; - $formfield = new $newfield($field->id, $userid); - $formfield->edit_field($mform); - } - } + // Display the header and the fields. + if ($display or $update) { + $mform->addElement('header', 'category_'.$categoryid, format_string($field->categoryname)); + foreach ($fields as $field) { + require_once($CFG->dirroot.'/user/profile/field/'.$field->datatype.'/field.class.php'); + $newfield = 'profile_field_'.$field->datatype; + $formfield = new $newfield($field->id, $userid, $field); + $formfield->edit_field($mform); } } } @@ -473,17 +541,16 @@ function profile_definition($mform, $userid = 0) { * @param int $userid */ function profile_definition_after_data($mform, $userid) { - global $CFG, $DB; + global $CFG; $userid = ($userid < 0) ? 0 : (int)$userid; - if ($fields = $DB->get_records('user_info_field')) { - foreach ($fields as $field) { - require_once($CFG->dirroot.'/user/profile/field/'.$field->datatype.'/field.class.php'); - $newfield = 'profile_field_'.$field->datatype; - $formfield = new $newfield($field->id, $userid); - $formfield->edit_after_data($mform); - } + $fields = profile_get_user_fields_with_data($userid); + foreach ($fields as $field) { + require_once($CFG->dirroot.'/user/profile/field/'.$field->datatype.'/field.class.php'); + $newfield = 'profile_field_'.$field->datatype; + $formfield = new $newfield($field->id, $userid, $field); + $formfield->edit_after_data($mform); } } @@ -494,16 +561,15 @@ function profile_definition_after_data($mform, $userid) { * @return array */ function profile_validation($usernew, $files) { - global $CFG, $DB; + global $CFG; $err = array(); - if ($fields = $DB->get_records('user_info_field')) { - foreach ($fields as $field) { - require_once($CFG->dirroot.'/user/profile/field/'.$field->datatype.'/field.class.php'); - $newfield = 'profile_field_'.$field->datatype; - $formfield = new $newfield($field->id, $usernew->id); - $err += $formfield->edit_validate_field($usernew, $files); - } + $fields = profile_get_user_fields_with_data($usernew->id); + foreach ($fields as $field) { + require_once($CFG->dirroot.'/user/profile/field/'.$field->datatype.'/field.class.php'); + $newfield = 'profile_field_'.$field->datatype; + $formfield = new $newfield($field->id, $usernew->id, $field); + $err += $formfield->edit_validate_field($usernew, $files); } return $err; } @@ -513,15 +579,14 @@ function profile_validation($usernew, $files) { * @param stdClass $usernew */ function profile_save_data($usernew) { - global $CFG, $DB; + global $CFG; - if ($fields = $DB->get_records('user_info_field')) { - foreach ($fields as $field) { - require_once($CFG->dirroot.'/user/profile/field/'.$field->datatype.'/field.class.php'); - $newfield = 'profile_field_'.$field->datatype; - $formfield = new $newfield($field->id, $usernew->id); - $formfield->edit_save_data($usernew); - } + $fields = profile_get_user_fields_with_data($usernew->id); + foreach ($fields as $field) { + require_once($CFG->dirroot.'/user/profile/field/'.$field->datatype.'/field.class.php'); + $newfield = 'profile_field_'.$field->datatype; + $formfield = new $newfield($field->id, $usernew->id, $field); + $formfield->edit_save_data($usernew); } } @@ -532,18 +597,15 @@ function profile_save_data($usernew) { function profile_display_fields($userid) { global $CFG, $USER, $DB; - if ($categories = $DB->get_records('user_info_category', null, 'sortorder ASC')) { - foreach ($categories as $category) { - if ($fields = $DB->get_records('user_info_field', array('categoryid' => $category->id), 'sortorder ASC')) { - foreach ($fields as $field) { - require_once($CFG->dirroot.'/user/profile/field/'.$field->datatype.'/field.class.php'); - $newfield = 'profile_field_'.$field->datatype; - $formfield = new $newfield($field->id, $userid); - if ($formfield->is_visible() and !$formfield->is_empty()) { - echo html_writer::tag('dt', format_string($formfield->field->name)); - echo html_writer::tag('dd', $formfield->display_data()); - } - } + $categories = profile_get_user_fields_with_data_by_category($userid); + foreach ($categories as $categoryid => $fields) { + foreach ($fields as $field) { + require_once($CFG->dirroot.'/user/profile/field/'.$field->datatype.'/field.class.php'); + $newfield = 'profile_field_'.$field->datatype; + $formfield = new $newfield($field->id, $userid, $field); + if ($formfield->is_visible() and !$formfield->is_empty()) { + echo html_writer::tag('dt', format_string($formfield->field->name)); + echo html_writer::tag('dd', $formfield->display_data()); } } } @@ -611,18 +673,17 @@ function profile_signup_fields($mform) { * @return stdClass */ function profile_user_record($userid, $onlyinuserobject = true) { - global $CFG, $DB; + global $CFG; $usercustomfields = new stdClass(); - if ($fields = $DB->get_records('user_info_field')) { - foreach ($fields as $field) { - require_once($CFG->dirroot.'/user/profile/field/'.$field->datatype.'/field.class.php'); - $newfield = 'profile_field_'.$field->datatype; - $formfield = new $newfield($field->id, $userid); - if (!$onlyinuserobject || $formfield->is_user_object_data()) { - $usercustomfields->{$field->shortname} = $formfield->data; - } + $fields = profile_get_user_fields_with_data($userid); + foreach ($fields as $field) { + require_once($CFG->dirroot.'/user/profile/field/'.$field->datatype.'/field.class.php'); + $newfield = 'profile_field_'.$field->datatype; + $formfield = new $newfield($field->id, $userid, $field); + if (!$onlyinuserobject || $formfield->is_user_object_data()) { + $usercustomfields->{$field->shortname} = $formfield->data; } } From b47fda71917f079e7655ee99bc59abf5cd2a47d9 Mon Sep 17 00:00:00 2001 From: Marina Glancy Date: Thu, 28 Sep 2017 12:09:31 +0800 Subject: [PATCH 2/2] MDL-60029 core_user: new api functions rearranged --- lib/myprofilelib.php | 5 +- user/lib.php | 9 +- user/profile/field/menu/field.class.php | 10 - user/profile/field/upgrade.txt | 6 + user/profile/lib.php | 258 ++++++++++++++---------- 5 files changed, 159 insertions(+), 129 deletions(-) create mode 100644 user/profile/field/upgrade.txt diff --git a/lib/myprofilelib.php b/lib/myprofilelib.php index 2a95a140fbf..c873280bc2b 100644 --- a/lib/myprofilelib.php +++ b/lib/myprofilelib.php @@ -369,10 +369,7 @@ function core_myprofile_navigation(core_user\output\myprofile\tree $tree, $user, $categories = profile_get_user_fields_with_data_by_category($user->id); foreach ($categories as $categoryid => $fields) { - foreach ($fields as $field) { - require_once($CFG->dirroot.'/user/profile/field/'.$field->datatype.'/field.class.php'); - $newfield = 'profile_field_'.$field->datatype; - $formfield = new $newfield($field->id, $user->id, $field); + foreach ($fields as $formfield) { if ($formfield->is_visible() and !$formfield->is_empty()) { $node = new core_user\output\myprofile\node('contact', 'custom_field_' . $formfield->field->shortname, format_string($formfield->field->name), null, null, $formfield->display_data()); diff --git a/user/lib.php b/user/lib.php index e9bbef3c757..a7f9942eedf 100644 --- a/user/lib.php +++ b/user/lib.php @@ -334,16 +334,13 @@ function user_get_user_details($user, $course = null, array $userfields = array( $categories = profile_get_user_fields_with_data_by_category($user->id); $userdetails['customfields'] = array(); foreach ($categories as $categoryid => $fields) { - foreach ($fields as $field) { - require_once($CFG->dirroot . '/user/profile/field/' . $field->datatype . '/field.class.php'); - $newfield = 'profile_field_' . $field->datatype; - $formfield = new $newfield($field->id, $user->id, $field); + foreach ($fields as $formfield) { if ($formfield->is_visible() and !$formfield->is_empty()) { // TODO: Part of MDL-50728, this conditional coding must be moved to // proper profile fields API so they are self-contained. // We only use display_data in fields that require text formatting. - if ($field->datatype == 'text' or $field->datatype == 'textarea') { + if ($formfield->field->datatype == 'text' or $formfield->field->datatype == 'textarea') { $fieldvalue = $formfield->display_data(); } else { // Cases: datetime, checkbox and menu. @@ -352,7 +349,7 @@ function user_get_user_details($user, $course = null, array $userfields = array( $userdetails['customfields'][] = array('name' => $formfield->field->name, 'value' => $fieldvalue, - 'type' => $field->datatype, 'shortname' => $formfield->field->shortname); + 'type' => $formfield->field->datatype, 'shortname' => $formfield->field->shortname); } } } diff --git a/user/profile/field/menu/field.class.php b/user/profile/field/menu/field.class.php index db63d099fd1..a304cc4102a 100644 --- a/user/profile/field/menu/field.class.php +++ b/user/profile/field/menu/field.class.php @@ -74,16 +74,6 @@ class profile_field_menu extends profile_field_base { } } - /** - * Old syntax of class constructor. Deprecated in PHP7. - * - * @deprecated since Moodle 3.1 - */ - public function profile_field_menu($fieldid=0, $userid=0, $fielddata=null) { - debugging('Use of class name as constructor is deprecated', DEBUG_DEVELOPER); - self::__construct($fieldid, $userid, $fielddata); - } - /** * Create the code snippet for this field instance * Overwrites the base class method diff --git a/user/profile/field/upgrade.txt b/user/profile/field/upgrade.txt new file mode 100644 index 00000000000..72fc8683d1b --- /dev/null +++ b/user/profile/field/upgrade.txt @@ -0,0 +1,6 @@ +This files describes API changes in /user/profile/field/* - user profile fields, +information provided here is intended especially for developers. + +=== 3.4 === + +* profile_field_base::__construct() now takes three arguments instead of two. Update your plugins if required. diff --git a/user/profile/lib.php b/user/profile/lib.php index f3a394973e0..6223bb63395 100644 --- a/user/profile/lib.php +++ b/user/profile/lib.php @@ -56,18 +56,44 @@ class profile_field_base { /** @var string */ public $dataformat; + /** @var string name of the user profile category */ + protected $categoryname; + /** * Constructor method. * @param int $fieldid id of the profile from the user_info_field table * @param int $userid id of the user for whom we are displaying data - * @param object $fielddata optional data for the field object. + * @param object $fielddata optional data for the field object plus additional fields 'hasuserdata', 'data' and 'dataformat' + * with user data. (If $fielddata->hasuserdata is empty, user data is not available and we should use default data). + * If this parameter is passed, constructor will not call load_data() at all. */ public function __construct($fieldid=0, $userid=0, $fielddata=null) { - global $USER; + global $CFG; + + if ($CFG->debugdeveloper) { + // In Moodle 3.4 the new argument $fielddata was added to the constructor. Make sure that + // plugin constructor properly passes this argument. + $backtrace = debug_backtrace(); + if (isset($backtrace[1]['class']) && $backtrace[1]['function'] === '__construct' && + in_array(self::class, class_parents($backtrace[1]['class']))) { + // If this constructor is called from the constructor of the plugin make sure that the third argument was passed through. + if (count($backtrace[1]['args']) >= 3 && count($backtrace[0]['args']) < 3) { + debugging($backtrace[1]['class'].'::__construct() must support $fielddata as the third argument ' . + 'and pass it to the parent constructor', DEBUG_DEVELOPER); + } + } + } $this->set_fieldid($fieldid); $this->set_userid($userid); - $this->load_data($fielddata); + if ($fielddata) { + $this->set_field($fielddata); + if ($userid && !empty($fielddata->hasuserdata)) { + $this->set_user_data($fielddata->data, $fielddata->dataformat); + } + } else { + $this->load_data(); + } } /** @@ -290,65 +316,93 @@ class profile_field_base { } /** - * Accessor method: Load the field record and user data associated with the object's fieldid and userid. - * If $fielddata is provided, it must be an object structure as follows: - * - $fielddata->field The user_info_field record for this field. - * - $fielddata->data The data column from user_info_data for this user. - * - $fielddata->dataformat The dataformat column from user_info_data for this user. + * Sets the field object and default data and format into $this->data and $this->dataformat * - * @internal This method should not generally be overwritten by child classes. - * @param object $fielddata Optional data for the field object. + * This method should be called before {@link self::set_user_data} + * + * @param stdClass $field + * @throws coding_exception */ - public function load_data($fielddata = null) { - global $DB; - - if (($fielddata !== null) && ($this->validate_fielddata($fielddata))) { - // If the field data was passed in, use it and return. - // Note that $fielddata->data will be in both $this->field->data and $this->data, but since PHP copies objects - // by reference, this adds no extra memory strain. - $this->field = $fielddata; - $this->inputname = 'profile_field_'.$fielddata->shortname; - $this->data = $fielddata->data; - $this->dataformat = $fielddata->dataformat; - - } else if (($this->fieldid != 0) && ($field = $DB->get_record('user_info_field', ['id' => $this->fieldid]))) { - // If no data is passed in, and the field id is known, get the data from the database. - $this->field = $field; - $this->inputname = 'profile_field_'.$field->shortname; - // Check for instance data and include it if present. Otherwise set to default. - if ($data = $DB->get_record('user_info_data', ['userid' => $this->userid, 'fieldid' => $this->fieldid], - 'data, dataformat')) { - $this->data = $data->data; - $this->dataformat = $data->dataformat; - } else { - $this->data = $this->field->defaultdata; - $this->dataformat = FORMAT_HTML; + public function set_field($field) { + global $CFG; + if ($CFG->debugdeveloper) { + $properties = ['id', 'shortname', 'name', 'datatype', 'description', 'descriptionformat', 'categoryid', 'sortorder', + 'required', 'locked', 'visible', 'forceunique', 'signup', 'defaultdata', 'defaultdataformat', 'param1', 'param2', + 'param3', 'param4', 'param5']; + foreach ($properties as $property) { + if (!property_exists($field, $property)) { + debugging('The \'' . $property . '\' property must be set.', DEBUG_DEVELOPER); + } } - - } else { - $this->field = null; - $this->inputname = ''; - $this->data = null; - $this->dataformat = null; } + if ($this->fieldid && $this->fieldid != $field->id) { + throw new coding_exception('Can not set field object after a different field id was set'); + } + $this->fieldid = $field->id; + $this->field = $field; + $this->inputname = 'profile_field_' . $this->field->shortname; + $this->data = $this->field->defaultdata; + $this->dataformat = FORMAT_HTML; } /** - * Validate that fielddata parameter used by 'load_data' is structured properly. - * @param object $fielddata - * @return bool - * @throws coding_exception + * Sets user id and user data for the field + * + * @param mixed $data + * @param int $dataformat */ - private function validate_fielddata($fielddata) { - $properties = ['id', 'shortname', 'name', 'datatype', 'description', 'descriptionformat', 'categoryid', 'sortorder', - 'required', 'locked', 'visible', 'forceunique', 'signup', 'defaultdata', 'defaultdataformat', 'param1', 'param2', - 'param3', 'param4', 'param5', 'data', 'dataformat']; - foreach ($properties as $property) { - if (!property_exists($fielddata, $property)) { - throw new \coding_exception('The \'' . $property . '\' property must be set.'); - } + public function set_user_data($data, $dataformat) { + $this->data = $data; + $this->dataformat = $dataformat; + } + + /** + * Set the name for the profile category where this field is + * + * @param string $categoryname + */ + public function set_category_name($categoryname) { + $this->categoryname = $categoryname; + } + + /** + * Returns the name of the profile category where this field is + * + * @return string + */ + public function get_category_name() { + global $DB; + if ($this->categoryname === null) { + $this->categoryname = $DB->get_field('user_info_category', 'name', ['id' => $this->field->categoryid]); + } + return $this->categoryname; + } + + /** + * Accessor method: Load the field record and user data associated with the + * object's fieldid and userid + * + * @internal This method should not generally be overwritten by child classes. + */ + public function load_data() { + global $DB; + + // Load the field object. + if (($this->fieldid == 0) or (!($field = $DB->get_record('user_info_field', array('id' => $this->fieldid))))) { + $this->field = null; + $this->inputname = ''; + } else { + $this->set_field($field); + } + + if (!empty($this->field) && $this->userid) { + $params = array('userid' => $this->userid, 'fieldid' => $this->fieldid); + if ($data = $DB->get_record('user_info_data', $params, 'data, dataformat')) { + $this->set_user_data($data->data, $data->dataformat); + } + } else { + $this->data = null; } - return true; } /** @@ -359,6 +413,8 @@ class profile_field_base { public function is_visible() { global $USER; + $context = $this->userid ? context_user::instance($this->userid) : context_system::instance(); + switch ($this->field->visible) { case PROFILE_VISIBLE_ALL: return true; @@ -366,12 +422,10 @@ class profile_field_base { if ($this->userid == $USER->id) { return true; } else { - return has_capability('moodle/user:viewalldetails', - context_user::instance($this->userid)); + return has_capability('moodle/user:viewalldetails', $context); } default: - return has_capability('moodle/user:viewalldetails', - context_user::instance($this->userid)); + return has_capability('moodle/user:viewalldetails', $context); } } @@ -446,40 +500,47 @@ class profile_field_base { /** * Returns an array of all custom field records with any defined data (or empty data), for the specified user id. * @param int $userid - * @return array + * @return profile_field_base[] */ function profile_get_user_fields_with_data($userid) { - global $DB; + global $DB, $CFG; // Join any user info data present with each user info field for the user object. - $sql = 'SELECT uif.*, uid.data, uid.dataformat '; + $sql = 'SELECT uif.*, uic.name AS categoryname '; + if ($userid) { + $sql .= ', uid.id AS hasuserdata, uid.data, uid.dataformat '; + } $sql .= 'FROM {user_info_field} uif '; - $sql .= 'LEFT JOIN {user_info_data} uid ON uif.id = uid.fieldid AND uid.userid = :userid '; - return $DB->get_records_sql($sql, ['userid' => $userid]); + $sql .= 'LEFT JOIN {user_info_category} uic ON uif.categoryid = uic.id '; + if ($userid) { + $sql .= 'LEFT JOIN {user_info_data} uid ON uif.id = uid.fieldid AND uid.userid = :userid '; + } + $sql .= 'ORDER BY uic.sortorder ASC, uif.sortorder ASC '; + $fields = $DB->get_records_sql($sql, ['userid' => $userid]); + $data = []; + foreach ($fields as $field) { + require_once($CFG->dirroot . '/user/profile/field/' . $field->datatype . '/field.class.php'); + $classname = 'profile_field_' . $field->datatype; + $field->hasuserdata = !empty($field->hasuserdata); + /** @var profile_field_base $fieldobject */ + $fieldobject = new $classname($field->id, $userid, $field); + $fieldobject->set_category_name($field->categoryname); + unset($field->categoryname); + $data[] = $fieldobject; + } + return $data; } /** * Returns an array of all custom field records with any defined data (or empty data), for the specified user id, by category. * @param int $userid - * @return array + * @return profile_field_base[][] */ function profile_get_user_fields_with_data_by_category($userid) { - global $DB; - - // Join any user info data present with each user info field for the user object. - $sql = 'SELECT uif.*, uic.name AS categoryname, uid.data, uid.dataformat '; - $sql .= 'FROM {user_info_field} uif '; - $sql .= 'INNER JOIN {user_info_category} uic ON uif.categoryid = uic.id '; - $sql .= 'LEFT JOIN {user_info_data} uid ON uif.id = uid.fieldid AND uid.userid = :userid '; - $sql .= 'ORDER BY uic.sortorder ASC, uif.sortorder ASC '; - $fields = $DB->get_records_sql($sql, ['userid' => $userid]); - $catid = 0; + $fields = profile_get_user_fields_with_data($userid); $data = []; foreach ($fields as $field) { - if ($field->categoryid != $catid) { - $catid = $field->categoryid; - } - $data[$catid][] = $field; + $data[$field->field->categoryid][] = $field; } return $data; } @@ -492,10 +553,7 @@ function profile_load_data($user) { global $CFG; $fields = profile_get_user_fields_with_data($user->id); - foreach ($fields as $field) { - require_once($CFG->dirroot.'/user/profile/field/'.$field->datatype.'/field.class.php'); - $newfield = 'profile_field_'.$field->datatype; - $formfield = new $newfield($field->id, $user->id, $field); + foreach ($fields as $formfield) { $formfield->edit_load_user_data($user); } } @@ -516,19 +574,16 @@ function profile_definition($mform, $userid = 0) { foreach ($categories as $categoryid => $fields) { // Check first if *any* fields will be displayed. $display = false; - foreach ($fields as $field) { - if ($field->visible != PROFILE_VISIBLE_NONE) { + foreach ($fields as $formfield) { + if ($formfield->is_visible()) { $display = true; } } // Display the header and the fields. if ($display or $update) { - $mform->addElement('header', 'category_'.$categoryid, format_string($field->categoryname)); - foreach ($fields as $field) { - require_once($CFG->dirroot.'/user/profile/field/'.$field->datatype.'/field.class.php'); - $newfield = 'profile_field_'.$field->datatype; - $formfield = new $newfield($field->id, $userid, $field); + $mform->addElement('header', 'category_'.$categoryid, format_string($formfield->get_category_name())); + foreach ($fields as $formfield) { $formfield->edit_field($mform); } } @@ -546,10 +601,7 @@ function profile_definition_after_data($mform, $userid) { $userid = ($userid < 0) ? 0 : (int)$userid; $fields = profile_get_user_fields_with_data($userid); - foreach ($fields as $field) { - require_once($CFG->dirroot.'/user/profile/field/'.$field->datatype.'/field.class.php'); - $newfield = 'profile_field_'.$field->datatype; - $formfield = new $newfield($field->id, $userid, $field); + foreach ($fields as $formfield) { $formfield->edit_after_data($mform); } } @@ -565,10 +617,7 @@ function profile_validation($usernew, $files) { $err = array(); $fields = profile_get_user_fields_with_data($usernew->id); - foreach ($fields as $field) { - require_once($CFG->dirroot.'/user/profile/field/'.$field->datatype.'/field.class.php'); - $newfield = 'profile_field_'.$field->datatype; - $formfield = new $newfield($field->id, $usernew->id, $field); + foreach ($fields as $formfield) { $err += $formfield->edit_validate_field($usernew, $files); } return $err; @@ -582,10 +631,7 @@ function profile_save_data($usernew) { global $CFG; $fields = profile_get_user_fields_with_data($usernew->id); - foreach ($fields as $field) { - require_once($CFG->dirroot.'/user/profile/field/'.$field->datatype.'/field.class.php'); - $newfield = 'profile_field_'.$field->datatype; - $formfield = new $newfield($field->id, $usernew->id, $field); + foreach ($fields as $formfield) { $formfield->edit_save_data($usernew); } } @@ -599,10 +645,7 @@ function profile_display_fields($userid) { $categories = profile_get_user_fields_with_data_by_category($userid); foreach ($categories as $categoryid => $fields) { - foreach ($fields as $field) { - require_once($CFG->dirroot.'/user/profile/field/'.$field->datatype.'/field.class.php'); - $newfield = 'profile_field_'.$field->datatype; - $formfield = new $newfield($field->id, $userid, $field); + foreach ($fields as $formfield) { if ($formfield->is_visible() and !$formfield->is_empty()) { echo html_writer::tag('dt', format_string($formfield->field->name)); echo html_writer::tag('dd', $formfield->display_data()); @@ -678,12 +721,9 @@ function profile_user_record($userid, $onlyinuserobject = true) { $usercustomfields = new stdClass(); $fields = profile_get_user_fields_with_data($userid); - foreach ($fields as $field) { - require_once($CFG->dirroot.'/user/profile/field/'.$field->datatype.'/field.class.php'); - $newfield = 'profile_field_'.$field->datatype; - $formfield = new $newfield($field->id, $userid, $field); + foreach ($fields as $formfield) { if (!$onlyinuserobject || $formfield->is_user_object_data()) { - $usercustomfields->{$field->shortname} = $formfield->data; + $usercustomfields->{$formfield->field->shortname} = $formfield->data; } }