4fae7c34da18ec797bbb4d2db1860cf223c52940
[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      * Example:
144      *
145      * array(
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.
152      *     )
153      * )
154      *
155      * @return array Where keys are the property names.
156      */
157     protected static function define_properties() {
158         return array();
159     }
161     /**
162      * Get the properties definition of this model..
163      *
164      * @return array
165      */
166     final public static function properties_definition() {
167         global $CFG;
169         static $def = null;
170         if ($def !== null) {
171             return $def;
172         }
174         $def = static::define_properties();
175         $def['id'] = array(
176             'default' => 0,
177             'type' => PARAM_INT,
178         );
179         $def['timecreated'] = array(
180             'default' => 0,
181             'type' => PARAM_INT,
182         );
183         $def['timemodified'] = array(
184             'default' => 0,
185             'type' => PARAM_INT
186         );
187         $def['usermodified'] = array(
188             'default' => 0,
189             'type' => PARAM_INT
190         );
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;
202             }
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);
215                 }
216             }
217         }
219         return $def;
220     }
222     /**
223      * Gets all the formatted properties.
224      *
225      * Formatted properties are properties which have a format associated with them.
226      *
227      * @return array Keys are property names, values are property format names.
228      */
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;
238             }
239         }
241         return $formatted;
242     }
244     /**
245      * Gets the default value for a property.
246      *
247      * This assumes that the property exists.
248      *
249      * @param string $property The property name.
250      * @return mixed
251      */
252     final protected static function get_property_default_value($property) {
253         $properties = static::properties_definition();
254         if (!isset($properties[$property]['default'])) {
255             return null;
256         }
257         return $properties[$property]['default'];
258     }
260     /**
261      * Gets the error message for a property.
262      *
263      * This assumes that the property exists.
264      *
265      * @param string $property The property name.
266      * @return lang_string
267      */
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');
272         }
273         return $properties[$property]['message'];
274     }
276     /**
277      * Returns whether or not a property was defined.
278      *
279      * @param  string $property The property name.
280      * @return boolean
281      */
282     final public static function has_property($property) {
283         $properties = static::properties_definition();
284         return isset($properties[$property]);
285     }
287     /**
288      * Returns whether or not a property is required.
289      *
290      * By definition a property with a default value is not required.
291      *
292      * @param  string $property The property name.
293      * @return boolean
294      */
295     final public static function is_property_required($property) {
296         $properties = static::properties_definition();
297         return !array_key_exists('default', $properties[$property]);
298     }
300     /**
301      * Populate this class with data from a DB record.
302      *
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.
305      *
306      * @param \stdClass $record A DB record.
307      * @return persistent
308      */
309     final public function from_record(stdClass $record) {
310         $record = (array) $record;
311         foreach ($record as $property => $value) {
312             $this->set($property, $value);
313         }
314         return $this;
315     }
317     /**
318      * Create a DB record from this class.
319      *
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.
322      *
323      * @return \stdClass
324      */
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);
330         }
331         return $data;
332     }
334     /**
335      * Load the data from the DB.
336      *
337      * @return persistent
338      */
339     final public function read() {
340         global $DB;
342         if ($this->get_id() <= 0) {
343             throw new coding_exception('id is required to load');
344         }
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;
351         return $this;
352     }
354     /**
355      * Hook to execute before a create.
356      *
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.
359      *
360      * This is only intended to be used by child classes, do not put any logic here!
361      *
362      * @return void
363      */
364     protected function before_create() {
365     }
367     /**
368      * Insert a record in the DB.
369      *
370      * @return persistent
371      */
372     final public function create() {
373         global $DB, $USER;
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.');
379         }
381         if (!$this->is_valid()) {
382             throw new invalid_persistent_exception($this->get_errors());
383         }
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.
389         $now = time();
390         $this->set('timecreated', $now);
391         $this->set('timemodified', $now);
392         $this->set('usermodified', $USER->id);
394         $record = $this->to_record();
395         unset($record->id);
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();
406         return $this;
407     }
409     /**
410      * Hook to execute after a create.
411      *
412      * This is only intended to be used by child classes, do not put any logic here!
413      *
414      * @return void
415      */
416     protected function after_create() {
417     }
419     /**
420      * Hook to execute before an update.
421      *
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.
424      *
425      * This is only intended to be used by child classes, do not put any logic here!
426      *
427      * @return void
428      */
429     protected function before_update() {
430     }
432     /**
433      * Update the existing record in the DB.
434      *
435      * @return bool True on success.
436      */
437     final public function update() {
438         global $DB, $USER;
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());
444         }
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;
457         // Save the 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);
466         return $result;
467     }
469     /**
470      * Hook to execute after an update.
471      *
472      * This is only intended to be used by child classes, do not put any logic here!
473      *
474      * @param bool $result Whether or not the update was successful.
475      * @return void
476      */
477     protected function after_update($result) {
478     }
480     /**
481      * Hook to execute before a delete.
482      *
483      * This is only intended to be used by child classes, do not put any logic here!
484      *
485      * @return void
486      */
487     protected function before_delete() {
488     }
490     /**
491      * Delete an entry from the database.
492      *
493      * @return bool True on success.
494      */
495     final public function delete() {
496         global $DB;
498         if ($this->get_id() <= 0) {
499             throw new coding_exception('id is required to delete');
500         }
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.
511         if ($result) {
512             $this->set('id', 0);
513         }
515         return $result;
516     }
518     /**
519      * Hook to execute after a delete.
520      *
521      * This is only intended to be used by child classes, do not put any logic here!
522      *
523      * @param bool $result Whether or not the delete was successful.
524      * @return void
525      */
526     protected function after_delete($result) {
527     }
529     /**
530      * Hook to execute before the validation.
531      *
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.
534      *
535      * This is only intended to be used by child classes, do not put any logic here!
536      *
537      * @return void
538      */
539     protected function before_validate() {
540     }
542     /**
543      * Validates the data.
544      *
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.
547      *
548      * protected function validate_propertyname($value) {
549      *     if ($value !== 'My expected value') {
550      *         return new lang_string('invaliddata', 'error');
551      *     }
552      *     return true
553      * }
554      *
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.
557      *
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.
561      *
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.
568      *
569      * @return array|true Returns true when the validation passed, or an array of properties with errors.
570      */
571     final public function validate() {
572         global $CFG;
574         // Before validate hook.
575         $this->before_validate();
577         // If this object has not been validated yet.
578         if ($this->validated !== true) {
580             $errors = array();
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');
590                     continue;
591                 }
593                 // Check that type of value is respected.
594                 try {
595                     if ($definition['type'] === PARAM_BOOL && $value === false) {
596                         // Validate_param() does not like false with PARAM_BOOL, better to convert it to int.
597                         $value = 0;
598                     }
599                     validate_param($value, $definition['type'], $definition['null']);
600                 } catch (invalid_parameter_exception $e) {
601                     $errors[$property] = static::get_property_error_message($property);
602                     continue;
603                 }
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);
608                     continue;
609                 }
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.');
620                         }
621                     }
623                     $valid = $this->{$method}($value);
624                     if ($valid !== true) {
625                         if (!($valid instanceof lang_string)) {
626                             throw new coding_exception('Unexpected error message.');
627                         }
628                         $errors[$property] = $valid;
629                         continue;
630                     }
631                 }
632             }
634             $this->validated = true;
635             $this->errors = $errors;
636         }
638         return empty($this->errors) ? true : $this->errors;
639     }
641     /**
642      * Returns whether or not the model is valid.
643      *
644      * @return boolean True when it is.
645      */
646     final public function is_valid() {
647         return $this->validate() === true;
648     }
650     /**
651      * Returns the validation errors.
652      *
653      * @return array
654      */
655     final public function get_errors() {
656         $this->validate();
657         return $this->errors;
658     }
660     /**
661      * Extract a record from a row of data.
662      *
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.
665      *
666      * e.g. persistent::extract_record($row, 'user'); should work.
667      *
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.
671      */
672     public static function extract_record($row, $prefix = null) {
673         if ($prefix === null) {
674             $prefix = static::TABLE . '_';
675         }
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;
683             }
684         }
686         return $data;
687     }
689     /**
690      * Load a list of records.
691      *
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.
697      *
698      * @return \tool_lp\persistent[]
699      */
700     public static function get_records($filters = array(), $sort = '', $order = 'ASC', $skip = 0, $limit = 0) {
701         global $DB;
703         $orderby = '';
704         if (!empty($sort)) {
705             $orderby = $sort . ' ' . $order;
706         }
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);
714         }
715         return $instances;
716     }
718     /**
719      * Load a single record.
720      *
721      * @param array $filters Filters to apply.
722      * @return false|\tool_lp\persistent
723      */
724     public static function get_record($filters = array()) {
725         global $DB;
727         $record = $DB->get_record(static::TABLE, $filters);
728         return $record ? new static(0, $record) : false;
729     }
731     /**
732      * Load a list of records based on a select query.
733      *
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[]
741      */
742     public static function get_records_select($select, $params = null, $sort = '', $fields = '*', $limitfrom = 0, $limitnum = 0) {
743         global $DB;
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);
751         }
753         return $instances;
755     }
757     /**
758      * Return the list of fields for use in a SELECT clause.
759      *
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.
762      *
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.
766      */
767     public static function get_sql_fields($alias, $prefix = null) {
768         $fields = array();
770         if ($prefix === null) {
771             $prefix = static::TABLE . '_';
772         }
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;
782         }
784         return implode(', ', $fields);
785     }
787     /**
788      * Count a list of records.
789      *
790      * @param array $conditions An array of conditions.
791      * @return int
792      */
793     public static function count_records(array $conditions = array()) {
794         global $DB;
796         $count = $DB->count_records(static::TABLE, $conditions);
797         return $count;
798     }
800     /**
801      * Count a list of records.
802      *
803      * @param string $select
804      * @param array $params
805      * @return int
806      */
807     public static function count_records_select($select, $params = null) {
808         global $DB;
810         $count = $DB->count_records_select(static::TABLE, $select, $params);
811         return $count;
812     }
814     /**
815      * Check if a record exists by ID.
816      *
817      * @param int $id Record ID.
818      * @return bool
819      */
820     public static function record_exists($id) {
821         global $DB;
822         return $DB->record_exists(static::TABLE, array('id' => $id));
823     }
825     /**
826      * Check if a records exists.
827      *
828      * @param string $select
829      * @param array $params
830      * @return bool
831      */
832     public static function record_exists_select($select, array $params = null) {
833         global $DB;
834         return $DB->record_exists_select(static::TABLE, $select, $params);
835     }