Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

Differences Between: [Versions 310 and 311] [Versions 311 and 401] [Versions 311 and 402] [Versions 311 and 403] [Versions 39 and 311]

   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   * Experimental pdo database class
  19   *
  20   * @package    core_dml
  21   * @copyright  2008 Andrei Bautu
  22   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  defined('MOODLE_INTERNAL') || die();
  26  
  27  require_once (__DIR__.'/moodle_database.php');
  28  require_once (__DIR__.'/pdo_moodle_recordset.php');
  29  
  30  /**
  31   * Experimental pdo database class
  32   *
  33   * @package    core_dml
  34   * @copyright  2008 Andrei Bautu
  35   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  36   */
  37  abstract class pdo_moodle_database extends moodle_database {
  38  
  39      protected $pdb;
  40      protected $lastError = null;
  41  
  42      /**
  43       * Constructor - instantiates the database, specifying if it's external (connect to other systems) or no (Moodle DB)
  44       *               note this has effect to decide if prefix checks must be performed or no
  45       * @param bool true means external database used
  46       */
  47      public function __construct($external=false) {
  48          parent::__construct($external);
  49      }
  50  
  51      /**
  52       * Connect to db
  53       * Must be called before other methods.
  54       * @param string $dbhost The database host.
  55       * @param string $dbuser The database username.
  56       * @param string $dbpass The database username's password.
  57       * @param string $dbname The name of the database being connected to.
  58       * @param mixed $prefix string means moodle db prefix, false used for external databases where prefix not used
  59       * @param array $dboptions driver specific options
  60       * @return bool success
  61       */
  62      public function connect($dbhost, $dbuser, $dbpass, $dbname, $prefix, array $dboptions=null) {
  63          $driverstatus = $this->driver_installed();
  64  
  65          if ($driverstatus !== true) {
  66              throw new dml_exception('dbdriverproblem', $driverstatus);
  67          }
  68  
  69          $this->store_settings($dbhost, $dbuser, $dbpass, $dbname, $prefix, $dboptions);
  70  
  71          try{
  72              $this->pdb = new PDO($this->get_dsn(), $this->dbuser, $this->dbpass, $this->get_pdooptions());
  73              // generic PDO settings to match adodb's default; subclasses can change this in configure_dbconnection
  74              $this->pdb->setAttribute(PDO::ATTR_CASE, PDO::CASE_LOWER);
  75              $this->pdb->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
  76              $this->configure_dbconnection();
  77              return true;
  78          } catch (PDOException $ex) {
  79              throw new dml_connection_exception($ex->getMessage());
  80              return false;
  81          }
  82      }
  83  
  84      /**
  85       * Returns the driver-dependent DSN for PDO based on members stored by connect.
  86       * Must be called after connect (or after $dbname, $dbhost, etc. members have been set).
  87       * @return string driver-dependent DSN
  88       */
  89      abstract protected function get_dsn();
  90  
  91      /**
  92       * Returns the driver-dependent connection attributes for PDO based on members stored by connect.
  93       * Must be called after $dbname, $dbhost, etc. members have been set.
  94       * @return array A key=>value array of PDO driver-specific connection options
  95       */
  96      protected function get_pdooptions() {
  97          return array(PDO::ATTR_PERSISTENT => !empty($this->dboptions['dbpersist']));
  98      }
  99  
 100      protected function configure_dbconnection() {
 101          //TODO: not needed preconfigure_dbconnection() stuff for PDO drivers?
 102      }
 103  
 104      /**
 105       * Returns general database library name
 106       * Note: can be used before connect()
 107       * @return string db type pdo, native
 108       */
 109      protected function get_dblibrary() {
 110          return 'pdo';
 111      }
 112  
 113      /**
 114       * Returns localised database type name
 115       * Note: can be used before connect()
 116       * @return string
 117       */
 118      public function get_name() {
 119          return get_string('pdo'.$this->get_dbtype(), 'install');
 120      }
 121  
 122      /**
 123       * Returns localised database configuration help.
 124       * Note: can be used before connect()
 125       * @return string
 126       */
 127      public function get_configuration_help() {
 128          return get_string('pdo'.$this->get_dbtype().'help', 'install');
 129      }
 130  
 131      /**
 132       * Returns database server info array
 133       * @return array Array containing 'description' and 'version' info
 134       */
 135      public function get_server_info() {
 136          $result = array();
 137          try {
 138              $result['description'] = $this->pdb->getAttribute(PDO::ATTR_SERVER_INFO);
 139          } catch(PDOException $ex) {}
 140          try {
 141              $result['version'] = $this->pdb->getAttribute(PDO::ATTR_SERVER_VERSION);
 142          } catch(PDOException $ex) {}
 143          return $result;
 144      }
 145  
 146      /**
 147       * Returns supported query parameter types
 148       * @return int bitmask of accepted SQL_PARAMS_*
 149       */
 150      protected function allowed_param_types() {
 151          return SQL_PARAMS_QM | SQL_PARAMS_NAMED;
 152      }
 153  
 154      /**
 155       * Returns last error reported by database engine.
 156       * @return string error message
 157       */
 158      public function get_last_error() {
 159          return $this->lastError;
 160      }
 161  
 162      /**
 163       * Function to print/save/ignore debugging messages related to SQL queries.
 164       */
 165      protected function debug_query($sql, $params = null) {
 166          echo '<hr /> (', $this->get_dbtype(), '): ',  htmlentities($sql, ENT_QUOTES, 'UTF-8');
 167          if($params) {
 168              echo ' (parameters ';
 169              print_r($params);
 170              echo ')';
 171          }
 172          echo '<hr />';
 173      }
 174  
 175      /**
 176       * Do NOT use in code, to be used by database_manager only!
 177       * @param string|array $sql query
 178       * @param array|null $tablenames an array of xmldb table names affected by this request.
 179       * @return bool true
 180       * @throws ddl_change_structure_exception A DDL specific exception is thrown for any errors.
 181       */
 182      public function change_database_structure($sql, $tablenames = null) {
 183          $this->get_manager(); // Includes DDL exceptions classes ;-)
 184          $sqls = (array)$sql;
 185  
 186          try {
 187              foreach ($sqls as $sql) {
 188                  $result = true;
 189                  $this->query_start($sql, null, SQL_QUERY_STRUCTURE);
 190  
 191                  try {
 192                      $this->pdb->exec($sql);
 193                  } catch (PDOException $ex) {
 194                      $this->lastError = $ex->getMessage();
 195                      $result = false;
 196                  }
 197                  $this->query_end($result);
 198              }
 199          } catch (ddl_change_structure_exception $e) {
 200              $this->reset_caches($tablenames);
 201              throw $e;
 202          }
 203  
 204          $this->reset_caches($tablenames);
 205          return true;
 206      }
 207  
 208      public function delete_records_select($table, $select, array $params=null) {
 209          $sql = "DELETE FROM {{$table}}";
 210          if ($select) {
 211              $sql .= " WHERE $select";
 212          }
 213          return $this->execute($sql, $params);
 214      }
 215  
 216      /**
 217       * Factory method that creates a recordset for return by a query. The generic pdo_moodle_recordset
 218       * class should fit most cases, but pdo_moodle_database subclasses can override this method to return
 219       * a subclass of pdo_moodle_recordset.
 220       * @param object $sth instance of PDOStatement
 221       * @return object instance of pdo_moodle_recordset
 222       */
 223      protected function create_recordset($sth) {
 224          return new pdo_moodle_recordset($sth);
 225      }
 226  
 227      /**
 228       * Execute general sql query. Should be used only when no other method suitable.
 229       * Do NOT use this to make changes in db structure, use database_manager methods instead!
 230       * @param string $sql query
 231       * @param array $params query parameters
 232       * @return bool success
 233       */
 234      public function execute($sql, array $params=null) {
 235          list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
 236  
 237          $result = true;
 238          $this->query_start($sql, $params, SQL_QUERY_UPDATE);
 239  
 240          try {
 241              $sth = $this->pdb->prepare($sql);
 242              $sth->execute($params);
 243          } catch (PDOException $ex) {
 244              $this->lastError = $ex->getMessage();
 245              $result = false;
 246          }
 247  
 248          $this->query_end($result);
 249          return $result;
 250      }
 251  
 252      /**
 253       * Get a number of records as an moodle_recordset.  $sql must be a complete SQL query.
 254       * Since this method is a little less readable, use of it should be restricted to
 255       * code where it's possible there might be large datasets being returned.  For known
 256       * small datasets use get_records_sql - it leads to simpler code.
 257       *
 258       * The return type is like:
 259       * @see function get_recordset.
 260       *
 261       * @param string $sql the SQL select query to execute.
 262       * @param array $params array of sql parameters
 263       * @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set).
 264       * @param int $limitnum return a subset comprising this many records (optional, required if $limitfrom is set).
 265       * @return moodle_recordset instance
 266       */
 267      public function get_recordset_sql($sql, array $params=null, $limitfrom=0, $limitnum=0) {
 268  
 269          $result = true;
 270  
 271          list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
 272          $sql = $this->get_limit_clauses($sql, $limitfrom, $limitnum);
 273          $this->query_start($sql, $params, SQL_QUERY_SELECT);
 274  
 275          try {
 276              $sth = $this->pdb->prepare($sql);
 277              $sth->execute($params);
 278              $result = $this->create_recordset($sth);
 279          } catch (PDOException $ex) {
 280              $this->lastError = $ex->getMessage();
 281              $result = false;
 282          }
 283  
 284          $this->query_end($result);
 285          return $result;
 286      }
 287  
 288      /**
 289       * Selects rows and return values of first column as array.
 290       *
 291       * @param string $sql The SQL query
 292       * @param array $params array of sql parameters
 293       * @return array of values
 294       */
 295      public function get_fieldset_sql($sql, array $params=null) {
 296          $rs = $this->get_recordset_sql($sql, $params);
 297          if (!$rs->valid()) {
 298              $rs->close(); // Not going to iterate (but exit), close rs
 299              return false;
 300          }
 301          $result = array();
 302          foreach($rs as $value) {
 303              $result[] = reset($value);
 304          }
 305          $rs->close();
 306          return $result;
 307      }
 308  
 309      /**
 310       * Get a number of records as an array of objects.
 311       *
 312       * Return value is like:
 313       * @see function get_records.
 314       *
 315       * @param string $sql the SQL select query to execute. The first column of this SELECT statement
 316       *   must be a unique value (usually the 'id' field), as it will be used as the key of the
 317       *   returned array.
 318       * @param array $params array of sql parameters
 319       * @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set).
 320       * @param int $limitnum return a subset comprising this many records (optional, required if $limitfrom is set).
 321       * @return array of objects, or empty array if no records were found, or false if an error occurred.
 322       */
 323      public function get_records_sql($sql, array $params=null, $limitfrom=0, $limitnum=0) {
 324          global $CFG;
 325  
 326          $rs = $this->get_recordset_sql($sql, $params, $limitfrom, $limitnum);
 327          if (!$rs->valid()) {
 328              $rs->close(); // Not going to iterate (but exit), close rs
 329              return false;
 330          }
 331          $objects = array();
 332          foreach($rs as $value) {
 333              $key = reset($value);
 334              if ($CFG->debugdeveloper && array_key_exists($key, $objects)) {
 335                  debugging("Did you remember to make the first column something unique in your call to get_records? Duplicate value '$key' found in column first column of '$sql'.", DEBUG_DEVELOPER);
 336              }
 337              $objects[$key] = (object)$value;
 338          }
 339          $rs->close();
 340          return $objects;
 341      }
 342  
 343      /**
 344       * Insert new record into database, as fast as possible, no safety checks, lobs not supported.
 345       * @param string $table name
 346       * @param mixed $params data record as object or array
 347       * @param bool $returnit return it of inserted record
 348       * @param bool $bulk true means repeated inserts expected
 349       * @param bool $customsequence true if 'id' included in $params, disables $returnid
 350       * @return bool|int true or new id
 351       */
 352      public function insert_record_raw($table, $params, $returnid=true, $bulk=false, $customsequence=false) {
 353          if (!is_array($params)) {
 354              $params = (array)$params;
 355          }
 356  
 357          if ($customsequence) {
 358              if (!isset($params['id'])) {
 359                  throw new coding_exception('moodle_database::insert_record_raw() id field must be specified if custom sequences used.');
 360              }
 361              $returnid = false;
 362          } else {
 363              unset($params['id']);
 364          }
 365  
 366          if (empty($params)) {
 367              throw new coding_exception('moodle_database::insert_record_raw() no fields found.');
 368          }
 369  
 370          $fields = implode(',', array_keys($params));
 371          $qms    = array_fill(0, count($params), '?');
 372          $qms    = implode(',', $qms);
 373  
 374          $sql = "INSERT INTO {{$table}} ($fields) VALUES($qms)";
 375          if (!$this->execute($sql, $params)) {
 376              return false;
 377          }
 378          if (!$returnid) {
 379              return true;
 380          }
 381          if ($id = $this->pdb->lastInsertId()) {
 382              return (int)$id;
 383          }
 384          return false;
 385      }
 386  
 387      /**
 388       * Insert a record into a table and return the "id" field if required,
 389       * Some conversions and safety checks are carried out. Lobs are supported.
 390       * If the return ID isn't required, then this just reports success as true/false.
 391       * $data is an object containing needed data
 392       * @param string $table The database table to be inserted into
 393       * @param object|array $dataobject A data object with values for one or more fields in the record
 394       * @param bool $returnid Should the id of the newly created record entry be returned? If this option is not requested then true/false is returned.
 395       * @param bool $bulk true means repeated inserts expected
 396       * @return bool|int true or new id
 397       */
 398      public function insert_record($table, $dataobject, $returnid=true, $bulk=false) {
 399          $dataobject = (array)$dataobject;
 400  
 401          $columns = $this->get_columns($table);
 402          if (empty($columns)) {
 403              throw new dml_exception('ddltablenotexist', $table);
 404          }
 405  
 406          $cleaned = array();
 407  
 408          foreach ($dataobject as $field=>$value) {
 409              if ($field === 'id') {
 410                  continue;
 411              }
 412              if (!isset($columns[$field])) {
 413                  continue;
 414              }
 415              $column = $columns[$field];
 416              if (is_bool($value)) {
 417                  $value = (int)$value; // prevent "false" problems
 418              }
 419              $cleaned[$field] = $value;
 420          }
 421  
 422          if (empty($cleaned)) {
 423              return false;
 424          }
 425  
 426          return $this->insert_record_raw($table, $cleaned, $returnid, $bulk);
 427      }
 428  
 429      /**
 430       * Update record in database, as fast as possible, no safety checks, lobs not supported.
 431       * @param string $table name
 432       * @param mixed $params data record as object or array
 433       * @param bool true means repeated updates expected
 434       * @return bool success
 435       */
 436      public function update_record_raw($table, $params, $bulk=false) {
 437          $params = (array)$params;
 438  
 439          if (!isset($params['id'])) {
 440              throw new coding_exception('moodle_database::update_record_raw() id field must be specified.');
 441          }
 442          $id = $params['id'];
 443          unset($params['id']);
 444  
 445          if (empty($params)) {
 446              throw new coding_exception('moodle_database::update_record_raw() no fields found.');
 447          }
 448  
 449          $sets = array();
 450          foreach ($params as $field=>$value) {
 451              $sets[] = "$field = ?";
 452          }
 453  
 454          $params[] = $id; // last ? in WHERE condition
 455  
 456          $sets = implode(',', $sets);
 457          $sql = "UPDATE {{$table}} SET $sets WHERE id=?";
 458          return $this->execute($sql, $params);
 459      }
 460  
 461      /**
 462       * Update a record in a table
 463       *
 464       * $dataobject is an object containing needed data
 465       * Relies on $dataobject having a variable "id" to
 466       * specify the record to update
 467       *
 468       * @param string $table The database table to be checked against.
 469       * @param object $dataobject An object with contents equal to fieldname=>fieldvalue. Must have an entry for 'id' to map to the table specified.
 470       * @param bool true means repeated updates expected
 471       * @return bool success
 472       */
 473      public function update_record($table, $dataobject, $bulk=false) {
 474          $dataobject = (array)$dataobject;
 475  
 476          $columns = $this->get_columns($table);
 477          $cleaned = array();
 478  
 479          foreach ($dataobject as $field=>$value) {
 480              if (!isset($columns[$field])) {
 481                  continue;
 482              }
 483              if (is_bool($value)) {
 484                  $value = (int)$value; // prevent "false" problems
 485              }
 486              $cleaned[$field] = $value;
 487          }
 488  
 489          return $this->update_record_raw($table, $cleaned, $bulk);
 490      }
 491  
 492      /**
 493       * Set a single field in every table row where the select statement evaluates to true.
 494       *
 495       * @param string $table The database table to be checked against.
 496       * @param string $newfield the field to set.
 497       * @param string $newvalue the value to set the field to.
 498       * @param string $select A fragment of SQL to be used in a where clause in the SQL call.
 499       * @param array $params array of sql parameters
 500       * @return bool success
 501       */
 502      public function set_field_select($table, $newfield, $newvalue, $select, array $params=null) {
 503          if ($select) {
 504              $select = "WHERE $select";
 505          }
 506          if (is_null($params)) {
 507              $params = array();
 508          }
 509          list($select, $params, $type) = $this->fix_sql_params($select, $params);
 510  
 511          if (is_bool($newvalue)) {
 512              $newvalue = (int)$newvalue; // prevent "false" problems
 513          }
 514          if (is_null($newvalue)) {
 515              $newfield = "$newfield = NULL";
 516          } else {
 517              // make sure SET and WHERE clauses use the same type of parameters,
 518              // because we don't support different types in the same query
 519              switch($type) {
 520              case SQL_PARAMS_NAMED:
 521                  $newfield = "$newfield = :newvalueforupdate";
 522                  $params['newvalueforupdate'] = $newvalue;
 523                  break;
 524              case SQL_PARAMS_QM:
 525                  $newfield = "$newfield = ?";
 526                  array_unshift($params, $newvalue);
 527                  break;
 528              default:
 529                  $this->lastError = __FILE__ . ' LINE: ' . __LINE__ . '.';
 530                  print_error(unknowparamtype, 'error', '', $this->lastError);
 531              }
 532          }
 533          $sql = "UPDATE {{$table}} SET $newfield $select";
 534          return $this->execute($sql, $params);
 535      }
 536  
 537      public function sql_concat() {
 538          print_error('TODO');
 539      }
 540  
 541      public function sql_concat_join($separator="' '", $elements=array()) {
 542          print_error('TODO');
 543      }
 544  
 545      /**
 546       * Return SQL for performing group concatenation on given field/expression
 547       *
 548       * @param string $field
 549       * @param string $separator
 550       * @param string $sort
 551       * @return string
 552       */
 553      public function sql_group_concat(string $field, string $separator = ', ', string $sort = ''): string {
 554          return ''; // TODO.
 555      }
 556  
 557      protected function begin_transaction() {
 558          $this->query_start('', NULL, SQL_QUERY_AUX);
 559          try {
 560              $this->pdb->beginTransaction();
 561          } catch(PDOException $ex) {
 562              $this->lastError = $ex->getMessage();
 563          }
 564          $this->query_end($result);
 565      }
 566  
 567      protected function commit_transaction() {
 568          $this->query_start('', NULL, SQL_QUERY_AUX);
 569  
 570          try {
 571              $this->pdb->commit();
 572          } catch(PDOException $ex) {
 573              $this->lastError = $ex->getMessage();
 574          }
 575          $this->query_end($result);
 576      }
 577  
 578      protected function rollback_transaction() {
 579          $this->query_start('', NULL, SQL_QUERY_AUX);
 580  
 581          try {
 582              $this->pdb->rollBack();
 583          } catch(PDOException $ex) {
 584              $this->lastError = $ex->getMessage();
 585          }
 586          $this->query_end($result);
 587      }
 588  
 589      /**
 590       * Import a record into a table, id field is required.
 591       * Basic safety checks only. Lobs are supported.
 592       * @param string $table name of database table to be inserted into
 593       * @param mixed $dataobject object or array with fields in the record
 594       * @return bool success
 595       */
 596      public function import_record($table, $dataobject) {
 597          $dataobject = (object)$dataobject;
 598  
 599          $columns = $this->get_columns($table);
 600          $cleaned = array();
 601          foreach ($dataobject as $field=>$value) {
 602              if (!isset($columns[$field])) {
 603                  continue;
 604              }
 605              $cleaned[$field] = $value;
 606          }
 607  
 608          return $this->insert_record_raw($table, $cleaned, false, true, true);
 609      }
 610  
 611      /**
 612       * Called before each db query.
 613       *
 614       * Overridden to ensure $this->lastErorr is reset each query
 615       *
 616       * @param string $sql
 617       * @param array|null $params An array of parameters.
 618       * @param int $type type of query
 619       * @param mixed $extrainfo driver specific extra information
 620       * @return void
 621       */
 622      protected function query_start($sql, ?array $params, $type, $extrainfo=null) {
 623          $this->lastError = null;
 624          parent::query_start($sql, $params, $type, $extrainfo);
 625      }
 626  }