[ Index ]

PHP Cross Reference of YOURLS

title

Body

[close]

/includes/Database/ -> YDB.php (source)

   1  <?php
   2  
   3  /**
   4   * Aura SQL wrapper for YOURLS that creates the almighty YDB object.
   5   *
   6   * A fine example of a "class that knows too much" (see https://en.wikipedia.org/wiki/God_object)
   7   *
   8   * Note to plugin authors: you most likely SHOULD NOT use directly methods and properties of this class. Use instead
   9   * function wrappers (e.g. don't use $ydb->option, or $ydb->set_option(), use yourls_*_options() functions instead).
  10   *
  11   * @since 1.7.3
  12   */
  13  
  14  namespace YOURLS\Database;
  15  
  16  use Aura\Sql\ExtendedPdo;
  17  use PDO;
  18  use PDOStatement;
  19  
  20  class YDB extends ExtendedPdo {
  21  
  22      /**
  23       * Debug mode, default false
  24       * @var bool
  25       */
  26      protected bool $debug = false;
  27  
  28      /**
  29       * Page context (ie "infos", "bookmark", "plugins"...)
  30       * @var string
  31       */
  32      protected string $context = '';
  33  
  34      /**
  35       * Information related to a short URL keyword (e.g. timestamp, long URL, ...)
  36       *
  37       * @var array
  38       *
  39       */
  40      protected array $infos = [];
  41  
  42      /**
  43       * Is YOURLS installed and ready to run?
  44       * @var bool
  45       */
  46      protected bool $installed = false;
  47  
  48      /**
  49       * Options
  50       * @var array
  51       */
  52      protected array $option = [];
  53  
  54      /**
  55       * Plugin admin pages information
  56       * @var array
  57       */
  58      protected array $plugin_pages = [];
  59  
  60      /**
  61       * Plugin information
  62       * @var array
  63       */
  64      protected array $plugins = [];
  65  
  66      /**
  67       * Are we emulating prepare statements ?
  68       * @var bool
  69       */
  70      protected bool $is_emulate_prepare;
  71  
  72      /**
  73       * Bypass shunt filter? See fetch_wrapper()
  74       * @var bool
  75       */
  76      private bool $bypass_shunt_filter = false;
  77  
  78      /**
  79       * @since 1.7.3
  80       * @param string $dsn     The data source name
  81       * @param string $user    The username
  82       * @param string $pass    The password
  83       * @param array  $options Driver-specific options
  84       */
  85      public function __construct($dsn, $user, $pass, $options) {
  86          parent::__construct($dsn, $user, $pass, $options);
  87      }
  88  
  89      /**
  90       * Init everything needed
  91       *
  92       * Everything we need to set up is done here in init(), not in the constructor, so even
  93       * when the connection fails (e.g. config error or DB dead), the constructor has worked,
  94       * and we have a $ydb object properly instantiated (and for instance yourls_die() can
  95       * correctly die, even if using $ydb methods)
  96       *
  97       * @since  1.7.3
  98       * @return void
  99       */
 100      public function init() {
 101          $this->connect_to_DB();
 102  
 103          $this->set_emulate_state();
 104  
 105          $this->start_profiler();
 106      }
 107  
 108      /**
 109       * Check if we emulate prepare statements, and set bool flag accordingly
 110       *
 111       * Check if current driver can PDO::getAttribute(PDO::ATTR_EMULATE_PREPARES)
 112       * Some combinations of PHP/MySQL don't support this function. See
 113       * https://travis-ci.org/YOURLS/YOURLS/jobs/271423782#L481
 114       *
 115       * @since  1.7.3
 116       * @return void
 117       */
 118      public function set_emulate_state() {
 119          try {
 120              $this->is_emulate_prepare = $this->getAttribute(PDO::ATTR_EMULATE_PREPARES);
 121          } catch (\PDOException $e) {
 122              $this->is_emulate_prepare = false;
 123          }
 124      }
 125  
 126      /**
 127       * Get emulate status
 128       *
 129       * @since  1.7.3
 130       * @return bool
 131       */
 132      public function get_emulate_state() {
 133          return $this->is_emulate_prepare;
 134      }
 135  
 136      /**
 137       * Initiate real connection to DB server
 138       *
 139       * This is to check that the server is running and/or the config is OK
 140       *
 141       * @since  1.7.3
 142       * @return void
 143       * @throws \PDOException
 144       */
 145      public function connect_to_DB() {
 146          try {
 147              list($dsn, $_user, $_pwd, $_opt, $_queries) = $this->args;
 148              $this->connect($dsn);
 149          } catch ( \Exception $e ) {
 150              $this->dead_or_error($e);
 151          }
 152      }
 153  
 154      /**
 155       * Die with an error message
 156       *
 157       * @since  1.7.3
 158       *
 159       * @param \Exception $exception
 160       *
 161       * @return void
 162       */
 163      public function dead_or_error(\Exception $exception) {
 164          // Use any /user/db_error.php file
 165          $file = YOURLS_USERDIR . '/db_error.php';
 166          if(file_exists($file)) {
 167              if(yourls_include_file_sandbox( $file ) === true) {
 168                  die();
 169              }
 170          }
 171  
 172          $message  = yourls__( 'Incorrect DB config, or could not connect to DB' );
 173          $message .= '<br/>' . get_class($exception) .': ' . $exception->getMessage();
 174          yourls_die( yourls__( $message ), yourls__( 'Fatal error' ), 503 );
 175          die();
 176  
 177      }
 178  
 179      /**
 180       * Start a Message Logger
 181       *
 182       * @since  1.7.3
 183       * @see    includes/Database/Logger.php
 184       * @see    includes/Database/Profiler.php
 185       * @return void
 186       */
 187      public function start_profiler() {
 188          // Instantiate a custom logger and make it the profiler
 189          $yourls_logger = new Logger();
 190          $profiler = new Profiler($yourls_logger);
 191          $this->setProfiler($profiler);
 192  
 193          /* By default, make "query" the log level. This way, each internal logging triggered
 194           * by Aura SQL will be a "query", and logging triggered by yourls_debug_log() will be
 195           * a "debug". See includes/functions-debug.php:yourls_debug_log()
 196           */
 197          $profiler->setLoglevel('query');
 198      }
 199  
 200      /**
 201       * @param string $context
 202       * @return void
 203       */
 204      public function set_html_context($context) {
 205          $this->context = $context;
 206      }
 207  
 208      /**
 209       * @return string
 210       */
 211      public function get_html_context() {
 212          return $this->context;
 213      }
 214  
 215      // Options low level functions, see \YOURLS\Database\Options
 216  
 217      /**
 218       * @param string $name
 219       * @param mixed  $value
 220       * @return void
 221       */
 222      public function set_option($name, $value) {
 223          $this->option[$name] = $value;
 224      }
 225  
 226      /**
 227       * @param  string $name
 228       * @return bool
 229       */
 230      public function has_option($name) {
 231          return array_key_exists($name, $this->option);
 232      }
 233  
 234      /**
 235       * @param  string $name
 236       * @return string
 237       */
 238      public function get_option($name) {
 239          return $this->option[$name];
 240      }
 241  
 242      /**
 243       * @param string $name
 244       * @return void
 245       */
 246      public function delete_option($name) {
 247          unset($this->option[$name]);
 248      }
 249  
 250  
 251      // Infos (related to keyword) low level functions
 252  
 253      /**
 254       * @param string $keyword
 255       * @param mixed  $infos
 256       * @return void
 257       */
 258      public function set_infos($keyword, $infos) {
 259          $this->infos[$keyword] = $infos;
 260      }
 261  
 262      /**
 263       * @param  string $keyword
 264       * @return bool
 265       */
 266      public function has_infos($keyword) {
 267          return array_key_exists($keyword, $this->infos);
 268      }
 269  
 270      /**
 271       * @param  string $keyword
 272       * @return array
 273       */
 274      public function get_infos($keyword) {
 275          return $this->infos[$keyword];
 276      }
 277  
 278      /**
 279       * @param string $keyword
 280       * @return void
 281       */
 282      public function delete_infos($keyword) {
 283          if (isset($this->infos[$keyword])) {
 284              unset($this->infos[$keyword]);
 285          }
 286      }
 287  
 288      /**
 289       * @param string $keyword
 290       * @param mixed  $infos
 291       * @return void
 292       */
 293      public function update_infos_if_exists($keyword, $infos) {
 294          if ($this->has_infos($keyword) && $this->infos[$keyword]) {
 295              $this->infos[$keyword] = array_merge($this->infos[$keyword], $infos);
 296          }
 297      }
 298  
 299      /**
 300       * @todo: infos & options are working the same way here. Abstract this.
 301       */
 302  
 303  
 304      // Plugin low level functions, see functions-plugins.php
 305  
 306      /**
 307       * @return array
 308       */
 309      public function get_plugins() {
 310          return $this->plugins;
 311      }
 312  
 313      /**
 314       * @param array $plugins
 315       * @return void
 316       */
 317      public function set_plugins(array $plugins) {
 318          $this->plugins = $plugins;
 319      }
 320  
 321      /**
 322       * @param string $plugin  plugin filename
 323       * @return void
 324       */
 325      public function add_plugin($plugin) {
 326          $this->plugins[] = $plugin;
 327      }
 328  
 329      /**
 330       * @param string $plugin  plugin filename
 331       * @return void
 332       */
 333      public function remove_plugin($plugin) {
 334          unset($this->plugins[$plugin]);
 335      }
 336  
 337  
 338      // Plugin Pages low level functions, see functions-plugins.php
 339  
 340      /**
 341       * @return array
 342       */
 343      public function get_plugin_pages() {
 344          return is_array( $this->plugin_pages ) ? $this->plugin_pages : [];
 345      }
 346  
 347      /**
 348       * @param array $pages
 349       * @return void
 350       */
 351      public function set_plugin_pages(array $pages) {
 352          $this->plugin_pages = $pages;
 353      }
 354  
 355      /**
 356       * @param string   $slug
 357       * @param string   $title
 358       * @param callable $function
 359       * @return void
 360       */
 361      public function add_plugin_page( $slug, $title, $function ) {
 362          $this->plugin_pages[ $slug ] = [
 363              'slug'     => $slug,
 364              'title'    => $title,
 365              'function' => $function,
 366          ];
 367      }
 368  
 369      /**
 370       * @param string $slug
 371       * @return void
 372       */
 373      public function remove_plugin_page( $slug ) {
 374          unset( $this->plugin_pages[ $slug ] );
 375      }
 376  
 377      /**
 378       * Return count of SQL queries performed
 379       *
 380       * @since  1.7.3
 381       * @return int
 382       */
 383      public function get_num_queries() {
 384          return count( (array) $this->get_queries() );
 385      }
 386  
 387      /**
 388       * Return SQL queries performed
 389       *
 390       * @since  1.7.3
 391       * @return array
 392       */
 393      public function get_queries() {
 394          $queries = $this->getProfiler()->getLogger()->getMessages();
 395  
 396          // Only keep messages that start with "SQL "
 397          $queries = array_filter($queries, function($query) {return substr( $query, 0, 4 ) === "SQL ";});
 398  
 399          return $queries;
 400      }
 401  
 402      /**
 403       * Set YOURLS installed state
 404       *
 405       * @since  1.7.3
 406       * @param  bool $bool
 407       * @return void
 408       */
 409      public function set_installed($bool) {
 410          $this->installed = $bool;
 411      }
 412  
 413      /**
 414       * Get YOURLS installed state
 415       *
 416       * @since  1.7.3
 417       * @return bool
 418       */
 419      public function is_installed() {
 420          return $this->installed;
 421      }
 422  
 423      /**
 424       * Return MySQL version
 425       *
 426       * @since  1.7.3
 427       * @return string
 428       */
 429      public function mysql_version() {
 430          return $this->pdo->getAttribute(PDO::ATTR_SERVER_VERSION);
 431      }
 432  
 433      /**
 434       * Fetch the number of affected rows
 435       *
 436       * @since 1.10.4
 437       * @param string $statement SQL statement to execute
 438       * @param array  $values    Optional. Values to bind to the statement. Default empty array.
 439       * @return int Number of affected rows
 440       */
 441      public function fetchAffected(string $statement, array $values = []): int {
 442          return $this->fetch_wrapper('fetchAffected', $statement, $values);
 443      }
 444  
 445      /**
 446       * Fetch all rows
 447       *
 448       * @since 1.10.4
 449       * @param string $statement SQL statement to execute
 450       * @param array  $values    Optional. Values to bind to the statement. Default empty array.
 451       * @return array All rows returned by the query
 452       */
 453      public function fetchAll(string $statement, array $values = []): array {
 454          return $this->fetch_wrapper('fetchAll', $statement, $values);
 455      }
 456  
 457      /**
 458       * Fetch all rows as associative arrays
 459       *
 460       * @since 1.10.4
 461       * @param string $statement SQL statement to execute
 462       * @param array  $values    Optional. Values to bind to the statement. Default empty array.
 463       * @return array All rows as associative arrays
 464       */
 465      public function fetchAssoc(string $statement, array $values = []): array {
 466          return $this->fetch_wrapper('fetchAssoc', $statement, $values);
 467      }
 468  
 469      /**
 470       * Fetch a single column from all rows
 471       *
 472       * @since 1.10.4
 473       * @param string $statement SQL statement to execute
 474       * @param array  $values    Optional. Values to bind to the statement. Default empty array.
 475       * @return array First column values from all rows
 476       */
 477      public function fetchCol(string $statement, array $values = []): array {
 478          return $this->fetch_wrapper('fetchCol', $statement, $values);
 479      }
 480  
 481      /**
 482       * Fetch rows grouped by the first column
 483       *
 484       * @since 1.10.4
 485       * @param string $statement SQL statement to execute
 486       * @param array  $values    Optional. Values to bind to the statement. Default empty array.
 487       * @param int    $style     Optional. PDO fetch style constant. Default PDO::FETCH_COLUMN.
 488       * @return array Rows grouped by the first column value
 489       */
 490      public function fetchGroup(string $statement, array $values = [], int $style = PDO::FETCH_COLUMN): array {
 491          return $this->fetch_wrapper('fetchGroup', $statement, $values, $style);
 492      }
 493  
 494      /**
 495       * Fetch a single row as an object
 496       *
 497       * @since 1.10.4
 498       * @param string $statement SQL statement to execute
 499       * @param array  $values    Optional. Values to bind to the statement. Default empty array.
 500       * @param string $class     Optional. Class name for the returned object. Default 'stdClass'.
 501       * @param array  $args      Optional. Constructor arguments for the class. Default empty array.
 502       * @return object|false Object representing the row, or false if no rows found
 503       */
 504      public function fetchObject(string $statement, array $values = [], string $class = 'stdClass', array $args = []): object|false {
 505          return $this->fetch_wrapper('fetchObject', $statement, $values, $class, $args);
 506      }
 507  
 508      /**
 509       * Fetch all rows as objects
 510       *
 511       * @since 1.10.4
 512       * @param string $statement SQL statement to execute
 513       * @param array  $values    Optional. Values to bind to the statement. Default empty array.
 514       * @param string $class     Optional. Class name for the returned objects. Default 'stdClass'.
 515       * @param array  $args      Optional. Constructor arguments for the class. Default empty array.
 516       * @return array All rows as objects
 517       */
 518      public function fetchObjects(string $statement, array $values = [], string $class = 'stdClass', array $args = []): array {
 519          return $this->fetch_wrapper('fetchObjects', $statement, $values, $class, $args);
 520      }
 521  
 522      /**
 523       * Fetch a single row as an array
 524       *
 525       * @since 1.10.4
 526       * @param string $statement SQL statement to execute
 527       * @param array  $values    Optional. Values to bind to the statement. Default empty array.
 528       * @return array|false Associative array representing the row, or false if no rows found
 529       */
 530      public function fetchOne(string $statement, array $values = []): array|false {
 531          return $this->fetch_wrapper('fetchOne', $statement, $values);
 532      }
 533  
 534      /**
 535       * Fetch key-value pairs
 536       *
 537       * @since 1.10.4
 538       * @param string $statement SQL statement to execute
 539       * @param array  $values    Optional. Values to bind to the statement. Default empty array.
 540       * @return array Associative array of key-value pairs
 541       */
 542      public function fetchPairs(string $statement, array $values = []): array {
 543          return $this->fetch_wrapper('fetchPairs', $statement, $values);
 544      }
 545  
 546      /**
 547       * Fetch a single value
 548       *
 549       * @since 1.10.4
 550       * @param string $statement SQL statement to execute
 551       * @param array  $values    Optional. Values to bind to the statement. Default empty array.
 552       * @return mixed Single value from the query result
 553       */
 554      public function fetchValue(string $statement, array $values = []): mixed {
 555          return $this->fetch_wrapper('fetchValue', $statement, $values);
 556      }
 557  
 558      /**
 559       * Performs a query with bound values and returns the resulting PDOStatement
 560       * You most likely should not use this method directly. Use the fetch_* methods instead.
 561       *
 562       * @since 1.10.4
 563       * @param string $statement The SQL statement to perform.
 564       * @param array  $values    Values to bind to the query
 565       * @return PDOStatement
 566       */
 567      public function perform(string $statement, array $values = []): PDOStatement {
 568          return $this->fetch_wrapper('perform', $statement, $values);
 569      }
 570  
 571      /**
 572       * Wrapper for all fetch methods, allowing plugins to intercept and modify query results.
 573       *
 574       * @since 1.10.4
 575       * @param string $method  The parent fetch method name to call (e.g., 'fetchAll', 'fetchValue')
 576       * @param mixed  ...$args Variable number of arguments to pass to the parent method
 577       * @return mixed The cached result if available, otherwise the fresh query result
 578       */
 579      public function fetch_wrapper(string $method, ...$args): mixed {
 580          // Allow plugins to short-circuit the whole function if we're not in bypass mode
 581          if (!$this->bypass_shunt_filter) {
 582              $pre = yourls_apply_filter('shunt_fetch_wrapper', yourls_shunt_default(), $method, ...$args);
 583              if (yourls_shunt_default() !== $pre) {
 584                  return $pre;
 585              }
 586          }
 587  
 588          // Filter the query statement
 589          $args[0] = yourls_apply_filter('fetch_wrapper_statement', $args[0], $method, $args);
 590  
 591          return parent::$method( ...$args);
 592      }
 593  
 594      /**
 595       * Execute a callback with filters temporarily disabled
 596       *
 597       * This method allows bypassing the plugin filter system for the duration of the callback execution. Useful to
 598       * prevent infinite loops when a filter needs to call the original method without re-triggering itself.
 599       *
 600       * Example usage:
 601       *      $ydb = yourls_get_db('write-get_from_cache');
 602       *      $result = $ydb->withoutFilters(function($db) use ($method, $args) {
 603       *          return $db->fetch_wrapper($method, ...$args);
 604       *      });
 605       *
 606       * @since 1.10.4
 607       * @param callable $callback
 608       * @return mixed
 609       */
 610      public function without_filters(callable $callback): mixed {
 611          $this->bypass_shunt_filter = true;
 612          try {
 613              return $callback($this);
 614          } finally {
 615              $this->bypass_shunt_filter = false;
 616          }
 617      }
 618  }


Generated: Wed Apr 8 05:10:41 2026 Cross-referenced by PHPXref 0.7.1