diff --git a/competency/classes/invalid_persistent_exception.php b/competency/classes/invalid_persistent_exception.php index 53133ad09aa..5d34db36291 100644 --- a/competency/classes/invalid_persistent_exception.php +++ b/competency/classes/invalid_persistent_exception.php @@ -26,24 +26,16 @@ namespace core_competency; defined('MOODLE_INTERNAL') || die(); +debugging('The class core_competency\\invalid_persistent_exception is deprecated. ' . + 'Please use core\\invalid_persistent_exception instead.'); + /** * Invalid persistent exception class. * * @package core_competency * @copyright 2015 Frédéric Massart - FMCorz.net * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @deprecated since Moodle 3.3 */ -class invalid_persistent_exception extends \moodle_exception { - - public function __construct(array $errors = array()) { - $forhumans = array(); - $debuginfo = array(); - foreach ($errors as $key => $message) { - $debuginfo[] = "$key: $message"; - $forhumans[] = $message; - } - parent::__construct('invalidpersistenterror', 'core_competency', null, - implode(', ', $forhumans), implode(' - ', $debuginfo)); - } - +class invalid_persistent_exception extends \core\invalid_persistent_exception { } diff --git a/competency/classes/persistent.php b/competency/classes/persistent.php index fe94961f17f..7f792ef7107 100644 --- a/competency/classes/persistent.php +++ b/competency/classes/persistent.php @@ -24,842 +24,24 @@ namespace core_competency; defined('MOODLE_INTERNAL') || die(); -use coding_exception; -use invalid_parameter_exception; -use lang_string; -use ReflectionMethod; -use stdClass; -use renderer_base; +// We need to alias the invalid_persistent_exception, because the persistent classes from +// core_competency used to throw a \core_competency\invalid_persistent_exception. They now +// fully inherit from \core\persistent which throws a core exception. Using class_alias +// ensures that previous try/catch statements still work. Also note that we always need +// need to alias, we cannot do it passively in the classloader because try/catch statements +// do not trigger a class loading. Note that for this trick to work, all the classes +// which were extending \core_competency\persistent still need to extend it or the alias +// won't be effective. +class_alias('core\\invalid_persistent_exception', 'core_competency\\invalid_persistent_exception'); /** * Abstract class for core_competency objects saved to the DB. * + * This is a legacy class which all core_competency persistent classes created prior + * to 3.3 must extend. + * * @copyright 2015 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -abstract class persistent { - - /** The table name. */ - const TABLE = null; - - /** @var array The model data. */ - private $data = array(); - - /** @var array The list of validation errors. */ - private $errors = array(); - - /** @var boolean If the data was already validated. */ - private $validated = false; - - /** - * Create an instance of this class. - * - * @param int $id If set, this is the id of an existing record, used to load the data. - * @param stdClass $record If set will be passed to {@link self::from_record()}. - */ - public function __construct($id = 0, stdClass $record = null) { - if ($id > 0) { - $this->set('id', $id); - $this->read(); - } - if (!empty($record)) { - $this->from_record($record); - } - } - - /** - * Magic method to capture getters and setters. - * - * @param string $method Callee. - * @param array $arguments List of arguments. - * @return mixed - */ - final public function __call($method, $arguments) { - if (strpos($method, 'get_') === 0) { - return $this->get(substr($method, 4)); - } else if (strpos($method, 'set_') === 0) { - return $this->set(substr($method, 4), $arguments[0]); - } - throw new coding_exception('Unexpected method call: ' . $method); - } - - /** - * Data getter. - * - * This is the main getter for all the properties. Developers can implement their own getters - * but they should be calling {@link self::get()} in order to retrieve the value. Essentially - * the getters defined by the developers would only ever be used as helper methods and will not - * be called internally at this stage. In other words, do not expect {@link self::to_record()} or - * {@link self::from_record()} to use them. - * - * This is protected because we wouldn't want the developers to get into the habit of - * using $persistent->get('property_name'), the lengthy getters must be used. - * - * @param string $property The property name. - * @return mixed - */ - final protected function get($property) { - if (!static::has_property($property)) { - throw new coding_exception('Unexpected property \'' . s($property) .'\' requested.'); - } - if (!array_key_exists($property, $this->data) && !static::is_property_required($property)) { - $this->set($property, static::get_property_default_value($property)); - } - return isset($this->data[$property]) ? $this->data[$property] : null; - } - - /** - * Data setter. - * - * This is the main setter for all the properties. Developers can implement their own setters - * but they should always be calling {@link self::set()} in order to set the value. Essentially - * the setters defined by the developers are helper methods and will not be called internally - * at this stage. In other words do not expect {@link self::to_record()} or - * {@link self::from_record()} to use them. - * - * This is protected because we wouldn't want the developers to get into the habit of - * using $persistent->set('property_name', ''), the lengthy setters must be used. - * - * @param string $property The property name. - * @param mixed $value The value. - * @return mixed - */ - final protected function set($property, $value) { - if (!static::has_property($property)) { - throw new coding_exception('Unexpected property \'' . s($property) .'\' requested.'); - } - if (!array_key_exists($property, $this->data) || $this->data[$property] != $value) { - // If the value is changing, we invalidate the model. - $this->validated = false; - } - $this->data[$property] = $value; - } - - /** - * Return the custom definition of the properties of this model. - * - * Each property MUST be listed here. - * - * The result of this method is cached internally for the whole request. - * - * The 'default' value can be a Closure when its value may change during a single request. - * For example if the default value is based on a $CFG property, then it should be wrapped in a closure - * to avoid running into scenarios where the true value of $CFG is not reflected in the definition. - * Do not abuse closures as they obviously add some overhead. - * - * Examples: - * - * array( - * 'property_name' => array( - * 'default' => 'Default value', // When not set, the property is considered as required. - * 'message' => new lang_string(...), // Defaults to invalid data error message. - * 'null' => NULL_ALLOWED, // Defaults to NULL_NOT_ALLOWED. Takes NULL_NOW_ALLOWED or NULL_ALLOWED. - * 'type' => PARAM_TYPE, // Mandatory. - * 'choices' => array(1, 2, 3) // An array of accepted values. - * ) - * ) - * - * array( - * 'dynamic_property_name' => array( - * 'default' => function() { - * return $CFG->something; - * }, - * 'type' => PARAM_INT, - * ) - * ) - * - * @return array Where keys are the property names. - */ - protected static function define_properties() { - return array(); - } - - /** - * Get the properties definition of this model.. - * - * @return array - */ - final public static function properties_definition() { - global $CFG; - - static $def = null; - if ($def !== null) { - return $def; - } - - $def = static::define_properties(); - $def['id'] = array( - 'default' => 0, - 'type' => PARAM_INT, - ); - $def['timecreated'] = array( - 'default' => 0, - 'type' => PARAM_INT, - ); - $def['timemodified'] = array( - 'default' => 0, - 'type' => PARAM_INT - ); - $def['usermodified'] = array( - 'default' => 0, - 'type' => PARAM_INT - ); - - // List of reserved property names. Mostly because we have methods (getters/setters) which would confict with them. - // Think about backwards compability before adding new ones here! - $reserved = array('errors', 'formatted_properties', 'records', 'records_select', 'property_default_value', - 'property_error_message', 'sql_fields'); - - foreach ($def as $property => $definition) { - - // Ensures that the null property is always set. - if (!array_key_exists('null', $definition)) { - $def[$property]['null'] = NULL_NOT_ALLOWED; - } - - // Warn the developers when they are doing something wrong. - if ($CFG->debugdeveloper) { - if (!array_key_exists('type', $definition)) { - throw new coding_exception('Missing type for: ' . $property); - - } else if (isset($definition['message']) && !($definition['message'] instanceof lang_string)) { - throw new coding_exception('Invalid error message for: ' . $property); - - } else if (in_array($property, $reserved)) { - throw new coding_exception('This property cannot be defined: ' . $property); - - } - } - } - - return $def; - } - - /** - * Gets all the formatted properties. - * - * Formatted properties are properties which have a format associated with them. - * - * @return array Keys are property names, values are property format names. - */ - final public static function get_formatted_properties() { - $properties = static::properties_definition(); - - $formatted = array(); - foreach ($properties as $property => $definition) { - $propertyformat = $property . 'format'; - if ($definition['type'] == PARAM_RAW && array_key_exists($propertyformat, $properties) - && $properties[$propertyformat]['type'] == PARAM_INT) { - $formatted[$property] = $propertyformat; - } - } - - return $formatted; - } - - /** - * Gets the default value for a property. - * - * This assumes that the property exists. - * - * @param string $property The property name. - * @return mixed - */ - final protected static function get_property_default_value($property) { - $properties = static::properties_definition(); - if (!isset($properties[$property]['default'])) { - return null; - } - $value = $properties[$property]['default']; - if ($value instanceof \Closure) { - return $value(); - } - return $value; - } - - /** - * Gets the error message for a property. - * - * This assumes that the property exists. - * - * @param string $property The property name. - * @return lang_string - */ - final protected static function get_property_error_message($property) { - $properties = static::properties_definition(); - if (!isset($properties[$property]['message'])) { - return new lang_string('invaliddata', 'error'); - } - return $properties[$property]['message']; - } - - /** - * Returns whether or not a property was defined. - * - * @param string $property The property name. - * @return boolean - */ - final public static function has_property($property) { - $properties = static::properties_definition(); - return isset($properties[$property]); - } - - /** - * Returns whether or not a property is required. - * - * By definition a property with a default value is not required. - * - * @param string $property The property name. - * @return boolean - */ - final public static function is_property_required($property) { - $properties = static::properties_definition(); - return !array_key_exists('default', $properties[$property]); - } - - /** - * Populate this class with data from a DB record. - * - * Note that this does not use any custom setter because the data here is intended to - * represent what is stored in the database. - * - * @param \stdClass $record A DB record. - * @return persistent - */ - final public function from_record(stdClass $record) { - $record = (array) $record; - foreach ($record as $property => $value) { - $this->set($property, $value); - } - return $this; - } - - /** - * Create a DB record from this class. - * - * Note that this does not use any custom getter because the data here is intended to - * represent what is stored in the database. - * - * @return \stdClass - */ - final public function to_record() { - $data = new stdClass(); - $properties = static::properties_definition(); - foreach ($properties as $property => $definition) { - $data->$property = $this->get($property); - } - return $data; - } - - /** - * Load the data from the DB. - * - * @return persistent - */ - final public function read() { - global $DB; - - if ($this->get_id() <= 0) { - throw new coding_exception('id is required to load'); - } - $record = $DB->get_record(static::TABLE, array('id' => $this->get_id()), '*', MUST_EXIST); - $this->from_record($record); - - // Validate the data as it comes from the database. - $this->validated = true; - - return $this; - } - - /** - * Hook to execute before a create. - * - * Please note that at this stage the data has already been validated and therefore - * any new data being set will not be validated before it is sent to the database. - * - * This is only intended to be used by child classes, do not put any logic here! - * - * @return void - */ - protected function before_create() { - } - - /** - * Insert a record in the DB. - * - * @return persistent - */ - final public function create() { - global $DB, $USER; - - if ($this->get_id()) { - // The validation methods rely on the ID to know if we're updating or not, the ID should be - // falsy whenever we are creating an object. - throw new coding_exception('Cannot create an object that has an ID defined.'); - } - - if (!$this->is_valid()) { - throw new invalid_persistent_exception($this->get_errors()); - } - - // Before create hook. - $this->before_create(); - - // We can safely set those values bypassing the validation because we know what we're doing. - $now = time(); - $this->set('timecreated', $now); - $this->set('timemodified', $now); - $this->set('usermodified', $USER->id); - - $record = $this->to_record(); - unset($record->id); - - $id = $DB->insert_record(static::TABLE, $record); - $this->set('id', $id); - - // We ensure that this is flagged as validated. - $this->validated = true; - - // After create hook. - $this->after_create(); - - return $this; - } - - /** - * Hook to execute after a create. - * - * This is only intended to be used by child classes, do not put any logic here! - * - * @return void - */ - protected function after_create() { - } - - /** - * Hook to execute before an update. - * - * Please note that at this stage the data has already been validated and therefore - * any new data being set will not be validated before it is sent to the database. - * - * This is only intended to be used by child classes, do not put any logic here! - * - * @return void - */ - protected function before_update() { - } - - /** - * Update the existing record in the DB. - * - * @return bool True on success. - */ - final public function update() { - global $DB, $USER; - - if ($this->get_id() <= 0) { - throw new coding_exception('id is required to update'); - } else if (!$this->is_valid()) { - throw new invalid_persistent_exception($this->get_errors()); - } - - // Before update hook. - $this->before_update(); - - // We can safely set those values after the validation because we know what we're doing. - $this->set('timemodified', time()); - $this->set('usermodified', $USER->id); - - $record = $this->to_record(); - unset($record->timecreated); - $record = (array) $record; - - // Save the record. - $result = $DB->update_record(static::TABLE, $record); - - // We ensure that this is flagged as validated. - $this->validated = true; - - // After update hook. - $this->after_update($result); - - return $result; - } - - /** - * Hook to execute after an update. - * - * This is only intended to be used by child classes, do not put any logic here! - * - * @param bool $result Whether or not the update was successful. - * @return void - */ - protected function after_update($result) { - } - - /** - * Hook to execute before a delete. - * - * This is only intended to be used by child classes, do not put any logic here! - * - * @return void - */ - protected function before_delete() { - } - - /** - * Delete an entry from the database. - * - * @return bool True on success. - */ - final public function delete() { - global $DB; - - if ($this->get_id() <= 0) { - throw new coding_exception('id is required to delete'); - } - - // Hook before delete. - $this->before_delete(); - - $result = $DB->delete_records(static::TABLE, array('id' => $this->get_id())); - - // Hook after delete. - $this->after_delete($result); - - // Reset the ID to avoid any confusion, this also invalidates the model's data. - if ($result) { - $this->set('id', 0); - } - - return $result; - } - - /** - * Hook to execute after a delete. - * - * This is only intended to be used by child classes, do not put any logic here! - * - * @param bool $result Whether or not the delete was successful. - * @return void - */ - protected function after_delete($result) { - } - - /** - * Hook to execute before the validation. - * - * This hook will not affect the validation results in any way but is useful to - * internally set properties which will need to be validated. - * - * This is only intended to be used by child classes, do not put any logic here! - * - * @return void - */ - protected function before_validate() { - } - - /** - * Validates the data. - * - * Developers can implement addition validation by defining a method as follows. Note that - * the method MUST return a lang_string() when there is an error, and true when the data is valid. - * - * protected function validate_propertyname($value) { - * if ($value !== 'My expected value') { - * return new lang_string('invaliddata', 'error'); - * } - * return true - * } - * - * It is OK to use other properties in your custom validation methods when you need to, however note - * they might not have been validated yet, so try not to rely on them too much. - * - * Note that the validation methods should be protected. Validating just one field is not - * recommended because of the possible dependencies between one field and another,also the - * field ID can be used to check whether the object is being updated or created. - * - * When validating foreign keys the persistent should only check that the associated model - * exists. The validation methods should not be used to check for a change in that relationship. - * The API method setting the attributes on the model should be responsible for that. - * E.g. On a course model, the method validate_categoryid will check that the category exists. - * However, if a course can never be moved outside of its category it would be up to the calling - * code to ensure that the category ID will not be altered. - * - * @return array|true Returns true when the validation passed, or an array of properties with errors. - */ - final public function validate() { - global $CFG; - - // Before validate hook. - $this->before_validate(); - - // If this object has not been validated yet. - if ($this->validated !== true) { - - $errors = array(); - $properties = static::properties_definition(); - foreach ($properties as $property => $definition) { - - // Get the data, bypassing the potential custom getter which could alter the data. - $value = $this->get($property); - - // Check if the property is required. - if ($value === null && static::is_property_required($property)) { - $errors[$property] = new lang_string('requiredelement', 'form'); - continue; - } - - // Check that type of value is respected. - try { - if ($definition['type'] === PARAM_BOOL && $value === false) { - // Validate_param() does not like false with PARAM_BOOL, better to convert it to int. - $value = 0; - } - validate_param($value, $definition['type'], $definition['null']); - } catch (invalid_parameter_exception $e) { - $errors[$property] = static::get_property_error_message($property); - continue; - } - - // Check that the value is part of a list of allowed values. - if (isset($definition['choices']) && !in_array($value, $definition['choices'])) { - $errors[$property] = static::get_property_error_message($property); - continue; - } - - // Call custom validation method. - $method = 'validate_' . $property; - if (method_exists($this, $method)) { - - // Warn the developers when they are doing something wrong. - if ($CFG->debugdeveloper) { - $reflection = new ReflectionMethod($this, $method); - if (!$reflection->isProtected()) { - throw new coding_exception('The method ' . get_class($this) . '::'. $method . ' should be protected.'); - } - } - - $valid = $this->{$method}($value); - if ($valid !== true) { - if (!($valid instanceof lang_string)) { - throw new coding_exception('Unexpected error message.'); - } - $errors[$property] = $valid; - continue; - } - } - } - - $this->validated = true; - $this->errors = $errors; - } - - return empty($this->errors) ? true : $this->errors; - } - - /** - * Returns whether or not the model is valid. - * - * @return boolean True when it is. - */ - final public function is_valid() { - return $this->validate() === true; - } - - /** - * Returns the validation errors. - * - * @return array - */ - final public function get_errors() { - $this->validate(); - return $this->errors; - } - - /** - * Extract a record from a row of data. - * - * Most likely used in combination with {@link self::get_sql_fields()}. This method is - * simple enough to be used by non-persistent classes, keep that in mind when modifying it. - * - * e.g. persistent::extract_record($row, 'user'); should work. - * - * @param stdClass $row The row of data. - * @param string $prefix The prefix the data fields are prefixed with, defaults to the table name followed by underscore. - * @return stdClass The extracted data. - */ - public static function extract_record($row, $prefix = null) { - if ($prefix === null) { - $prefix = str_replace('_', '', static::TABLE) . '_'; - } - $prefixlength = strlen($prefix); - - $data = new stdClass(); - foreach ($row as $property => $value) { - if (strpos($property, $prefix) === 0) { - $propertyname = substr($property, $prefixlength); - $data->$propertyname = $value; - } - } - - return $data; - } - - /** - * Load a list of records. - * - * @param array $filters Filters to apply. - * @param string $sort Field to sort by. - * @param string $order Sort order. - * @param int $skip Limitstart. - * @param int $limit Number of rows to return. - * - * @return \core_competency\persistent[] - */ - public static function get_records($filters = array(), $sort = '', $order = 'ASC', $skip = 0, $limit = 0) { - global $DB; - - $orderby = ''; - if (!empty($sort)) { - $orderby = $sort . ' ' . $order; - } - - $records = $DB->get_records(static::TABLE, $filters, $orderby, '*', $skip, $limit); - $instances = array(); - - foreach ($records as $record) { - $newrecord = new static(0, $record); - array_push($instances, $newrecord); - } - return $instances; - } - - /** - * Load a single record. - * - * @param array $filters Filters to apply. - * @return false|\core_competency\persistent - */ - public static function get_record($filters = array()) { - global $DB; - - $record = $DB->get_record(static::TABLE, $filters); - return $record ? new static(0, $record) : false; - } - - /** - * Load a list of records based on a select query. - * - * @param string $select - * @param array $params - * @param string $sort - * @param string $fields - * @param int $limitfrom - * @param int $limitnum - * @return \core_competency\persistent[] - */ - public static function get_records_select($select, $params = null, $sort = '', $fields = '*', $limitfrom = 0, $limitnum = 0) { - global $DB; - - $records = $DB->get_records_select(static::TABLE, $select, $params, $sort, $fields, $limitfrom, $limitnum); - - // We return class instances. - $instances = array(); - foreach ($records as $key => $record) { - $instances[$key] = new static(0, $record); - } - - return $instances; - - } - - /** - * Return the list of fields for use in a SELECT clause. - * - * Having the complete list of fields prefixed allows for multiple persistents to be fetched - * in a single query. Use {@link self::extract_record()} to extract the records from the query result. - * - * @param string $alias The alias used for the table. - * @param string $prefix The prefix to use for each field, defaults to the table name followed by underscore. - * @return string The SQL fragment. - */ - public static function get_sql_fields($alias, $prefix = null) { - global $CFG; - $fields = array(); - - if ($prefix === null) { - $prefix = str_replace('_', '', static::TABLE) . '_'; - } - - // Get the properties and move ID to the top. - $properties = static::properties_definition(); - $id = $properties['id']; - unset($properties['id']); - $properties = array('id' => $id) + $properties; - - foreach ($properties as $property => $definition) { - $as = $prefix . $property; - $fields[] = $alias . '.' . $property . ' AS ' . $as; - - // Warn developers that the query will not always work. - if ($CFG->debugdeveloper && strlen($as) > 30) { - throw new coding_exception("The alias '$as' for column '$alias.$property' exceeds 30 characters" . - " and will therefore not work across all supported databases."); - } - } - - return implode(', ', $fields); - } - - /** - * Count a list of records. - * - * @param array $conditions An array of conditions. - * @return int - */ - public static function count_records(array $conditions = array()) { - global $DB; - - $count = $DB->count_records(static::TABLE, $conditions); - return $count; - } - - /** - * Count a list of records. - * - * @param string $select - * @param array $params - * @return int - */ - public static function count_records_select($select, $params = null) { - global $DB; - - $count = $DB->count_records_select(static::TABLE, $select, $params); - return $count; - } - - /** - * Check if a record exists by ID. - * - * @param int $id Record ID. - * @return bool - */ - public static function record_exists($id) { - global $DB; - return $DB->record_exists(static::TABLE, array('id' => $id)); - } - - /** - * Check if a records exists. - * - * @param string $select - * @param array $params - * @return bool - */ - public static function record_exists_select($select, array $params = null) { - global $DB; - return $DB->record_exists_select(static::TABLE, $select, $params); - } - +abstract class persistent extends \core\persistent { } diff --git a/competency/tests/api_test.php b/competency/tests/api_test.php index d55446bee4c..3b053e8c352 100644 --- a/competency/tests/api_test.php +++ b/competency/tests/api_test.php @@ -2824,7 +2824,7 @@ class core_competency_api_testcase extends advanced_testcase { 'proficiency' => true, 'grade' => 3 )); $usercompetency->create(); $this->fail('Invalid grade not detected in framework scale'); - } catch (\core_competency\invalid_persistent_exception $e) { + } catch (\core\invalid_persistent_exception $e) { $this->assertTrue(true); } @@ -2834,7 +2834,7 @@ class core_competency_api_testcase extends advanced_testcase { 'proficiency' => true, 'grade' => 5 )); $usercompetency->create(); $this->fail('Invalid grade not detected in competency scale'); - } catch (\core_competency\invalid_persistent_exception $e) { + } catch (\core\invalid_persistent_exception $e) { $this->assertTrue(true); } @@ -2844,7 +2844,7 @@ class core_competency_api_testcase extends advanced_testcase { 'proficiency' => true, 'grade' => 1 )); $usercompetency->create(); $this->assertTrue(true); - } catch (\core_competency\invalid_persistent_exception $e) { + } catch (\core\invalid_persistent_exception $e) { $this->fail('Valide grade rejected in framework scale'); } @@ -2854,7 +2854,7 @@ class core_competency_api_testcase extends advanced_testcase { 'proficiency' => true, 'grade' => 4 )); $usercompetency->create(); $this->assertTrue(true); - } catch (\core_competency\invalid_persistent_exception $e) { + } catch (\core\invalid_persistent_exception $e) { $this->fail('Valide grade rejected in competency scale'); } } diff --git a/competency/tests/external_test.php b/competency/tests/external_test.php index 0b989bfee4a..609a2cbaf67 100644 --- a/competency/tests/external_test.php +++ b/competency/tests/external_test.php @@ -621,7 +621,7 @@ class core_competency_external_testcase extends externallib_advanced_testcase { try { $result = $this->update_competency_framework($f2->get_id(), 4, true); $this->fail('The scale cannot be changed once used.'); - } catch (\core_competency\invalid_persistent_exception $e) { + } catch (\core\invalid_persistent_exception $e) { $this->assertRegexp('/scaleid/', $e->getMessage()); } } diff --git a/competency/upgrade.txt b/competency/upgrade.txt new file mode 100644 index 00000000000..4b77551d1f3 --- /dev/null +++ b/competency/upgrade.txt @@ -0,0 +1,9 @@ +This files describes API changes in /competency/*. The information provided +here is intended especially for developers. + +=== 3.3 === + +* Deprecated classes and their new equivalent: + - core_competency\persistent -> core\persistent + - core_competency\invalid_persistent_exception -> core\invalid_persistent_exception + diff --git a/lib/classes/invalid_persistent_exception.php b/lib/classes/invalid_persistent_exception.php new file mode 100644 index 00000000000..9ff50bdec1b --- /dev/null +++ b/lib/classes/invalid_persistent_exception.php @@ -0,0 +1,49 @@ +. + +/** + * Invalid persistent exception. + * + * @package core + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Invalid persistent exception class. + * + * @package core + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class invalid_persistent_exception extends \moodle_exception { + + public function __construct(array $errors = array()) { + $forhumans = array(); + $debuginfo = array(); + foreach ($errors as $key => $message) { + $debuginfo[] = "$key: $message"; + $forhumans[] = $message; + } + parent::__construct('invalidpersistenterror', 'core', null, + implode(', ', $forhumans), implode(' - ', $debuginfo)); + } + +} diff --git a/lib/classes/persistent.php b/lib/classes/persistent.php new file mode 100644 index 00000000000..463e06efc24 --- /dev/null +++ b/lib/classes/persistent.php @@ -0,0 +1,865 @@ +. + +/** + * Abstract class for objects saved to the DB. + * + * @package core + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core; +defined('MOODLE_INTERNAL') || die(); + +use coding_exception; +use invalid_parameter_exception; +use lang_string; +use ReflectionMethod; +use stdClass; +use renderer_base; + +/** + * Abstract class for core objects saved to the DB. + * + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class persistent { + + /** The table name. */ + const TABLE = null; + + /** @var array The model data. */ + private $data = array(); + + /** @var array The list of validation errors. */ + private $errors = array(); + + /** @var boolean If the data was already validated. */ + private $validated = false; + + /** + * Create an instance of this class. + * + * @param int $id If set, this is the id of an existing record, used to load the data. + * @param stdClass $record If set will be passed to {@link self::from_record()}. + */ + public function __construct($id = 0, stdClass $record = null) { + if ($id > 0) { + $this->set('id', $id); + $this->read(); + } + if (!empty($record)) { + $this->from_record($record); + } + } + + /** + * Magic method to capture getters and setters. + * + * @param string $method Callee. + * @param array $arguments List of arguments. + * @return mixed + */ + final public function __call($method, $arguments) { + if (strpos($method, 'get_') === 0) { + return $this->get(substr($method, 4)); + } else if (strpos($method, 'set_') === 0) { + return $this->set(substr($method, 4), $arguments[0]); + } + throw new coding_exception('Unexpected method call: ' . $method); + } + + /** + * Data getter. + * + * This is the main getter for all the properties. Developers can implement their own getters + * but they should be calling {@link self::get()} in order to retrieve the value. Essentially + * the getters defined by the developers would only ever be used as helper methods and will not + * be called internally at this stage. In other words, do not expect {@link self::to_record()} or + * {@link self::from_record()} to use them. + * + * This is protected because we wouldn't want the developers to get into the habit of + * using $persistent->get('property_name'), the lengthy getters must be used. + * + * @param string $property The property name. + * @return mixed + */ + final protected function get($property) { + if (!static::has_property($property)) { + throw new coding_exception('Unexpected property \'' . s($property) .'\' requested.'); + } + if (!array_key_exists($property, $this->data) && !static::is_property_required($property)) { + $this->set($property, static::get_property_default_value($property)); + } + return isset($this->data[$property]) ? $this->data[$property] : null; + } + + /** + * Data setter. + * + * This is the main setter for all the properties. Developers can implement their own setters + * but they should always be calling {@link self::set()} in order to set the value. Essentially + * the setters defined by the developers are helper methods and will not be called internally + * at this stage. In other words do not expect {@link self::to_record()} or + * {@link self::from_record()} to use them. + * + * This is protected because we wouldn't want the developers to get into the habit of + * using $persistent->set('property_name', ''), the lengthy setters must be used. + * + * @param string $property The property name. + * @param mixed $value The value. + * @return mixed + */ + final protected function set($property, $value) { + if (!static::has_property($property)) { + throw new coding_exception('Unexpected property \'' . s($property) .'\' requested.'); + } + if (!array_key_exists($property, $this->data) || $this->data[$property] != $value) { + // If the value is changing, we invalidate the model. + $this->validated = false; + } + $this->data[$property] = $value; + } + + /** + * Return the custom definition of the properties of this model. + * + * Each property MUST be listed here. + * + * The result of this method is cached internally for the whole request. + * + * The 'default' value can be a Closure when its value may change during a single request. + * For example if the default value is based on a $CFG property, then it should be wrapped in a closure + * to avoid running into scenarios where the true value of $CFG is not reflected in the definition. + * Do not abuse closures as they obviously add some overhead. + * + * Examples: + * + * array( + * 'property_name' => array( + * 'default' => 'Default value', // When not set, the property is considered as required. + * 'message' => new lang_string(...), // Defaults to invalid data error message. + * 'null' => NULL_ALLOWED, // Defaults to NULL_NOT_ALLOWED. Takes NULL_NOW_ALLOWED or NULL_ALLOWED. + * 'type' => PARAM_TYPE, // Mandatory. + * 'choices' => array(1, 2, 3) // An array of accepted values. + * ) + * ) + * + * array( + * 'dynamic_property_name' => array( + * 'default' => function() { + * return $CFG->something; + * }, + * 'type' => PARAM_INT, + * ) + * ) + * + * @return array Where keys are the property names. + */ + protected static function define_properties() { + return array(); + } + + /** + * Get the properties definition of this model.. + * + * @return array + */ + final public static function properties_definition() { + global $CFG; + + static $def = null; + if ($def !== null) { + return $def; + } + + $def = static::define_properties(); + $def['id'] = array( + 'default' => 0, + 'type' => PARAM_INT, + ); + $def['timecreated'] = array( + 'default' => 0, + 'type' => PARAM_INT, + ); + $def['timemodified'] = array( + 'default' => 0, + 'type' => PARAM_INT + ); + $def['usermodified'] = array( + 'default' => 0, + 'type' => PARAM_INT + ); + + // List of reserved property names. Mostly because we have methods (getters/setters) which would confict with them. + // Think about backwards compability before adding new ones here! + $reserved = array('errors', 'formatted_properties', 'records', 'records_select', 'property_default_value', + 'property_error_message', 'sql_fields'); + + foreach ($def as $property => $definition) { + + // Ensures that the null property is always set. + if (!array_key_exists('null', $definition)) { + $def[$property]['null'] = NULL_NOT_ALLOWED; + } + + // Warn the developers when they are doing something wrong. + if ($CFG->debugdeveloper) { + if (!array_key_exists('type', $definition)) { + throw new coding_exception('Missing type for: ' . $property); + + } else if (isset($definition['message']) && !($definition['message'] instanceof lang_string)) { + throw new coding_exception('Invalid error message for: ' . $property); + + } else if (in_array($property, $reserved)) { + throw new coding_exception('This property cannot be defined: ' . $property); + + } + } + } + + return $def; + } + + /** + * Gets all the formatted properties. + * + * Formatted properties are properties which have a format associated with them. + * + * @return array Keys are property names, values are property format names. + */ + final public static function get_formatted_properties() { + $properties = static::properties_definition(); + + $formatted = array(); + foreach ($properties as $property => $definition) { + $propertyformat = $property . 'format'; + if ($definition['type'] == PARAM_RAW && array_key_exists($propertyformat, $properties) + && $properties[$propertyformat]['type'] == PARAM_INT) { + $formatted[$property] = $propertyformat; + } + } + + return $formatted; + } + + /** + * Gets the default value for a property. + * + * This assumes that the property exists. + * + * @param string $property The property name. + * @return mixed + */ + final protected static function get_property_default_value($property) { + $properties = static::properties_definition(); + if (!isset($properties[$property]['default'])) { + return null; + } + $value = $properties[$property]['default']; + if ($value instanceof \Closure) { + return $value(); + } + return $value; + } + + /** + * Gets the error message for a property. + * + * This assumes that the property exists. + * + * @param string $property The property name. + * @return lang_string + */ + final protected static function get_property_error_message($property) { + $properties = static::properties_definition(); + if (!isset($properties[$property]['message'])) { + return new lang_string('invaliddata', 'error'); + } + return $properties[$property]['message']; + } + + /** + * Returns whether or not a property was defined. + * + * @param string $property The property name. + * @return boolean + */ + final public static function has_property($property) { + $properties = static::properties_definition(); + return isset($properties[$property]); + } + + /** + * Returns whether or not a property is required. + * + * By definition a property with a default value is not required. + * + * @param string $property The property name. + * @return boolean + */ + final public static function is_property_required($property) { + $properties = static::properties_definition(); + return !array_key_exists('default', $properties[$property]); + } + + /** + * Populate this class with data from a DB record. + * + * Note that this does not use any custom setter because the data here is intended to + * represent what is stored in the database. + * + * @param \stdClass $record A DB record. + * @return persistent + */ + final public function from_record(stdClass $record) { + $record = (array) $record; + foreach ($record as $property => $value) { + $this->set($property, $value); + } + return $this; + } + + /** + * Create a DB record from this class. + * + * Note that this does not use any custom getter because the data here is intended to + * represent what is stored in the database. + * + * @return \stdClass + */ + final public function to_record() { + $data = new stdClass(); + $properties = static::properties_definition(); + foreach ($properties as $property => $definition) { + $data->$property = $this->get($property); + } + return $data; + } + + /** + * Load the data from the DB. + * + * @return persistent + */ + final public function read() { + global $DB; + + if ($this->get_id() <= 0) { + throw new coding_exception('id is required to load'); + } + $record = $DB->get_record(static::TABLE, array('id' => $this->get_id()), '*', MUST_EXIST); + $this->from_record($record); + + // Validate the data as it comes from the database. + $this->validated = true; + + return $this; + } + + /** + * Hook to execute before a create. + * + * Please note that at this stage the data has already been validated and therefore + * any new data being set will not be validated before it is sent to the database. + * + * This is only intended to be used by child classes, do not put any logic here! + * + * @return void + */ + protected function before_create() { + } + + /** + * Insert a record in the DB. + * + * @return persistent + */ + final public function create() { + global $DB, $USER; + + if ($this->get_id()) { + // The validation methods rely on the ID to know if we're updating or not, the ID should be + // falsy whenever we are creating an object. + throw new coding_exception('Cannot create an object that has an ID defined.'); + } + + if (!$this->is_valid()) { + throw new invalid_persistent_exception($this->get_errors()); + } + + // Before create hook. + $this->before_create(); + + // We can safely set those values bypassing the validation because we know what we're doing. + $now = time(); + $this->set('timecreated', $now); + $this->set('timemodified', $now); + $this->set('usermodified', $USER->id); + + $record = $this->to_record(); + unset($record->id); + + $id = $DB->insert_record(static::TABLE, $record); + $this->set('id', $id); + + // We ensure that this is flagged as validated. + $this->validated = true; + + // After create hook. + $this->after_create(); + + return $this; + } + + /** + * Hook to execute after a create. + * + * This is only intended to be used by child classes, do not put any logic here! + * + * @return void + */ + protected function after_create() { + } + + /** + * Hook to execute before an update. + * + * Please note that at this stage the data has already been validated and therefore + * any new data being set will not be validated before it is sent to the database. + * + * This is only intended to be used by child classes, do not put any logic here! + * + * @return void + */ + protected function before_update() { + } + + /** + * Update the existing record in the DB. + * + * @return bool True on success. + */ + final public function update() { + global $DB, $USER; + + if ($this->get_id() <= 0) { + throw new coding_exception('id is required to update'); + } else if (!$this->is_valid()) { + throw new invalid_persistent_exception($this->get_errors()); + } + + // Before update hook. + $this->before_update(); + + // We can safely set those values after the validation because we know what we're doing. + $this->set('timemodified', time()); + $this->set('usermodified', $USER->id); + + $record = $this->to_record(); + unset($record->timecreated); + $record = (array) $record; + + // Save the record. + $result = $DB->update_record(static::TABLE, $record); + + // We ensure that this is flagged as validated. + $this->validated = true; + + // After update hook. + $this->after_update($result); + + return $result; + } + + /** + * Hook to execute after an update. + * + * This is only intended to be used by child classes, do not put any logic here! + * + * @param bool $result Whether or not the update was successful. + * @return void + */ + protected function after_update($result) { + } + + /** + * Hook to execute before a delete. + * + * This is only intended to be used by child classes, do not put any logic here! + * + * @return void + */ + protected function before_delete() { + } + + /** + * Delete an entry from the database. + * + * @return bool True on success. + */ + final public function delete() { + global $DB; + + if ($this->get_id() <= 0) { + throw new coding_exception('id is required to delete'); + } + + // Hook before delete. + $this->before_delete(); + + $result = $DB->delete_records(static::TABLE, array('id' => $this->get_id())); + + // Hook after delete. + $this->after_delete($result); + + // Reset the ID to avoid any confusion, this also invalidates the model's data. + if ($result) { + $this->set('id', 0); + } + + return $result; + } + + /** + * Hook to execute after a delete. + * + * This is only intended to be used by child classes, do not put any logic here! + * + * @param bool $result Whether or not the delete was successful. + * @return void + */ + protected function after_delete($result) { + } + + /** + * Hook to execute before the validation. + * + * This hook will not affect the validation results in any way but is useful to + * internally set properties which will need to be validated. + * + * This is only intended to be used by child classes, do not put any logic here! + * + * @return void + */ + protected function before_validate() { + } + + /** + * Validates the data. + * + * Developers can implement addition validation by defining a method as follows. Note that + * the method MUST return a lang_string() when there is an error, and true when the data is valid. + * + * protected function validate_propertyname($value) { + * if ($value !== 'My expected value') { + * return new lang_string('invaliddata', 'error'); + * } + * return true + * } + * + * It is OK to use other properties in your custom validation methods when you need to, however note + * they might not have been validated yet, so try not to rely on them too much. + * + * Note that the validation methods should be protected. Validating just one field is not + * recommended because of the possible dependencies between one field and another,also the + * field ID can be used to check whether the object is being updated or created. + * + * When validating foreign keys the persistent should only check that the associated model + * exists. The validation methods should not be used to check for a change in that relationship. + * The API method setting the attributes on the model should be responsible for that. + * E.g. On a course model, the method validate_categoryid will check that the category exists. + * However, if a course can never be moved outside of its category it would be up to the calling + * code to ensure that the category ID will not be altered. + * + * @return array|true Returns true when the validation passed, or an array of properties with errors. + */ + final public function validate() { + global $CFG; + + // Before validate hook. + $this->before_validate(); + + // If this object has not been validated yet. + if ($this->validated !== true) { + + $errors = array(); + $properties = static::properties_definition(); + foreach ($properties as $property => $definition) { + + // Get the data, bypassing the potential custom getter which could alter the data. + $value = $this->get($property); + + // Check if the property is required. + if ($value === null && static::is_property_required($property)) { + $errors[$property] = new lang_string('requiredelement', 'form'); + continue; + } + + // Check that type of value is respected. + try { + if ($definition['type'] === PARAM_BOOL && $value === false) { + // Validate_param() does not like false with PARAM_BOOL, better to convert it to int. + $value = 0; + } + validate_param($value, $definition['type'], $definition['null']); + } catch (invalid_parameter_exception $e) { + $errors[$property] = static::get_property_error_message($property); + continue; + } + + // Check that the value is part of a list of allowed values. + if (isset($definition['choices']) && !in_array($value, $definition['choices'])) { + $errors[$property] = static::get_property_error_message($property); + continue; + } + + // Call custom validation method. + $method = 'validate_' . $property; + if (method_exists($this, $method)) { + + // Warn the developers when they are doing something wrong. + if ($CFG->debugdeveloper) { + $reflection = new ReflectionMethod($this, $method); + if (!$reflection->isProtected()) { + throw new coding_exception('The method ' . get_class($this) . '::'. $method . ' should be protected.'); + } + } + + $valid = $this->{$method}($value); + if ($valid !== true) { + if (!($valid instanceof lang_string)) { + throw new coding_exception('Unexpected error message.'); + } + $errors[$property] = $valid; + continue; + } + } + } + + $this->validated = true; + $this->errors = $errors; + } + + return empty($this->errors) ? true : $this->errors; + } + + /** + * Returns whether or not the model is valid. + * + * @return boolean True when it is. + */ + final public function is_valid() { + return $this->validate() === true; + } + + /** + * Returns the validation errors. + * + * @return array + */ + final public function get_errors() { + $this->validate(); + return $this->errors; + } + + /** + * Extract a record from a row of data. + * + * Most likely used in combination with {@link self::get_sql_fields()}. This method is + * simple enough to be used by non-persistent classes, keep that in mind when modifying it. + * + * e.g. persistent::extract_record($row, 'user'); should work. + * + * @param stdClass $row The row of data. + * @param string $prefix The prefix the data fields are prefixed with, defaults to the table name followed by underscore. + * @return stdClass The extracted data. + */ + public static function extract_record($row, $prefix = null) { + if ($prefix === null) { + $prefix = str_replace('_', '', static::TABLE) . '_'; + } + $prefixlength = strlen($prefix); + + $data = new stdClass(); + foreach ($row as $property => $value) { + if (strpos($property, $prefix) === 0) { + $propertyname = substr($property, $prefixlength); + $data->$propertyname = $value; + } + } + + return $data; + } + + /** + * Load a list of records. + * + * @param array $filters Filters to apply. + * @param string $sort Field to sort by. + * @param string $order Sort order. + * @param int $skip Limitstart. + * @param int $limit Number of rows to return. + * + * @return \core\persistent[] + */ + public static function get_records($filters = array(), $sort = '', $order = 'ASC', $skip = 0, $limit = 0) { + global $DB; + + $orderby = ''; + if (!empty($sort)) { + $orderby = $sort . ' ' . $order; + } + + $records = $DB->get_records(static::TABLE, $filters, $orderby, '*', $skip, $limit); + $instances = array(); + + foreach ($records as $record) { + $newrecord = new static(0, $record); + array_push($instances, $newrecord); + } + return $instances; + } + + /** + * Load a single record. + * + * @param array $filters Filters to apply. + * @return false|\core\persistent + */ + public static function get_record($filters = array()) { + global $DB; + + $record = $DB->get_record(static::TABLE, $filters); + return $record ? new static(0, $record) : false; + } + + /** + * Load a list of records based on a select query. + * + * @param string $select + * @param array $params + * @param string $sort + * @param string $fields + * @param int $limitfrom + * @param int $limitnum + * @return \core\persistent[] + */ + public static function get_records_select($select, $params = null, $sort = '', $fields = '*', $limitfrom = 0, $limitnum = 0) { + global $DB; + + $records = $DB->get_records_select(static::TABLE, $select, $params, $sort, $fields, $limitfrom, $limitnum); + + // We return class instances. + $instances = array(); + foreach ($records as $key => $record) { + $instances[$key] = new static(0, $record); + } + + return $instances; + + } + + /** + * Return the list of fields for use in a SELECT clause. + * + * Having the complete list of fields prefixed allows for multiple persistents to be fetched + * in a single query. Use {@link self::extract_record()} to extract the records from the query result. + * + * @param string $alias The alias used for the table. + * @param string $prefix The prefix to use for each field, defaults to the table name followed by underscore. + * @return string The SQL fragment. + */ + public static function get_sql_fields($alias, $prefix = null) { + global $CFG; + $fields = array(); + + if ($prefix === null) { + $prefix = str_replace('_', '', static::TABLE) . '_'; + } + + // Get the properties and move ID to the top. + $properties = static::properties_definition(); + $id = $properties['id']; + unset($properties['id']); + $properties = array('id' => $id) + $properties; + + foreach ($properties as $property => $definition) { + $as = $prefix . $property; + $fields[] = $alias . '.' . $property . ' AS ' . $as; + + // Warn developers that the query will not always work. + if ($CFG->debugdeveloper && strlen($as) > 30) { + throw new coding_exception("The alias '$as' for column '$alias.$property' exceeds 30 characters" . + " and will therefore not work across all supported databases."); + } + } + + return implode(', ', $fields); + } + + /** + * Count a list of records. + * + * @param array $conditions An array of conditions. + * @return int + */ + public static function count_records(array $conditions = array()) { + global $DB; + + $count = $DB->count_records(static::TABLE, $conditions); + return $count; + } + + /** + * Count a list of records. + * + * @param string $select + * @param array $params + * @return int + */ + public static function count_records_select($select, $params = null) { + global $DB; + + $count = $DB->count_records_select(static::TABLE, $select, $params); + return $count; + } + + /** + * Check if a record exists by ID. + * + * @param int $id Record ID. + * @return bool + */ + public static function record_exists($id) { + global $DB; + return $DB->record_exists(static::TABLE, array('id' => $id)); + } + + /** + * Check if a records exists. + * + * @param string $select + * @param array $params + * @return bool + */ + public static function record_exists_select($select, array $params = null) { + global $DB; + return $DB->record_exists_select(static::TABLE, $select, $params); + } + +} diff --git a/competency/tests/persistent_test.php b/lib/tests/persistent_test.php similarity index 67% rename from competency/tests/persistent_test.php rename to lib/tests/persistent_test.php index fc8966c4099..ad88c028dd0 100644 --- a/competency/tests/persistent_test.php +++ b/lib/tests/persistent_test.php @@ -17,7 +17,7 @@ /** * Persistent class tests. * - * @package core_competency + * @package core * @copyright 2015 Frédéric Massart - FMCorz.net * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ @@ -28,16 +28,47 @@ global $CFG; /** * Persistent testcase. * - * @package core_competency + * @package core * @copyright 2015 Frédéric Massart - FMCorz.net * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class core_competency_persistent_testcase extends advanced_testcase { +class core_persistent_testcase extends advanced_testcase { public function setUp() { + $this->make_persistent_table(); $this->resetAfterTest(); } + /** + * Make the table for the persistent. + */ + protected function make_persistent_table() { + global $DB; + $dbman = $DB->get_manager(); + + $table = new xmldb_table(core_testable_persistent::TABLE); + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('shortname', XMLDB_TYPE_CHAR, '100', null, null, null, null); + $table->add_field('idnumber', XMLDB_TYPE_CHAR, '100', null, null, null, null); + $table->add_field('description', XMLDB_TYPE_TEXT, null, null, null, null, null); + $table->add_field('descriptionformat', XMLDB_TYPE_INTEGER, '4', null, XMLDB_NOTNULL, null, '0'); + $table->add_field('parentid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'); + $table->add_field('path', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null); + $table->add_field('sortorder', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('scaleid', XMLDB_TYPE_INTEGER, '10', null, null, null, null); + $table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('usermodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'); + + $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); + + if ($dbman->table_exists($table)) { + $dbman->drop_table($table); + } + + $dbman->create_table($table); + } + public function test_properties_definition() { $expected = array( 'shortname' => array( @@ -75,9 +106,9 @@ class core_competency_persistent_testcase extends advanced_testcase { 'message' => new lang_string('invalidrequest', 'error'), 'null' => NULL_NOT_ALLOWED ), - 'competencyframeworkid' => array( + 'scaleid' => array( + 'default' => null, 'type' => PARAM_INT, - 'default' => 0, 'null' => NULL_ALLOWED ), 'id' => array( @@ -100,37 +131,12 @@ class core_competency_persistent_testcase extends advanced_testcase { 'type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED ), - 'ruletype' => array( - 'type' => PARAM_RAW, - 'default' => null, - 'null' => NULL_ALLOWED, - ), - 'ruleconfig' => array( - 'type' => PARAM_RAW, - 'default' => null, - 'null' => NULL_ALLOWED, - ), - 'ruleoutcome' => array( - 'type' => PARAM_RAW, - 'default' => 0, - 'null' => NULL_NOT_ALLOWED - ), - 'scaleid' => array( - 'default' => null, - 'type' => PARAM_INT, - 'null' => NULL_ALLOWED - ), - 'scaleconfiguration' => array( - 'type' => PARAM_RAW, - 'default' => null, - 'null' => NULL_ALLOWED - ) ); - $this->assertEquals($expected, core_competency_testable_persistent::properties_definition()); + $this->assertEquals($expected, core_testable_persistent::properties_definition()); } public function test_to_record() { - $p = new core_competency_testable_persistent(); + $p = new core_testable_persistent(); $expected = (object) array( 'shortname' => '', 'idnumber' => null, @@ -139,22 +145,17 @@ class core_competency_persistent_testcase extends advanced_testcase { 'parentid' => 0, 'path' => '', 'sortorder' => null, - 'competencyframeworkid' => null, 'id' => 0, 'timecreated' => 0, 'timemodified' => 0, 'usermodified' => 0, - 'ruletype' => null, - 'ruleconfig' => null, - 'ruleoutcome' => 0, 'scaleid' => null, - 'scaleconfiguration' => null, ); $this->assertEquals($expected, $p->to_record()); } public function test_from_record() { - $p = new core_competency_testable_persistent(); + $p = new core_testable_persistent(); $data = (object) array( 'shortname' => 'ddd', 'idnumber' => 'abc', @@ -163,16 +164,11 @@ class core_competency_persistent_testcase extends advanced_testcase { 'parentid' => 999, 'path' => '/a/b/c', 'sortorder' => 12, - 'competencyframeworkid' => 5, 'id' => 1, 'timecreated' => 2, 'timemodified' => 3, 'usermodified' => 4, - 'ruletype' => null, - 'ruleconfig' => null, - 'ruleoutcome' => 0, 'scaleid' => null, - 'scaleconfiguration' => null, ); $p->from_record($data); $this->assertEquals($data, $p->to_record()); @@ -182,7 +178,7 @@ class core_competency_persistent_testcase extends advanced_testcase { * @expectedException coding_exception */ public function test_from_record_invalid_param() { - $p = new core_competency_testable_persistent(); + $p = new core_testable_persistent(); $data = (object) array( 'invalidparam' => 'abc' ); @@ -195,7 +191,7 @@ class core_competency_persistent_testcase extends advanced_testcase { 'idnumber' => 'abc', 'sortorder' => 0 ); - $p = new core_competency_testable_persistent(0, $data); + $p = new core_testable_persistent(0, $data); $this->assertFalse(isset($p->beforevalidate)); $this->assertTrue($p->validate()); $this->assertTrue(isset($p->beforevalidate)); @@ -215,7 +211,7 @@ class core_competency_persistent_testcase extends advanced_testcase { $data = (object) array( 'idnumber' => 'abc' ); - $p = new core_competency_testable_persistent(0, $data); + $p = new core_testable_persistent(0, $data); $expected = array( 'sortorder' => new lang_string('requiredelement', 'form'), ); @@ -228,7 +224,7 @@ class core_competency_persistent_testcase extends advanced_testcase { 'idnumber' => 'abc', 'sortorder' => 10, ); - $p = new core_competency_testable_persistent(0, $data); + $p = new core_testable_persistent(0, $data); $expected = array( 'sortorder' => new lang_string('invalidkey', 'error'), ); @@ -241,7 +237,7 @@ class core_competency_persistent_testcase extends advanced_testcase { 'idnumber' => 'abc', 'sortorder' => 'abc', ); - $p = new core_competency_testable_persistent(0, $data); + $p = new core_testable_persistent(0, $data); $expected = array( 'sortorder' => new lang_string('invalidrequest', 'error'), ); @@ -255,7 +251,7 @@ class core_competency_persistent_testcase extends advanced_testcase { 'sortorder' => 0, 'descriptionformat' => -100 ); - $p = new core_competency_testable_persistent(0, $data); + $p = new core_testable_persistent(0, $data); $expected = array( 'descriptionformat' => new lang_string('invaliddata', 'error'), ); @@ -268,7 +264,7 @@ class core_competency_persistent_testcase extends advanced_testcase { 'idnumber' => 'abc', 'sortorder' => 'NaN' ); - $p = new core_competency_testable_persistent(0, $data); + $p = new core_testable_persistent(0, $data); $this->assertFalse($p->is_valid()); $this->assertArrayHasKey('sortorder', $p->get_errors()); } @@ -277,28 +273,28 @@ class core_competency_persistent_testcase extends advanced_testcase { $data = (object) array( 'idnumber' => null, 'sortorder' => 0, - 'competencyframeworkid' => 'bad!' + 'scaleid' => 'bad!' ); - $p = new core_competency_testable_persistent(0, $data); + $p = new core_testable_persistent(0, $data); $this->assertFalse($p->is_valid()); $this->assertArrayHasKey('idnumber', $p->get_errors()); - $this->assertArrayHasKey('competencyframeworkid', $p->get_errors()); + $this->assertArrayHasKey('scaleid', $p->get_errors()); $p->set_idnumber('abc'); $this->assertFalse($p->is_valid()); $this->assertArrayNotHasKey('idnumber', $p->get_errors()); - $this->assertArrayHasKey('competencyframeworkid', $p->get_errors()); - $p->set_competencyframeworkid(null); + $this->assertArrayHasKey('scaleid', $p->get_errors()); + $p->set_scaleid(null); $this->assertTrue($p->is_valid()); - $this->assertArrayNotHasKey('competencyframeworkid', $p->get_errors()); + $this->assertArrayNotHasKey('scaleid', $p->get_errors()); } public function test_create() { global $DB; - $p = new core_competency_testable_persistent(0, (object) array('sortorder' => 123, 'idnumber' => 'abc')); + $p = new core_testable_persistent(0, (object) array('sortorder' => 123, 'idnumber' => 'abc')); $this->assertFalse(isset($p->beforecreate)); $this->assertFalse(isset($p->aftercreate)); $p->create(); - $record = $DB->get_record(core_competency_testable_persistent::TABLE, array('id' => $p->get_id()), '*', MUST_EXIST); + $record = $DB->get_record(core_testable_persistent::TABLE, array('id' => $p->get_id()), '*', MUST_EXIST); $expected = $p->to_record(); $this->assertTrue(isset($p->beforecreate)); $this->assertTrue(isset($p->aftercreate)); @@ -310,7 +306,7 @@ class core_competency_persistent_testcase extends advanced_testcase { public function test_update() { global $DB; - $p = new core_competency_testable_persistent(0, (object) array('sortorder' => 123, 'idnumber' => 'abc')); + $p = new core_testable_persistent(0, (object) array('sortorder' => 123, 'idnumber' => 'abc')); $p->create(); $id = $p->get_id(); $p->set_sortorder(456); @@ -320,7 +316,7 @@ class core_competency_persistent_testcase extends advanced_testcase { $p->update(); $expected = $p->to_record(); - $record = $DB->get_record(core_competency_testable_persistent::TABLE, array('id' => $p->get_id()), '*', MUST_EXIST); + $record = $DB->get_record(core_testable_persistent::TABLE, array('id' => $p->get_id()), '*', MUST_EXIST); $this->assertTrue(isset($p->beforeupdate)); $this->assertTrue(isset($p->afterupdate)); $this->assertEquals($id, $record->id); @@ -330,16 +326,16 @@ class core_competency_persistent_testcase extends advanced_testcase { } public function test_read() { - $p = new core_competency_testable_persistent(0, (object) array('sortorder' => 123, 'idnumber' => 'abc')); + $p = new core_testable_persistent(0, (object) array('sortorder' => 123, 'idnumber' => 'abc')); $p->create(); unset($p->beforevalidate); unset($p->beforecreate); unset($p->aftercreate); - $p2 = new core_competency_testable_persistent($p->get_id()); + $p2 = new core_testable_persistent($p->get_id()); $this->assertEquals($p, $p2); - $p3 = new core_competency_testable_persistent(); + $p3 = new core_testable_persistent(); $p3->set_id($p->get_id()); $p3->read(); $this->assertEquals($p, $p3); @@ -348,23 +344,23 @@ class core_competency_persistent_testcase extends advanced_testcase { public function test_delete() { global $DB; - $p = new core_competency_testable_persistent(0, (object) array('sortorder' => 123, 'idnumber' => 'abc')); + $p = new core_testable_persistent(0, (object) array('sortorder' => 123, 'idnumber' => 'abc')); $p->create(); $this->assertNotEquals(0, $p->get_id()); - $this->assertTrue($DB->record_exists_select(core_competency_testable_persistent::TABLE, 'id = ?', array($p->get_id()))); + $this->assertTrue($DB->record_exists_select(core_testable_persistent::TABLE, 'id = ?', array($p->get_id()))); $this->assertFalse(isset($p->beforedelete)); $this->assertFalse(isset($p->afterdelete)); $p->delete(); - $this->assertFalse($DB->record_exists_select(core_competency_testable_persistent::TABLE, 'id = ?', array($p->get_id()))); + $this->assertFalse($DB->record_exists_select(core_testable_persistent::TABLE, 'id = ?', array($p->get_id()))); $this->assertEquals(0, $p->get_id()); $this->assertEquals(true, $p->beforedelete); $this->assertEquals(true, $p->afterdelete); } public function test_has_property() { - $this->assertFalse(core_competency_testable_persistent::has_property('unknown')); - $this->assertTrue(core_competency_testable_persistent::has_property('idnumber')); + $this->assertFalse(core_testable_persistent::has_property('unknown')); + $this->assertTrue(core_testable_persistent::has_property('idnumber')); } public function test_custom_setter_getter() { @@ -373,48 +369,43 @@ class core_competency_persistent_testcase extends advanced_testcase { $path = array(1, 2, 3); $json = json_encode($path); - $p = new core_competency_testable_persistent(0, (object) array('sortorder' => 0, 'idnumber' => 'abc')); + $p = new core_testable_persistent(0, (object) array('sortorder' => 0, 'idnumber' => 'abc')); $p->set_path($path); $this->assertEquals($path, $p->get_path()); $this->assertEquals($json, $p->to_record()->path); $p->create(); - $record = $DB->get_record(core_competency_testable_persistent::TABLE, array('id' => $p->get_id()), 'id, path', MUST_EXIST); + $record = $DB->get_record(core_testable_persistent::TABLE, array('id' => $p->get_id()), 'id, path', MUST_EXIST); $this->assertEquals($json, $record->path); } public function test_record_exists() { global $DB; - $this->assertFalse($DB->record_exists(core_competency_testable_persistent::TABLE, array('idnumber' => 'abc'))); - $p = new core_competency_testable_persistent(0, (object) array('sortorder' => 123, 'idnumber' => 'abc')); + $this->assertFalse($DB->record_exists(core_testable_persistent::TABLE, array('idnumber' => 'abc'))); + $p = new core_testable_persistent(0, (object) array('sortorder' => 123, 'idnumber' => 'abc')); $p->create(); $id = $p->get_id(); - $this->assertTrue(core_competency_testable_persistent::record_exists($id)); - $this->assertTrue($DB->record_exists(core_competency_testable_persistent::TABLE, array('idnumber' => 'abc'))); + $this->assertTrue(core_testable_persistent::record_exists($id)); + $this->assertTrue($DB->record_exists(core_testable_persistent::TABLE, array('idnumber' => 'abc'))); $p->delete(); - $this->assertFalse(core_competency_testable_persistent::record_exists($id)); + $this->assertFalse(core_testable_persistent::record_exists($id)); } public function test_get_sql_fields() { $expected = '' . - 'c.id AS comp_id, ' . - 'c.shortname AS comp_shortname, ' . - 'c.idnumber AS comp_idnumber, ' . - 'c.description AS comp_description, ' . - 'c.descriptionformat AS comp_descriptionformat, ' . - 'c.parentid AS comp_parentid, ' . - 'c.path AS comp_path, ' . - 'c.sortorder AS comp_sortorder, ' . - 'c.competencyframeworkid AS comp_competencyframeworkid, ' . - 'c.ruletype AS comp_ruletype, ' . - 'c.ruleconfig AS comp_ruleconfig, ' . - 'c.ruleoutcome AS comp_ruleoutcome, ' . - 'c.scaleid AS comp_scaleid, ' . - 'c.scaleconfiguration AS comp_scaleconfiguration, ' . - 'c.timecreated AS comp_timecreated, ' . - 'c.timemodified AS comp_timemodified, ' . - 'c.usermodified AS comp_usermodified'; - $this->assertEquals($expected, core_competency_testable_persistent::get_sql_fields('c', 'comp_')); + 'c.id AS prefix_id, ' . + 'c.shortname AS prefix_shortname, ' . + 'c.idnumber AS prefix_idnumber, ' . + 'c.description AS prefix_description, ' . + 'c.descriptionformat AS prefix_descriptionformat, ' . + 'c.parentid AS prefix_parentid, ' . + 'c.path AS prefix_path, ' . + 'c.sortorder AS prefix_sortorder, ' . + 'c.scaleid AS prefix_scaleid, ' . + 'c.timecreated AS prefix_timecreated, ' . + 'c.timemodified AS prefix_timemodified, ' . + 'c.usermodified AS prefix_usermodified'; + $this->assertEquals($expected, core_testable_persistent::get_sql_fields('c', 'prefix_')); } /** @@ -422,20 +413,20 @@ class core_competency_persistent_testcase extends advanced_testcase { * @expectedExceptionMessageRegExp /The alias .+ exceeds 30 characters/ */ public function test_get_sql_fields_too_long() { - core_competency_testable_persistent::get_sql_fields('c'); + core_testable_persistent::get_sql_fields('c'); } } /** * Example persistent class. * - * @package core_competency + * @package core * @copyright 2015 Frédéric Massart - FMCorz.net * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class core_competency_testable_persistent extends \core_competency\persistent { +class core_testable_persistent extends \core\persistent { - const TABLE = 'competency'; + const TABLE = 'phpunit_persistent'; protected static function define_properties() { return array( @@ -467,34 +458,10 @@ class core_competency_testable_persistent extends \core_competency\persistent { 'type' => PARAM_INT, 'message' => new lang_string('invalidrequest', 'error') ), - 'competencyframeworkid' => array( - 'type' => PARAM_INT, - 'default' => 0, - 'null' => NULL_ALLOWED - ), - 'ruletype' => array( - 'type' => PARAM_RAW, - 'default' => null, - 'null' => NULL_ALLOWED, - ), - 'ruleconfig' => array( - 'type' => PARAM_RAW, - 'default' => null, - 'null' => NULL_ALLOWED, - ), - 'ruleoutcome' => array( - 'type' => PARAM_RAW, - 'default' => 0 - ), 'scaleid' => array( 'type' => PARAM_INT, 'default' => null, 'null' => NULL_ALLOWED - ), - 'scaleconfiguration' => array( - 'type' => PARAM_RAW, - 'default' => null, - 'null' => NULL_ALLOWED ) ); }