Merge branch 'MDL-65051-master' of git://github.com/lameze/moodle
[moodle.git] / completion / data_object.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  * Course completion critieria aggregation
19  *
20  * @package core_completion
21  * @category completion
22  * @copyright 2009 Catalyst IT Ltd
23  * @author Aaron Barnes <aaronb@catalyst.net.nz>
24  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25  */
27 defined('MOODLE_INTERNAL') || die();
30 /**
31  * Trigger for the new data_object api.
32  *
33  * See data_object::__constructor
34  */
35 define('DATA_OBJECT_FETCH_BY_KEY',  2);
37 /**
38  * A data abstraction object that holds methods and attributes
39  *
40  * @package core_completion
41  * @category completion
42  * @copyright 2009 Catalyst IT Ltd
43  * @author Aaron Barnes <aaronb@catalyst.net.nz>
44  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
45  */
46 abstract class data_object {
48     /* @var string Table that the class maps to in the database */
49     public $table;
51     /* @var array Array of required table fields, must start with 'id'. */
52     public $required_fields = array('id');
54     /**
55      * Array of optional fields with default values - usually long text information that is not always needed.
56      * If you want to create an instance without optional fields use: new data_object($only_required_fields, false);
57      * @var array
58      */
59     public $optional_fields = array();
61     /* @var Array of unique fields, used in where clauses and constructor */
62     public $unique_fields = array();
64     /* @var int The primary key */
65     public $id;
68     /**
69      * Constructor. Optionally (and by default) attempts to fetch corresponding row from DB.
70      *
71      * If $fetch is not false, there are a few different things that can happen:
72      * - true:
73      *   load corresponding row from the database, using $params as the WHERE clause
74      *
75      * - DATA_OBJECT_FETCH_BY_KEY:
76      *  load corresponding row from the database, using only the $id in the WHERE clause (if set),
77      *  otherwise using the columns listed in $this->unique_fields.
78      *
79      * - array():
80      *   load corresponding row from the database, using the columns listed in this array
81      *   in the WHERE clause
82      *
83      * @param   array   $params     required parameters and their values for this data object
84      * @param   mixed   $fetch      if false, do not attempt to fetch from the database, otherwise see notes
85      */
86     public function __construct($params = null, $fetch = true) {
88         if (is_object($params)) {
89             throw new coding_exception('data_object params should be in the form of an array, not an object');
90         }
92         // If no params given, apply defaults for optional fields
93         if (empty($params) || !is_array($params)) {
94             self::set_properties($this, $this->optional_fields);
95             return;
96         }
98         // If fetch is false, do not load from database
99         if ($fetch === false) {
100             self::set_properties($this, $params);
101             return;
102         }
104         // Compose where clause only from fields in unique_fields
105         if ($fetch === DATA_OBJECT_FETCH_BY_KEY && !empty($this->unique_fields)) {
106             if (empty($params['id'])) {
107                 $where = array_intersect_key($params, array_flip($this->unique_fields));
108             }
109             else {
110                 $where = array('id' => $params['id']);
111             }
112         // Compose where clause from given field names
113         } else if (is_array($fetch) && !empty($fetch)) {
114             $where = array_intersect_key($params, array_flip($fetch));
115         // Use entire params array for where clause
116         } else {
117             $where = $params;
118         }
120         // Attempt to load from database
121         if ($data = $this->fetch($where)) {
122             // Apply data from database, then data sent to constructor
123             self::set_properties($this, $data);
124             self::set_properties($this, $params);
125         } else {
126             // Apply defaults for optional fields, then data from constructor
127             self::set_properties($this, $this->optional_fields);
128             self::set_properties($this, $params);
129         }
130     }
132     /**
133      * Makes sure all the optional fields are loaded.
134      *
135      * If id present (==instance exists in db) fetches data from db.
136      * Defaults are used for new instances.
137      */
138     public function load_optional_fields() {
139         global $DB;
140         foreach ($this->optional_fields as $field=>$default) {
141             if (property_exists($this, $field)) {
142                 continue;
143             }
144             if (empty($this->id)) {
145                 $this->$field = $default;
146             } else {
147                 $this->$field = $DB->get_field($this->table, $field, array('id', $this->id));
148             }
149         }
150     }
152     /**
153      * Finds and returns a data_object instance based on params.
154      *
155      * This function MUST be overridden by all deriving classes.
156      *
157      * @param array $params associative arrays varname => value
158      * @throws coding_exception This function MUST be overridden
159      * @return data_object instance  of data_object or false if none found.
160      */
161     public static function fetch($params) {
162         throw new coding_exception('fetch() method needs to be overridden in each subclass of data_object');
163     }
165     /**
166      * Finds and returns all data_object instances based on params.
167      *
168      * This function MUST be overridden by all deriving classes.
169      *
170      * @param array $params associative arrays varname => value
171      * @throws coding_exception This function MUST be overridden
172      * @return array array of data_object instances or false if none found.
173      */
174     public static function fetch_all($params) {
175         throw new coding_exception('fetch_all() method needs to be overridden in each subclass of data_object');
176     }
178     /**
179      * Factory method - uses the parameters to retrieve matching instance from the DB.
180      *
181      * @final
182      * @param string $table The table name to fetch from
183      * @param string $classname The class that you want the result instantiated as
184      * @param array $params Any params required to select the desired row
185      * @return object Instance of $classname or false.
186      */
187     protected static function fetch_helper($table, $classname, $params) {
188         if ($instances = self::fetch_all_helper($table, $classname, $params)) {
189             if (count($instances) > 1) {
190                 // we should not tolerate any errors here - problems might appear later
191                 print_error('morethanonerecordinfetch','debug');
192             }
193             return reset($instances);
194         } else {
195             return false;
196         }
197     }
199     /**
200      * Factory method - uses the parameters to retrieve all matching instances from the DB.
201      *
202      * @final
203      * @param string $table The table name to fetch from
204      * @param string $classname The class that you want the result instantiated as
205      * @param array $params Any params required to select the desired row
206      * @return mixed array of object instances or false if not found
207      */
208     public static function fetch_all_helper($table, $classname, $params) {
209         $instance = new $classname();
211         $classvars = (array)$instance;
212         $params    = (array)$params;
214         $wheresql = array();
216         $dbparams = array();
217         foreach ($params as $var=>$value) {
218             if (!in_array($var, $instance->required_fields) and !array_key_exists($var, $instance->optional_fields)) {
219                 continue;
220             }
221             if (is_null($value)) {
222                 $wheresql[] = " $var IS NULL ";
223             } else {
224                 $wheresql[] = " $var = ? ";
225                 $dbparams[] = $value;
226             }
227         }
229         if (empty($wheresql)) {
230             $wheresql = '';
231         } else {
232             $wheresql = implode("AND", $wheresql);
233         }
235         global $DB;
236         if ($datas = $DB->get_records_select($table, $wheresql, $dbparams)) {
238             $result = array();
239             foreach($datas as $data) {
240                 $instance = new $classname();
241                 self::set_properties($instance, $data);
242                 $result[$instance->id] = $instance;
243             }
244             return $result;
246         } else {
248             return false;
249         }
250     }
252     /**
253      * Updates this object in the Database, based on its object variables. ID must be set.
254      *
255      * @return bool success
256      */
257     public function update() {
258         global $DB;
260         if (empty($this->id)) {
261             debugging('Can not update data object, no id!');
262             return false;
263         }
265         $data = $this->get_record_data();
267         $DB->update_record($this->table, $data);
269         $this->notify_changed(false);
270         return true;
271     }
273     /**
274      * Deletes this object from the database.
275      *
276      * @return bool success
277      */
278     public function delete() {
279         global $DB;
281         if (empty($this->id)) {
282             debugging('Can not delete data object, no id!');
283             return false;
284         }
286         $data = $this->get_record_data();
288         if ($DB->delete_records($this->table, array('id'=>$this->id))) {
289             $this->notify_changed(true);
290             return true;
292         } else {
293             return false;
294         }
295     }
297     /**
298      * Returns object with fields and values that are defined in database
299      *
300      * @return stdClass
301      */
302     public function get_record_data() {
303         $data = new stdClass();
305         foreach ($this as $var=>$value) {
306             if (in_array($var, $this->required_fields) or array_key_exists($var, $this->optional_fields)) {
307                 if (is_object($value) or is_array($value)) {
308                     debugging("Incorrect property '$var' found when inserting data object");
309                 } else {
310                     $data->$var = $value;
311                 }
312             }
313         }
314         return $data;
315     }
317     /**
318      * Records this object in the Database, sets its id to the returned value, and returns that value.
319      * If successful this function also fetches the new object data from database and stores it
320      * in object properties.
321      *
322      * @return int PK ID if successful, false otherwise
323      */
324     public function insert() {
325         global $DB;
327         if (!empty($this->id)) {
328             debugging("Data object already exists!");
329             return false;
330         }
332         $data = $this->get_record_data();
334         $this->id = $DB->insert_record($this->table, $data);
336         // set all object properties from real db data
337         $this->update_from_db();
339         $this->notify_changed(false);
340         return $this->id;
341     }
343     /**
344      * Using this object's id field, fetches the matching record in the DB, and looks at
345      * each variable in turn. If the DB has different data, the db's data is used to update
346      * the object. This is different from the update() function, which acts on the DB record
347      * based on the object.
348      *
349      * @return bool True for success, false otherwise.
350      */
351     public function update_from_db() {
352         if (empty($this->id)) {
353             debugging("The object could not be used in its state to retrieve a matching record from the DB, because its id field is not set.");
354             return false;
355         }
356         global $DB;
357         if (!$params = $DB->get_record($this->table, array('id' => $this->id))) {
358             debugging("Object with this id:{$this->id} does not exist in table:{$this->table}, can not update from db!");
359             return false;
360         }
362         self::set_properties($this, $params);
364         return true;
365     }
367     /**
368      * Given an associated array or object, cycles through each key/variable
369      * and assigns the value to the corresponding variable in this object.
370      *
371      * @final
372      * @param data_object $instance
373      * @param array $params
374      */
375     public static function set_properties(&$instance, $params) {
376         $params = (array) $params;
377         foreach ($params as $var => $value) {
378             if (in_array($var, $instance->required_fields) or array_key_exists($var, $instance->optional_fields)) {
379                 $instance->$var = $value;
380             }
381         }
382     }
384     /**
385      * Called immediately after the object data has been inserted, updated, or
386      * deleted in the database. Default does nothing, can be overridden to
387      * hook in special behaviour.
388      *
389      * @param bool $deleted Set this to true if it has been deleted.
390      */
391     public function notify_changed($deleted) {
392     }