MDL-53655 tool_lp: Allow closures in default values of persistents
[moodle.git] / admin / tool / lp / classes / persistent.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
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.
8 //
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.
13 //
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/>.
17 /**
18  * Abstract class for tool_lp objects saved to the DB.
19  *
20  * @package    tool_lp
21  * @copyright  2015 Damyon Wiese
22  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
24 namespace tool_lp;
25 defined('MOODLE_INTERNAL') || die();
27 use coding_exception;
28 use invalid_parameter_exception;
29 use lang_string;
30 use ReflectionMethod;
31 use stdClass;
32 use renderer_base;
34 /**
35  * Abstract class for tool_lp objects saved to the DB.
36  *
37  * @copyright  2015 Damyon Wiese
38  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
39  */
40 abstract class persistent {
42     /** The table name. */
43     const TABLE = null;
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;
54     /**
55      * Create an instance of this class.
56      *
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()}.
59      */
60     public function __construct($id = 0, stdClass $record = null) {
61         if ($id > 0) {
62             $this->set('id', $id);
63             $this->read();
64         }
65         if (!empty($record)) {
66             $this->from_record($record);
67         }
68     }
70     /**
71      * Magic method to capture getters and setters.
72      *
73      * @param  string $method Callee.
74      * @param  array $arguments List of arguments.
75      * @return mixed
76      */
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]);
82         }
83         throw new coding_exception('Unexpected method call: ' . $method);
84     }
86     /**
87      * Data getter.
88      *
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.
94      *
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.
97      *
98      * @param  string $property The property name.
99      * @return mixed
100      */
101     final protected function get($property) {
102         if (!static::has_property($property)) {
103             throw new coding_exception('Unexpected property \'' . s($property) .'\' requested.');
104         }
105         if (!array_key_exists($property, $this->data) && !static::is_property_required($property)) {
106             $this->set($property, static::get_property_default_value($property));
107         }
108         return isset($this->data[$property]) ? $this->data[$property] : null;
109     }
111     /**
112      * Data setter.
113      *
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.
119      *
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.
122      *
123      * @param  string $property The property name.
124      * @param  mixed $value The value.
125      * @return mixed
126      */
127     final protected function set($property, $value) {
128         if (!static::has_property($property)) {
129             throw new coding_exception('Unexpected property \'' . s($property) .'\' requested.');
130         }
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;
134         }
135         $this->data[$property] = $value;
136     }
138     /**
139      * Return the custom definition of the properties of this model.
140      *
141      * Each property MUST be listed here.
142      *
143      * The result of this method is cached internally for the whole request.
144      *
145      * The 'default' value can be a Closure when its value may change during a single request.
146      * For example if the default value is based on a $CFG property, then it should be wrapped in a closure
147      * to avoid running into scenarios where the true value of $CFG is not reflected in the definition.
148      * Do not abuse closures as they obviously add some overhead.
149      *
150      * Examples:
151      *
152      * array(
153      *     'property_name' => array(
154      *         'default' => 'Default value',        // When not set, the property is considered as required.
155      *         'message' => new lang_string(...),   // Defaults to invalid data error message.
156      *         'null' => NULL_ALLOWED,              // Defaults to NULL_NOT_ALLOWED. Takes NULL_NOW_ALLOWED or NULL_ALLOWED.
157      *         'type' => PARAM_TYPE,                // Mandatory.
158      *         'choices' => array(1, 2, 3)          // An array of accepted values.
159      *     )
160      * )
161      *
162      * array(
163      *     'dynamic_property_name' => array(
164      *         'default' => function() {
165      *             return $CFG->something;
166      *         },
167      *         'type' => PARAM_INT,
168      *     )
169      * )
170      *
171      * @return array Where keys are the property names.
172      */
173     protected static function define_properties() {
174         return array();
175     }
177     /**
178      * Get the properties definition of this model..
179      *
180      * @return array
181      */
182     final public static function properties_definition() {
183         global $CFG;
185         static $def = null;
186         if ($def !== null) {
187             return $def;
188         }
190         $def = static::define_properties();
191         $def['id'] = array(
192             'default' => 0,
193             'type' => PARAM_INT,
194         );
195         $def['timecreated'] = array(
196             'default' => 0,
197             'type' => PARAM_INT,
198         );
199         $def['timemodified'] = array(
200             'default' => 0,
201             'type' => PARAM_INT
202         );
203         $def['usermodified'] = array(
204             'default' => 0,
205             'type' => PARAM_INT
206         );
208         // List of reserved property names. Mostly because we have methods (getters/setters) which would confict with them.
209         // Think about backwards compability before adding new ones here!
210         $reserved = array('errors', 'formatted_properties', 'records', 'records_select', 'property_default_value',
211             'property_error_message', 'sql_fields');
213         foreach ($def as $property => $definition) {
215             // Ensures that the null property is always set.
216             if (!array_key_exists('null', $definition)) {
217                 $def[$property]['null'] = NULL_NOT_ALLOWED;
218             }
220             // Warn the developers when they are doing something wrong.
221             if ($CFG->debugdeveloper) {
222                 if (!array_key_exists('type', $definition)) {
223                     throw new coding_exception('Missing type for: ' . $property);
225                 } else if (isset($definition['message']) && !($definition['message'] instanceof lang_string)) {
226                     throw new coding_exception('Invalid error message for: ' . $property);
228                 } else if (in_array($property, $reserved)) {
229                     throw new coding_exception('This property cannot be defined: ' . $property);
231                 }
232             }
233         }
235         return $def;
236     }
238     /**
239      * Gets all the formatted properties.
240      *
241      * Formatted properties are properties which have a format associated with them.
242      *
243      * @return array Keys are property names, values are property format names.
244      */
245     final public static function get_formatted_properties() {
246         $properties = static::properties_definition();
248         $formatted = array();
249         foreach ($properties as $property => $definition) {
250             $propertyformat = $property . 'format';
251             if ($definition['type'] == PARAM_RAW && array_key_exists($propertyformat, $properties)
252                     && $properties[$propertyformat]['type'] == PARAM_INT) {
253                 $formatted[$property] = $propertyformat;
254             }
255         }
257         return $formatted;
258     }
260     /**
261      * Gets the default value for a property.
262      *
263      * This assumes that the property exists.
264      *
265      * @param string $property The property name.
266      * @return mixed
267      */
268     final protected static function get_property_default_value($property) {
269         $properties = static::properties_definition();
270         if (!isset($properties[$property]['default'])) {
271             return null;
272         }
273         $value = $properties[$property]['default'];
274         if ($value instanceof \Closure) {
275             return $value();
276         }
277         return $value;
278     }
280     /**
281      * Gets the error message for a property.
282      *
283      * This assumes that the property exists.
284      *
285      * @param string $property The property name.
286      * @return lang_string
287      */
288     final protected static function get_property_error_message($property) {
289         $properties = static::properties_definition();
290         if (!isset($properties[$property]['message'])) {
291             return new lang_string('invaliddata', 'error');
292         }
293         return $properties[$property]['message'];
294     }
296     /**
297      * Returns whether or not a property was defined.
298      *
299      * @param  string $property The property name.
300      * @return boolean
301      */
302     final public static function has_property($property) {
303         $properties = static::properties_definition();
304         return isset($properties[$property]);
305     }
307     /**
308      * Returns whether or not a property is required.
309      *
310      * By definition a property with a default value is not required.
311      *
312      * @param  string $property The property name.
313      * @return boolean
314      */
315     final public static function is_property_required($property) {
316         $properties = static::properties_definition();
317         return !array_key_exists('default', $properties[$property]);
318     }
320     /**
321      * Populate this class with data from a DB record.
322      *
323      * Note that this does not use any custom setter because the data here is intended to
324      * represent what is stored in the database.
325      *
326      * @param \stdClass $record A DB record.
327      * @return persistent
328      */
329     final public function from_record(stdClass $record) {
330         $record = (array) $record;
331         foreach ($record as $property => $value) {
332             $this->set($property, $value);
333         }
334         return $this;
335     }
337     /**
338      * Create a DB record from this class.
339      *
340      * Note that this does not use any custom getter because the data here is intended to
341      * represent what is stored in the database.
342      *
343      * @return \stdClass
344      */
345     final public function to_record() {
346         $data = new stdClass();
347         $properties = static::properties_definition();
348         foreach ($properties as $property => $definition) {
349             $data->$property = $this->get($property);
350         }
351         return $data;
352     }
354     /**
355      * Load the data from the DB.
356      *
357      * @return persistent
358      */
359     final public function read() {
360         global $DB;
362         if ($this->get_id() <= 0) {
363             throw new coding_exception('id is required to load');
364         }
365         $record = $DB->get_record(static::TABLE, array('id' => $this->get_id()), '*', MUST_EXIST);
366         $this->from_record($record);
368         // Validate the data as it comes from the database.
369         $this->validated = true;
371         return $this;
372     }
374     /**
375      * Hook to execute before a create.
376      *
377      * Please note that at this stage the data has already been validated and therefore
378      * any new data being set will not be validated before it is sent to the database.
379      *
380      * This is only intended to be used by child classes, do not put any logic here!
381      *
382      * @return void
383      */
384     protected function before_create() {
385     }
387     /**
388      * Insert a record in the DB.
389      *
390      * @return persistent
391      */
392     final public function create() {
393         global $DB, $USER;
395         if ($this->get_id()) {
396             // The validation methods rely on the ID to know if we're updating or not, the ID should be
397             // falsy whenever we are creating an object.
398             throw new coding_exception('Cannot create an object that has an ID defined.');
399         }
401         if (!$this->is_valid()) {
402             throw new invalid_persistent_exception($this->get_errors());
403         }
405         // Before create hook.
406         $this->before_create();
408         // We can safely set those values bypassing the validation because we know what we're doing.
409         $now = time();
410         $this->set('timecreated', $now);
411         $this->set('timemodified', $now);
412         $this->set('usermodified', $USER->id);
414         $record = $this->to_record();
415         unset($record->id);
417         $id = $DB->insert_record(static::TABLE, $record);
418         $this->set('id', $id);
420         // We ensure that this is flagged as validated.
421         $this->validated = true;
423         // After create hook.
424         $this->after_create();
426         return $this;
427     }
429     /**
430      * Hook to execute after a create.
431      *
432      * This is only intended to be used by child classes, do not put any logic here!
433      *
434      * @return void
435      */
436     protected function after_create() {
437     }
439     /**
440      * Hook to execute before an update.
441      *
442      * Please note that at this stage the data has already been validated and therefore
443      * any new data being set will not be validated before it is sent to the database.
444      *
445      * This is only intended to be used by child classes, do not put any logic here!
446      *
447      * @return void
448      */
449     protected function before_update() {
450     }
452     /**
453      * Update the existing record in the DB.
454      *
455      * @return bool True on success.
456      */
457     final public function update() {
458         global $DB, $USER;
460         if ($this->get_id() <= 0) {
461             throw new coding_exception('id is required to update');
462         } else if (!$this->is_valid()) {
463             throw new invalid_persistent_exception($this->get_errors());
464         }
466         // Before update hook.
467         $this->before_update();
469         // We can safely set those values after the validation because we know what we're doing.
470         $this->set('timemodified', time());
471         $this->set('usermodified', $USER->id);
473         $record = $this->to_record();
474         unset($record->timecreated);
475         $record = (array) $record;
477         // Save the record.
478         $result = $DB->update_record(static::TABLE, $record);
480         // We ensure that this is flagged as validated.
481         $this->validated = true;
483         // After update hook.
484         $this->after_update($result);
486         return $result;
487     }
489     /**
490      * Hook to execute after an update.
491      *
492      * This is only intended to be used by child classes, do not put any logic here!
493      *
494      * @param bool $result Whether or not the update was successful.
495      * @return void
496      */
497     protected function after_update($result) {
498     }
500     /**
501      * Hook to execute before a delete.
502      *
503      * This is only intended to be used by child classes, do not put any logic here!
504      *
505      * @return void
506      */
507     protected function before_delete() {
508     }
510     /**
511      * Delete an entry from the database.
512      *
513      * @return bool True on success.
514      */
515     final public function delete() {
516         global $DB;
518         if ($this->get_id() <= 0) {
519             throw new coding_exception('id is required to delete');
520         }
522         // Hook before delete.
523         $this->before_delete();
525         $result = $DB->delete_records(static::TABLE, array('id' => $this->get_id()));
527         // Hook after delete.
528         $this->after_delete($result);
530         // Reset the ID to avoid any confusion, this also invalidates the model's data.
531         if ($result) {
532             $this->set('id', 0);
533         }
535         return $result;
536     }
538     /**
539      * Hook to execute after a delete.
540      *
541      * This is only intended to be used by child classes, do not put any logic here!
542      *
543      * @param bool $result Whether or not the delete was successful.
544      * @return void
545      */
546     protected function after_delete($result) {
547     }
549     /**
550      * Hook to execute before the validation.
551      *
552      * This hook will not affect the validation results in any way but is useful to
553      * internally set properties which will need to be validated.
554      *
555      * This is only intended to be used by child classes, do not put any logic here!
556      *
557      * @return void
558      */
559     protected function before_validate() {
560     }
562     /**
563      * Validates the data.
564      *
565      * Developers can implement addition validation by defining a method as follows. Note that
566      * the method MUST return a lang_string() when there is an error, and true when the data is valid.
567      *
568      * protected function validate_propertyname($value) {
569      *     if ($value !== 'My expected value') {
570      *         return new lang_string('invaliddata', 'error');
571      *     }
572      *     return true
573      * }
574      *
575      * It is OK to use other properties in your custom validation methods when you need to, however note
576      * they might not have been validated yet, so try not to rely on them too much.
577      *
578      * Note that the validation methods should be protected. Validating just one field is not
579      * recommended because of the possible dependencies between one field and another,also the
580      * field ID can be used to check whether the object is being updated or created.
581      *
582      * When validating foreign keys the persistent should only check that the associated model
583      * exists. The validation methods should not be used to check for a change in that relationship.
584      * The API method setting the attributes on the model should be responsible for that.
585      * E.g. On a course model, the method validate_categoryid will check that the category exists.
586      * However, if a course can never be moved outside of its category it would be up to the calling
587      * code to ensure that the category ID will not be altered.
588      *
589      * @return array|true Returns true when the validation passed, or an array of properties with errors.
590      */
591     final public function validate() {
592         global $CFG;
594         // Before validate hook.
595         $this->before_validate();
597         // If this object has not been validated yet.
598         if ($this->validated !== true) {
600             $errors = array();
601             $properties = static::properties_definition();
602             foreach ($properties as $property => $definition) {
604                 // Get the data, bypassing the potential custom getter which could alter the data.
605                 $value = $this->get($property);
607                 // Check if the property is required.
608                 if ($value === null && static::is_property_required($property)) {
609                     $errors[$property] = new lang_string('requiredelement', 'form');
610                     continue;
611                 }
613                 // Check that type of value is respected.
614                 try {
615                     if ($definition['type'] === PARAM_BOOL && $value === false) {
616                         // Validate_param() does not like false with PARAM_BOOL, better to convert it to int.
617                         $value = 0;
618                     }
619                     validate_param($value, $definition['type'], $definition['null']);
620                 } catch (invalid_parameter_exception $e) {
621                     $errors[$property] = static::get_property_error_message($property);
622                     continue;
623                 }
625                 // Check that the value is part of a list of allowed values.
626                 if (isset($definition['choices']) && !in_array($value, $definition['choices'])) {
627                     $errors[$property] = static::get_property_error_message($property);
628                     continue;
629                 }
631                 // Call custom validation method.
632                 $method = 'validate_' . $property;
633                 if (method_exists($this, $method)) {
635                     // Warn the developers when they are doing something wrong.
636                     if ($CFG->debugdeveloper) {
637                         $reflection = new ReflectionMethod($this, $method);
638                         if (!$reflection->isProtected()) {
639                             throw new coding_exception('The method ' . get_class($this) . '::'. $method . ' should be protected.');
640                         }
641                     }
643                     $valid = $this->{$method}($value);
644                     if ($valid !== true) {
645                         if (!($valid instanceof lang_string)) {
646                             throw new coding_exception('Unexpected error message.');
647                         }
648                         $errors[$property] = $valid;
649                         continue;
650                     }
651                 }
652             }
654             $this->validated = true;
655             $this->errors = $errors;
656         }
658         return empty($this->errors) ? true : $this->errors;
659     }
661     /**
662      * Returns whether or not the model is valid.
663      *
664      * @return boolean True when it is.
665      */
666     final public function is_valid() {
667         return $this->validate() === true;
668     }
670     /**
671      * Returns the validation errors.
672      *
673      * @return array
674      */
675     final public function get_errors() {
676         $this->validate();
677         return $this->errors;
678     }
680     /**
681      * Extract a record from a row of data.
682      *
683      * Most likely used in combination with {@link self::get_sql_fields()}. This method is
684      * simple enough to be used by non-persistent classes, keep that in mind when modifying it.
685      *
686      * e.g. persistent::extract_record($row, 'user'); should work.
687      *
688      * @param stdClass $row The row of data.
689      * @param string $prefix The prefix the data fields are prefixed with, defaults to the table name followed by underscore.
690      * @return stdClass The extracted data.
691      */
692     public static function extract_record($row, $prefix = null) {
693         if ($prefix === null) {
694             $prefix = static::TABLE . '_';
695         }
696         $prefixlength = strlen($prefix);
698         $data = new stdClass();
699         foreach ($row as $property => $value) {
700             if (strpos($property, $prefix) === 0) {
701                 $propertyname = substr($property, $prefixlength);
702                 $data->$propertyname = $value;
703             }
704         }
706         return $data;
707     }
709     /**
710      * Load a list of records.
711      *
712      * @param array $filters Filters to apply.
713      * @param string $sort Field to sort by.
714      * @param string $order Sort order.
715      * @param int $skip Limitstart.
716      * @param int $limit Number of rows to return.
717      *
718      * @return \tool_lp\persistent[]
719      */
720     public static function get_records($filters = array(), $sort = '', $order = 'ASC', $skip = 0, $limit = 0) {
721         global $DB;
723         $orderby = '';
724         if (!empty($sort)) {
725             $orderby = $sort . ' ' . $order;
726         }
728         $records = $DB->get_records(static::TABLE, $filters, $orderby, '*', $skip, $limit);
729         $instances = array();
731         foreach ($records as $record) {
732             $newrecord = new static(0, $record);
733             array_push($instances, $newrecord);
734         }
735         return $instances;
736     }
738     /**
739      * Load a single record.
740      *
741      * @param array $filters Filters to apply.
742      * @return false|\tool_lp\persistent
743      */
744     public static function get_record($filters = array()) {
745         global $DB;
747         $record = $DB->get_record(static::TABLE, $filters);
748         return $record ? new static(0, $record) : false;
749     }
751     /**
752      * Load a list of records based on a select query.
753      *
754      * @param string $select
755      * @param array $params
756      * @param string $sort
757      * @param string $fields
758      * @param int $limitfrom
759      * @param int $limitnum
760      * @return \tool_lp\persistent[]
761      */
762     public static function get_records_select($select, $params = null, $sort = '', $fields = '*', $limitfrom = 0, $limitnum = 0) {
763         global $DB;
765         $records = $DB->get_records_select(static::TABLE, $select, $params, $sort, $fields, $limitfrom, $limitnum);
767         // We return class instances.
768         $instances = array();
769         foreach ($records as $key => $record) {
770             $instances[$key] = new static(0, $record);
771         }
773         return $instances;
775     }
777     /**
778      * Return the list of fields for use in a SELECT clause.
779      *
780      * Having the complete list of fields prefixed allows for multiple persistents to be fetched
781      * in a single query. Use {@link self::extract_record()} to extract the records from the query result.
782      *
783      * @param string $alias The alias used for the table.
784      * @param string $prefix The prefix to use for each field, defaults to the table name followed by underscore.
785      * @return string The SQL fragment.
786      */
787     public static function get_sql_fields($alias, $prefix = null) {
788         $fields = array();
790         if ($prefix === null) {
791             $prefix = static::TABLE . '_';
792         }
794         // Get the properties and move ID to the top.
795         $properties = static::properties_definition();
796         $id = $properties['id'];
797         unset($properties['id']);
798         $properties = array('id' => $id) + $properties;
800         foreach ($properties as $property => $definition) {
801             $fields[] = $alias . '.' . $property . ' AS ' . $prefix . $property;
802         }
804         return implode(', ', $fields);
805     }
807     /**
808      * Count a list of records.
809      *
810      * @param array $conditions An array of conditions.
811      * @return int
812      */
813     public static function count_records(array $conditions = array()) {
814         global $DB;
816         $count = $DB->count_records(static::TABLE, $conditions);
817         return $count;
818     }
820     /**
821      * Count a list of records.
822      *
823      * @param string $select
824      * @param array $params
825      * @return int
826      */
827     public static function count_records_select($select, $params = null) {
828         global $DB;
830         $count = $DB->count_records_select(static::TABLE, $select, $params);
831         return $count;
832     }
834     /**
835      * Check if a record exists by ID.
836      *
837      * @param int $id Record ID.
838      * @return bool
839      */
840     public static function record_exists($id) {
841         global $DB;
842         return $DB->record_exists(static::TABLE, array('id' => $id));
843     }
845     /**
846      * Check if a records exists.
847      *
848      * @param string $select
849      * @param array $params
850      * @return bool
851      */
852     public static function record_exists_select($select, array $params = null) {
853         global $DB;
854         return $DB->record_exists_select(static::TABLE, $select, $params);
855     }