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/>.
16
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 */
26
27defined('MOODLE_INTERNAL') || die();
28
29
30/**
31 * Trigger for the new data_object api.
32 *
33 * See data_object::__constructor
34 */
35define('DATA_OBJECT_FETCH_BY_KEY',  2);
36
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 */
46abstract class data_object {
47
48    /* @var string Table that the class maps to in the database */
49    public $table;
50
51    /* @var array Array of required table fields, must start with 'id'. */
52    public $required_fields = array('id');
53
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();
60
61    /* @var Array of unique fields, used in where clauses and constructor */
62    public $unique_fields = array();
63
64    /* @var int The primary key */
65    public $id;
66
67
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) {
87
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        }
91
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        }
97
98        // If fetch is false, do not load from database
99        if ($fetch === false) {
100            self::set_properties($this, $params);
101            return;
102        }
103
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        }
119
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    }
131
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    }
151
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    }
164
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    }
177
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    }
198
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();
210
211        $classvars = (array)$instance;
212        $params    = (array)$params;
213
214        $wheresql = array();
215
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        }
228
229        if (empty($wheresql)) {
230            $wheresql = '';
231        } else {
232            $wheresql = implode("AND", $wheresql);
233        }
234
235        global $DB;
236        if ($datas = $DB->get_records_select($table, $wheresql, $dbparams)) {
237
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;
245
246        } else {
247
248            return false;
249        }
250    }
251
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;
259
260        if (empty($this->id)) {
261            debugging('Can not update data object, no id!');
262            return false;
263        }
264
265        $data = $this->get_record_data();
266
267        $DB->update_record($this->table, $data);
268
269        $this->notify_changed(false);
270        return true;
271    }
272
273    /**
274     * Deletes this object from the database.
275     *
276     * @return bool success
277     */
278    public function delete() {
279        global $DB;
280
281        if (empty($this->id)) {
282            debugging('Can not delete data object, no id!');
283            return false;
284        }
285
286        $data = $this->get_record_data();
287
288        if ($DB->delete_records($this->table, array('id'=>$this->id))) {
289            $this->notify_changed(true);
290            return true;
291
292        } else {
293            return false;
294        }
295    }
296
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();
304
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    }
316
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;
326
327        if (!empty($this->id)) {
328            debugging("Data object already exists!");
329            return false;
330        }
331
332        $data = $this->get_record_data();
333
334        $this->id = $DB->insert_record($this->table, $data);
335
336        // set all object properties from real db data
337        $this->update_from_db();
338
339        $this->notify_changed(false);
340        return $this->id;
341    }
342
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        }
361
362        self::set_properties($this, $params);
363
364        return true;
365    }
366
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    }
383
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    }
393}
394