2 // This file is part of Moodle - http://moodle.org/
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 // GNU General Public License for more details.
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
18 * Abstract class for tool_lp objects saved to the DB.
21 * @copyright 2015 Damyon Wiese
22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25 defined('MOODLE_INTERNAL') || die();
28 use invalid_parameter_exception;
35 * Abstract class for tool_lp objects saved to the DB.
37 * @copyright 2015 Damyon Wiese
38 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
40 abstract class persistent {
42 /** The table name. */
45 /** @var array The model data. */
46 private $data = array();
48 /** @var array The list of validation errors. */
49 private $errors = array();
51 /** @var boolean If the data was already validated. */
52 private $validated = false;
55 * Create an instance of this class.
57 * @param int $id If set, this is the id of an existing record, used to load the data.
58 * @param stdClass $record If set will be passed to {@link self::from_record()}.
60 public function __construct($id = 0, stdClass $record = null) {
62 $this->set('id', $id);
65 if (!empty($record)) {
66 $this->from_record($record);
71 * Magic method to capture getters and setters.
73 * @param string $method Callee.
74 * @param array $arguments List of arguments.
77 final public function __call($method, $arguments) {
78 if (strpos($method, 'get_') === 0) {
79 return $this->get(substr($method, 4));
80 } else if (strpos($method, 'set_') === 0) {
81 return $this->set(substr($method, 4), $arguments[0]);
83 throw new coding_exception('Unexpected method call: ' . $method);
89 * This is the main getter for all the properties. Developers can implement their own getters
90 * but they should be calling {@link self::get()} in order to retrieve the value. Essentially
91 * the getters defined by the developers would only ever be used as helper methods and will not
92 * be called internally at this stage. In other words, do not expect {@link self::to_record()} or
93 * {@link self::from_record()} to use them.
95 * This is protected because we wouldn't want the developers to get into the habit of
96 * using $persistent->get('property_name'), the lengthy getters must be used.
98 * @param string $property The property name.
101 final protected function get($property) {
102 if (!static::has_property($property)) {
103 throw new coding_exception('Unexpected property \'' . s($property) .'\' requested.');
105 if (!array_key_exists($property, $this->data) && !static::is_property_required($property)) {
106 $this->set($property, static::get_property_default_value($property));
108 return isset($this->data[$property]) ? $this->data[$property] : null;
114 * This is the main setter for all the properties. Developers can implement their own setters
115 * but they should always be calling {@link self::set()} in order to set the value. Essentially
116 * the setters defined by the developers are helper methods and will not be called internally
117 * at this stage. In other words do not expect {@link self::to_record()} or
118 * {@link self::from_record()} to use them.
120 * This is protected because we wouldn't want the developers to get into the habit of
121 * using $persistent->set('property_name', ''), the lengthy setters must be used.
123 * @param string $property The property name.
124 * @param mixed $value The value.
127 final protected function set($property, $value) {
128 if (!static::has_property($property)) {
129 throw new coding_exception('Unexpected property \'' . s($property) .'\' requested.');
131 if (!array_key_exists($property, $this->data) || $this->data[$property] != $value) {
132 // If the value is changing, we invalidate the model.
133 $this->validated = false;
135 $this->data[$property] = $value;
139 * Return the custom definition of the properties of this model.
141 * Each property MUST be listed here.
146 * 'property_name' => array(
147 * 'default' => 'Default value', // When not set, the property is considered as required.
148 * 'message' => new lang_string(...), // Defaults to invalid data error message.
149 * 'null' => NULL_ALLOWED, // Defaults to NULL_NOT_ALLOWED. Takes NULL_NOW_ALLOWED or NULL_ALLOWED.
150 * 'type' => PARAM_TYPE, // Mandatory.
151 * 'choices' => array(1, 2, 3) // An array of accepted values.
155 * @return array Where keys are the property names.
157 protected static function define_properties() {
162 * Get the properties definition of this model..
166 final public static function properties_definition() {
174 $def = static::define_properties();
179 $def['timecreated'] = array(
183 $def['timemodified'] = array(
187 $def['usermodified'] = array(
192 // List of reserved property names. Mostly because we have methods (getters/setters) which would confict with them.
193 // Think about backwards compability before adding new ones here!
194 $reserved = array('errors', 'formatted_properties', 'records', 'records_select', 'property_default_value',
195 'property_error_message', 'sql_fields');
197 foreach ($def as $property => $definition) {
199 // Ensures that the null property is always set.
200 if (!array_key_exists('null', $definition)) {
201 $def[$property]['null'] = NULL_NOT_ALLOWED;
204 // Warn the developers when they are doing something wrong.
205 if ($CFG->debugdeveloper) {
206 if (!array_key_exists('type', $definition)) {
207 throw new coding_exception('Missing type for: ' . $property);
209 } else if (isset($definition['message']) && !($definition['message'] instanceof lang_string)) {
210 throw new coding_exception('Invalid error message for: ' . $property);
212 } else if (in_array($property, $reserved)) {
213 throw new coding_exception('This property cannot be defined: ' . $property);
223 * Gets all the formatted properties.
225 * Formatted properties are properties which have a format associated with them.
227 * @return array Keys are property names, values are property format names.
229 final public static function get_formatted_properties() {
230 $properties = static::properties_definition();
232 $formatted = array();
233 foreach ($properties as $property => $definition) {
234 $propertyformat = $property . 'format';
235 if ($definition['type'] == PARAM_RAW && array_key_exists($propertyformat, $properties)
236 && $properties[$propertyformat]['type'] == PARAM_INT) {
237 $formatted[$property] = $propertyformat;
245 * Gets the default value for a property.
247 * This assumes that the property exists.
249 * @param string $property The property name.
252 final protected static function get_property_default_value($property) {
253 $properties = static::properties_definition();
254 if (!isset($properties[$property]['default'])) {
257 return $properties[$property]['default'];
261 * Gets the error message for a property.
263 * This assumes that the property exists.
265 * @param string $property The property name.
266 * @return lang_string
268 final protected static function get_property_error_message($property) {
269 $properties = static::properties_definition();
270 if (!isset($properties[$property]['message'])) {
271 return new lang_string('invaliddata', 'error');
273 return $properties[$property]['message'];
277 * Returns whether or not a property was defined.
279 * @param string $property The property name.
282 final public static function has_property($property) {
283 $properties = static::properties_definition();
284 return isset($properties[$property]);
288 * Returns whether or not a property is required.
290 * By definition a property with a default value is not required.
292 * @param string $property The property name.
295 final public static function is_property_required($property) {
296 $properties = static::properties_definition();
297 return !array_key_exists('default', $properties[$property]);
301 * Populate this class with data from a DB record.
303 * Note that this does not use any custom setter because the data here is intended to
304 * represent what is stored in the database.
306 * @param \stdClass $record A DB record.
309 final public function from_record(stdClass $record) {
310 $record = (array) $record;
311 foreach ($record as $property => $value) {
312 $this->set($property, $value);
318 * Create a DB record from this class.
320 * Note that this does not use any custom getter because the data here is intended to
321 * represent what is stored in the database.
325 final public function to_record() {
326 $data = new stdClass();
327 $properties = static::properties_definition();
328 foreach ($properties as $property => $definition) {
329 $data->$property = $this->get($property);
335 * Load the data from the DB.
339 final public function read() {
342 if ($this->get_id() <= 0) {
343 throw new coding_exception('id is required to load');
345 $record = $DB->get_record(static::TABLE, array('id' => $this->get_id()), '*', MUST_EXIST);
346 $this->from_record($record);
348 // Validate the data as it comes from the database.
349 $this->validated = true;
355 * Hook to execute before a create.
357 * Please note that at this stage the data has already been validated and therefore
358 * any new data being set will not be validated before it is sent to the database.
360 * This is only intended to be used by child classes, do not put any logic here!
364 protected function before_create() {
368 * Insert a record in the DB.
372 final public function create() {
375 if ($this->get_id()) {
376 // The validation methods rely on the ID to know if we're updating or not, the ID should be
377 // falsy whenever we are creating an object.
378 throw new coding_exception('Cannot create an object that has an ID defined.');
381 if (!$this->is_valid()) {
382 throw new invalid_persistent_exception($this->get_errors());
385 // Before create hook.
386 $this->before_create();
388 // We can safely set those values bypassing the validation because we know what we're doing.
390 $this->set('timecreated', $now);
391 $this->set('timemodified', $now);
392 $this->set('usermodified', $USER->id);
394 $record = $this->to_record();
397 $id = $DB->insert_record(static::TABLE, $record);
398 $this->set('id', $id);
400 // We ensure that this is flagged as validated.
401 $this->validated = true;
403 // After create hook.
404 $this->after_create();
410 * Hook to execute after a create.
412 * This is only intended to be used by child classes, do not put any logic here!
416 protected function after_create() {
420 * Hook to execute before an update.
422 * Please note that at this stage the data has already been validated and therefore
423 * any new data being set will not be validated before it is sent to the database.
425 * This is only intended to be used by child classes, do not put any logic here!
429 protected function before_update() {
433 * Update the existing record in the DB.
435 * @return bool True on success.
437 final public function update() {
440 if ($this->get_id() <= 0) {
441 throw new coding_exception('id is required to update');
442 } else if (!$this->is_valid()) {
443 throw new invalid_persistent_exception($this->get_errors());
446 // Before update hook.
447 $this->before_update();
449 // We can safely set those values after the validation because we know what we're doing.
450 $this->set('timemodified', time());
451 $this->set('usermodified', $USER->id);
453 $record = $this->to_record();
454 unset($record->timecreated);
455 $record = (array) $record;
458 $result = $DB->update_record(static::TABLE, $record);
460 // We ensure that this is flagged as validated.
461 $this->validated = true;
463 // After update hook.
464 $this->after_update($result);
470 * Hook to execute after an update.
472 * This is only intended to be used by child classes, do not put any logic here!
474 * @param bool $result Whether or not the update was successful.
477 protected function after_update($result) {
481 * Hook to execute before a delete.
483 * This is only intended to be used by child classes, do not put any logic here!
487 protected function before_delete() {
491 * Delete an entry from the database.
493 * @return bool True on success.
495 final public function delete() {
498 if ($this->get_id() <= 0) {
499 throw new coding_exception('id is required to delete');
502 // Hook before delete.
503 $this->before_delete();
505 $result = $DB->delete_records(static::TABLE, array('id' => $this->get_id()));
507 // Hook after delete.
508 $this->after_delete($result);
510 // Reset the ID to avoid any confusion, this also invalidates the model's data.
519 * Hook to execute after a delete.
521 * This is only intended to be used by child classes, do not put any logic here!
523 * @param bool $result Whether or not the delete was successful.
526 protected function after_delete($result) {
530 * Hook to execute before the validation.
532 * This hook will not affect the validation results in any way but is useful to
533 * internally set properties which will need to be validated.
535 * This is only intended to be used by child classes, do not put any logic here!
539 protected function before_validate() {
543 * Validates the data.
545 * Developers can implement addition validation by defining a method as follows. Note that
546 * the method MUST return a lang_string() when there is an error, and true when the data is valid.
548 * protected function validate_propertyname($value) {
549 * if ($value !== 'My expected value') {
550 * return new lang_string('invaliddata', 'error');
555 * It is OK to use other properties in your custom validation methods when you need to, however note
556 * they might not have been validated yet, so try not to rely on them too much.
558 * Note that the validation methods should be protected. Validating just one field is not
559 * recommended because of the possible dependencies between one field and another,also the
560 * field ID can be used to check whether the object is being updated or created.
562 * When validating foreign keys the persistent should only check that the associated model
563 * exists. The validation methods should not be used to check for a change in that relationship.
564 * The API method setting the attributes on the model should be responsible for that.
565 * E.g. On a course model, the method validate_categoryid will check that the category exists.
566 * However, if a course can never be moved outside of its category it would be up to the calling
567 * code to ensure that the category ID will not be altered.
569 * @return array|true Returns true when the validation passed, or an array of properties with errors.
571 final public function validate() {
574 // Before validate hook.
575 $this->before_validate();
577 // If this object has not been validated yet.
578 if ($this->validated !== true) {
581 $properties = static::properties_definition();
582 foreach ($properties as $property => $definition) {
584 // Get the data, bypassing the potential custom getter which could alter the data.
585 $value = $this->get($property);
587 // Check if the property is required.
588 if ($value === null && static::is_property_required($property)) {
589 $errors[$property] = new lang_string('requiredelement', 'form');
593 // Check that type of value is respected.
595 if ($definition['type'] === PARAM_BOOL && $value === false) {
596 // Validate_param() does not like false with PARAM_BOOL, better to convert it to int.
599 validate_param($value, $definition['type'], $definition['null']);
600 } catch (invalid_parameter_exception $e) {
601 $errors[$property] = static::get_property_error_message($property);
605 // Check that the value is part of a list of allowed values.
606 if (isset($definition['choices']) && !in_array($value, $definition['choices'])) {
607 $errors[$property] = static::get_property_error_message($property);
611 // Call custom validation method.
612 $method = 'validate_' . $property;
613 if (method_exists($this, $method)) {
615 // Warn the developers when they are doing something wrong.
616 if ($CFG->debugdeveloper) {
617 $reflection = new ReflectionMethod($this, $method);
618 if (!$reflection->isProtected()) {
619 throw new coding_exception('The method ' . get_class($this) . '::'. $method . ' should be protected.');
623 $valid = $this->{$method}($value);
624 if ($valid !== true) {
625 if (!($valid instanceof lang_string)) {
626 throw new coding_exception('Unexpected error message.');
628 $errors[$property] = $valid;
634 $this->validated = true;
635 $this->errors = $errors;
638 return empty($this->errors) ? true : $this->errors;
642 * Returns whether or not the model is valid.
644 * @return boolean True when it is.
646 final public function is_valid() {
647 return $this->validate() === true;
651 * Returns the validation errors.
655 final public function get_errors() {
657 return $this->errors;
661 * Extract a record from a row of data.
663 * Most likely used in combination with {@link self::get_sql_fields()}. This method is
664 * simple enough to be used by non-persistent classes, keep that in mind when modifying it.
666 * e.g. persistent::extract_record($row, 'user'); should work.
668 * @param stdClass $row The row of data.
669 * @param string $prefix The prefix the data fields are prefixed with, defaults to the table name followed by underscore.
670 * @return stdClass The extracted data.
672 public static function extract_record($row, $prefix = null) {
673 if ($prefix === null) {
674 $prefix = static::TABLE . '_';
676 $prefixlength = strlen($prefix);
678 $data = new stdClass();
679 foreach ($row as $property => $value) {
680 if (strpos($property, $prefix) === 0) {
681 $propertyname = substr($property, $prefixlength);
682 $data->$propertyname = $value;
690 * Load a list of records.
692 * @param array $filters Filters to apply.
693 * @param string $sort Field to sort by.
694 * @param string $order Sort order.
695 * @param int $skip Limitstart.
696 * @param int $limit Number of rows to return.
698 * @return \tool_lp\persistent[]
700 public static function get_records($filters = array(), $sort = '', $order = 'ASC', $skip = 0, $limit = 0) {
705 $orderby = $sort . ' ' . $order;
708 $records = $DB->get_records(static::TABLE, $filters, $orderby, '*', $skip, $limit);
709 $instances = array();
711 foreach ($records as $record) {
712 $newrecord = new static(0, $record);
713 array_push($instances, $newrecord);
719 * Load a single record.
721 * @param array $filters Filters to apply.
722 * @return false|\tool_lp\persistent
724 public static function get_record($filters = array()) {
727 $record = $DB->get_record(static::TABLE, $filters);
728 return $record ? new static(0, $record) : false;
732 * Load a list of records based on a select query.
734 * @param string $select
735 * @param array $params
736 * @param string $sort
737 * @param string $fields
738 * @param int $limitfrom
739 * @param int $limitnum
740 * @return \tool_lp\persistent[]
742 public static function get_records_select($select, $params = null, $sort = '', $fields = '*', $limitfrom = 0, $limitnum = 0) {
745 $records = $DB->get_records_select(static::TABLE, $select, $params, $sort, $fields, $limitfrom, $limitnum);
747 // We return class instances.
748 $instances = array();
749 foreach ($records as $key => $record) {
750 $instances[$key] = new static(0, $record);
758 * Return the list of fields for use in a SELECT clause.
760 * Having the complete list of fields prefixed allows for multiple persistents to be fetched
761 * in a single query. Use {@link self::extract_record()} to extract the records from the query result.
763 * @param string $alias The alias used for the table.
764 * @param string $prefix The prefix to use for each field, defaults to the table name followed by underscore.
765 * @return string The SQL fragment.
767 public static function get_sql_fields($alias, $prefix = null) {
770 if ($prefix === null) {
771 $prefix = static::TABLE . '_';
774 // Get the properties and move ID to the top.
775 $properties = static::properties_definition();
776 $id = $properties['id'];
777 unset($properties['id']);
778 $properties = array('id' => $id) + $properties;
780 foreach ($properties as $property => $definition) {
781 $fields[] = $alias . '.' . $property . ' AS ' . $prefix . $property;
784 return implode(', ', $fields);
788 * Count a list of records.
790 * @param array $conditions An array of conditions.
793 public static function count_records(array $conditions = array()) {
796 $count = $DB->count_records(static::TABLE, $conditions);
801 * Count a list of records.
803 * @param string $select
804 * @param array $params
807 public static function count_records_select($select, $params = null) {
810 $count = $DB->count_records_select(static::TABLE, $select, $params);
815 * Check if a record exists by ID.
817 * @param int $id Record ID.
820 public static function record_exists($id) {
822 return $DB->record_exists(static::TABLE, array('id' => $id));
826 * Check if a records exists.
828 * @param string $select
829 * @param array $params
832 public static function record_exists_select($select, array $params = null) {
834 return $DB->record_exists_select(static::TABLE, $select, $params);