[ Index ] |
PHP Cross Reference of Joomla 4.2.2 documentation |
[Summary view] [Print] [Text view]
1 <?php 2 /** 3 * Part of the Joomla Framework Database Package 4 * 5 * @copyright Copyright (C) 2005 - 2021 Open Source Matters, Inc. All rights reserved. 6 * @license GNU General Public License version 2 or later; see LICENSE 7 */ 8 9 namespace Joomla\Database\Sqlsrv; 10 11 use Joomla\Database\DatabaseDriver; 12 use Joomla\Database\DatabaseEvents; 13 use Joomla\Database\Event\ConnectionEvent; 14 use Joomla\Database\Exception\ConnectionFailureException; 15 use Joomla\Database\Exception\ExecutionFailureException; 16 use Joomla\Database\Exception\PrepareStatementFailureException; 17 use Joomla\Database\Exception\UnsupportedAdapterException; 18 use Joomla\Database\StatementInterface; 19 20 /** 21 * SQL Server Database Driver 22 * 23 * @link https://www.php.net/manual/en/book.sqlsrv.php 24 * @since 1.0 25 */ 26 class SqlsrvDriver extends DatabaseDriver 27 { 28 /** 29 * The name of the database driver. 30 * 31 * @var string 32 * @since 1.0 33 */ 34 public $name = 'sqlsrv'; 35 36 /** 37 * The character(s) used to quote SQL statement names such as table names or field names, etc. 38 * 39 * If a single character string the same character is used for both sides of the quoted name, else the first character will be used for the 40 * opening quote and the second for the closing quote. 41 * 42 * @var string 43 * @since 1.0 44 */ 45 protected $nameQuote = '[]'; 46 47 /** 48 * The null or zero representation of a timestamp for the database driver. 49 * 50 * @var string 51 * @since 1.0 52 */ 53 protected $nullDate = '1900-01-01 00:00:00'; 54 55 /** 56 * The minimum supported database version. 57 * 58 * @var string 59 * @since 1.0 60 */ 61 protected static $dbMinimum = '11.0.2100.60'; 62 63 /** 64 * Test to see if the SQLSRV connector is available. 65 * 66 * @return boolean True on success, false otherwise. 67 * 68 * @since 1.0 69 */ 70 public static function isSupported() 71 { 72 return \function_exists('sqlsrv_connect'); 73 } 74 75 /** 76 * Constructor. 77 * 78 * @param array $options List of options used to configure the connection 79 * 80 * @since 1.0 81 */ 82 public function __construct(array $options) 83 { 84 // Get some basic values from the options. 85 $options['host'] = $options['host'] ?? 'localhost'; 86 $options['user'] = $options['user'] ?? ''; 87 $options['password'] = $options['password'] ?? ''; 88 $options['database'] = $options['database'] ?? ''; 89 $options['select'] = isset($options['select']) ? (bool) $options['select'] : true; 90 91 // Finalize initialisation 92 parent::__construct($options); 93 } 94 95 /** 96 * Connects to the database if needed. 97 * 98 * @return void Returns void if the database connected successfully. 99 * 100 * @since 1.0 101 * @throws \RuntimeException 102 */ 103 public function connect() 104 { 105 if ($this->connection) 106 { 107 return; 108 } 109 110 // Make sure the SQLSRV extension for PHP is installed and enabled. 111 if (!static::isSupported()) 112 { 113 throw new UnsupportedAdapterException('PHP extension sqlsrv_connect is not available.'); 114 } 115 116 // Build the connection configuration array. 117 $config = [ 118 'Database' => $this->options['database'], 119 'uid' => $this->options['user'], 120 'pwd' => $this->options['password'], 121 'CharacterSet' => 'UTF-8', 122 'ReturnDatesAsStrings' => true, 123 ]; 124 125 // Attempt to connect to the server. 126 if (!($this->connection = @ sqlsrv_connect($this->options['host'], $config))) 127 { 128 throw new ConnectionFailureException('Could not connect to SQL Server'); 129 } 130 131 // Make sure that DB warnings are not returned as errors. 132 sqlsrv_configure('WarningsReturnAsErrors', 0); 133 134 // If auto-select is enabled select the given database. 135 if ($this->options['select'] && !empty($this->options['database'])) 136 { 137 $this->select($this->options['database']); 138 } 139 140 $this->dispatchEvent(new ConnectionEvent(DatabaseEvents::POST_CONNECT, $this)); 141 } 142 143 /** 144 * Disconnects the database. 145 * 146 * @return void 147 * 148 * @since 1.0 149 */ 150 public function disconnect() 151 { 152 // Close the connection. 153 if (\is_resource($this->connection)) 154 { 155 sqlsrv_close($this->connection); 156 } 157 158 parent::disconnect(); 159 } 160 161 /** 162 * Get table constraints 163 * 164 * @param string $tableName The name of the database table. 165 * 166 * @return array Any constraints available for the table. 167 * 168 * @since 1.0 169 */ 170 protected function getTableConstraints($tableName) 171 { 172 $this->connect(); 173 174 return $this->setQuery('SELECT CONSTRAINT_NAME FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE TABLE_NAME = ' . $this->quote($tableName)) 175 ->loadColumn(); 176 } 177 178 /** 179 * Rename constraints. 180 * 181 * @param array $constraints Array(strings) of table constraints 182 * @param string $prefix A string 183 * @param string $backup A string 184 * 185 * @return void 186 * 187 * @since 1.0 188 */ 189 protected function renameConstraints($constraints = [], $prefix = null, $backup = null) 190 { 191 $this->connect(); 192 193 foreach ($constraints as $constraint) 194 { 195 $this->setQuery('sp_rename ' . $constraint . ',' . str_replace($prefix, $backup, $constraint)) 196 ->execute(); 197 } 198 } 199 200 /** 201 * Method to escape a string for usage in an SQL statement. 202 * 203 * The escaping for MSSQL isn't handled in the driver though that would be nice. Because of this we need to handle the escaping ourselves. 204 * 205 * @param string $text The string to be escaped. 206 * @param boolean $extra Optional parameter to provide extra escaping. 207 * 208 * @return string The escaped string. 209 * 210 * @since 1.0 211 */ 212 public function escape($text, $extra = false) 213 { 214 if (\is_int($text)) 215 { 216 return $text; 217 } 218 219 if (\is_float($text)) 220 { 221 // Force the dot as a decimal point. 222 return str_replace(',', '.', $text); 223 } 224 225 $result = str_replace("'", "''", $text); 226 227 // SQL Server does not accept NULL byte in query string 228 $result = str_replace("\0", "' + CHAR(0) + N'", $result); 229 230 // Fix for SQL Sever escape sequence, see https://support.microsoft.com/en-us/kb/164291 231 $result = str_replace( 232 ["\\\n", "\\\r", "\\\\\r\r\n"], 233 ["\\\\\n\n", "\\\\\r\r", "\\\\\r\n\r\n"], 234 $result 235 ); 236 237 if ($extra) 238 { 239 // Escape special chars 240 $result = str_replace( 241 ['[', '_', '%'], 242 ['[[]', '[_]', '[%]'], 243 $result 244 ); 245 } 246 247 return $result; 248 } 249 250 /** 251 * Quotes and optionally escapes a string to database requirements for use in database queries. 252 * 253 * @param mixed $text A string or an array of strings to quote. 254 * @param boolean $escape True (default) to escape the string, false to leave it unchanged. 255 * 256 * @return string The quoted input string. 257 * 258 * @since 1.6.0 259 */ 260 public function quote($text, $escape = true) 261 { 262 if (\is_array($text)) 263 { 264 return parent::quote($text, $escape); 265 } 266 267 // To support unicode on MSSQL we have to add prefix N 268 return 'N\'' . ($escape ? $this->escape($text) : $text) . '\''; 269 } 270 271 /** 272 * Quotes a binary string to database requirements for use in database queries. 273 * 274 * @param string $data A binary string to quote. 275 * 276 * @return string The binary quoted input string. 277 * 278 * @since 1.7.0 279 */ 280 public function quoteBinary($data) 281 { 282 // ODBC syntax for hexadecimal literals 283 return '0x' . bin2hex($data); 284 } 285 286 /** 287 * Determines if the connection to the server is active. 288 * 289 * @return boolean True if connected to the database engine. 290 * 291 * @since 1.0 292 */ 293 public function connected() 294 { 295 // TODO: Run a blank query here 296 return true; 297 } 298 299 /** 300 * Drops a table from the database. 301 * 302 * @param string $table The name of the database table to drop. 303 * @param boolean $ifExists Optionally specify that the table must exist before it is dropped. 304 * 305 * @return $this 306 * 307 * @since 1.0 308 */ 309 public function dropTable($table, $ifExists = true) 310 { 311 $this->connect(); 312 313 if ($ifExists) 314 { 315 $this->setQuery( 316 'IF EXISTS(SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = ' 317 . $this->quote($table) . ') DROP TABLE ' . $table 318 ); 319 } 320 else 321 { 322 $this->setQuery('DROP TABLE ' . $table); 323 } 324 325 $this->execute(); 326 327 return $this; 328 } 329 330 /** 331 * Method to get the database collation in use by sampling a text field of a table in the database. 332 * 333 * @return string|boolean The collation in use by the database or boolean false if not supported. 334 * 335 * @since 1.0 336 */ 337 public function getCollation() 338 { 339 // TODO: Not fake this 340 return 'MSSQL UTF-8 (UCS2)'; 341 } 342 343 /** 344 * Method to get the database connection collation in use by sampling a text field of a table in the database. 345 * 346 * @return string|boolean The collation in use by the database connection (string) or boolean false if not supported. 347 * 348 * @since 1.6.0 349 * @throws \RuntimeException 350 */ 351 public function getConnectionCollation() 352 { 353 // TODO: Not fake this 354 return 'MSSQL UTF-8 (UCS2)'; 355 } 356 357 /** 358 * Method to get the database encryption details (cipher and protocol) in use. 359 * 360 * @return string The database encryption details. 361 * 362 * @since 2.0.0 363 * @throws \RuntimeException 364 */ 365 public function getConnectionEncryption(): string 366 { 367 // TODO: Not fake this 368 return ''; 369 } 370 371 /** 372 * Method to test if the database TLS connections encryption are supported. 373 * 374 * @return boolean Whether the database supports TLS connections encryption. 375 * 376 * @since 2.0.0 377 */ 378 public function isConnectionEncryptionSupported(): bool 379 { 380 // TODO: Not fake this 381 return false; 382 } 383 384 /** 385 * Retrieves field information about the given tables. 386 * 387 * @param mixed $table A table name 388 * @param boolean $typeOnly True to only return field types. 389 * 390 * @return array An array of fields. 391 * 392 * @since 1.0 393 * @throws \RuntimeException 394 */ 395 public function getTableColumns($table, $typeOnly = true) 396 { 397 $result = []; 398 399 $table_temp = $this->replacePrefix((string) $table); 400 401 // Set the query to get the table fields statement. 402 $this->setQuery( 403 'SELECT column_name as Field, data_type as Type, is_nullable as \'Null\', column_default as \'Default\'' . 404 ' FROM information_schema.columns WHERE table_name = ' . $this->quote($table_temp) 405 ); 406 $fields = $this->loadObjectList(); 407 408 // If we only want the type as the value add just that to the list. 409 if ($typeOnly) 410 { 411 foreach ($fields as $field) 412 { 413 $result[$field->Field] = preg_replace('/[(0-9)]/', '', $field->Type); 414 } 415 } 416 else 417 { 418 // If we want the whole field data object add that to the list. 419 foreach ($fields as $field) 420 { 421 $field->Default = preg_replace("/(^(\(\(|\('|\(N'|\()|(('\)|(?<!\()\)\)|\))$))/i", '', $field->Default); 422 $result[$field->Field] = $field; 423 } 424 } 425 426 return $result; 427 } 428 429 /** 430 * Shows the table CREATE statement that creates the given tables. 431 * 432 * This is unsupported by MSSQL. 433 * 434 * @param mixed $tables A table name or a list of table names. 435 * 436 * @return array A list of the create SQL for the tables. 437 * 438 * @since 1.0 439 * @throws \RuntimeException 440 */ 441 public function getTableCreate($tables) 442 { 443 $this->connect(); 444 445 return ''; 446 } 447 448 /** 449 * Get the details list of keys for a table. 450 * 451 * @param string $table The name of the table. 452 * 453 * @return array An array of the column specification for the table. 454 * 455 * @since 1.0 456 * @throws \RuntimeException 457 */ 458 public function getTableKeys($table) 459 { 460 $this->connect(); 461 462 // TODO To implement. 463 return []; 464 } 465 466 /** 467 * Method to get an array of all tables in the database. 468 * 469 * @return array An array of all the tables in the database. 470 * 471 * @since 1.0 472 * @throws \RuntimeException 473 */ 474 public function getTableList() 475 { 476 $this->connect(); 477 478 // Set the query to get the tables statement. 479 return $this->setQuery('SELECT name FROM ' . $this->getDatabase() . '.sys.Tables WHERE type = \'U\';')->loadColumn(); 480 } 481 482 /** 483 * Get the version of the database connector. 484 * 485 * @return string The database connector version. 486 * 487 * @since 1.0 488 */ 489 public function getVersion() 490 { 491 $this->connect(); 492 493 $version = sqlsrv_server_info($this->connection); 494 495 return $version['SQLServerVersion']; 496 } 497 498 /** 499 * Inserts a row into a table based on an object's properties. 500 * 501 * @param string $table The name of the database table to insert into. 502 * @param object $object A reference to an object whose public properties match the table fields. 503 * @param string $key The name of the primary key. If provided the object property is updated. 504 * 505 * @return boolean True on success. 506 * 507 * @since 1.0 508 * @throws \RuntimeException 509 */ 510 public function insertObject($table, &$object, $key = null) 511 { 512 $fields = []; 513 $values = []; 514 $tableColumns = $this->getTableColumns($table); 515 $statement = 'INSERT INTO ' . $this->quoteName($table) . ' (%s) VALUES (%s)'; 516 517 foreach (get_object_vars($object) as $k => $v) 518 { 519 // Skip columns that don't exist in the table. 520 if (!\array_key_exists($k, $tableColumns)) 521 { 522 continue; 523 } 524 525 // Only process non-null scalars. 526 if (\is_array($v) || \is_object($v) || $v === null) 527 { 528 continue; 529 } 530 531 if (!$this->checkFieldExists($table, $k)) 532 { 533 continue; 534 } 535 536 if ($k[0] === '_') 537 { 538 // Internal field 539 continue; 540 } 541 542 if ($k === $key && $key == 0) 543 { 544 continue; 545 } 546 547 $fields[] = $this->quoteName($k); 548 $values[] = $this->quote($v); 549 } 550 551 // Set the query and execute the insert. 552 $this->setQuery(sprintf($statement, implode(',', $fields), implode(',', $values)))->execute(); 553 554 $id = $this->insertid(); 555 556 if ($key && $id) 557 { 558 $object->$key = $id; 559 } 560 561 return true; 562 } 563 564 /** 565 * Method to get the auto-incremented value from the last INSERT statement. 566 * 567 * @return integer The value of the auto-increment field from the last inserted row. 568 * 569 * @since 1.0 570 */ 571 public function insertid() 572 { 573 $this->connect(); 574 575 // TODO: SELECT IDENTITY 576 $this->setQuery('SELECT @@IDENTITY'); 577 578 return (int) $this->loadResult(); 579 } 580 581 /** 582 * Execute the SQL statement. 583 * 584 * @return boolean 585 * 586 * @since 1.0 587 * @throws \RuntimeException 588 */ 589 public function execute() 590 { 591 $this->connect(); 592 593 // Take a local copy so that we don't modify the original query and cause issues later 594 $sql = $this->replacePrefix((string) $this->sql); 595 596 // Increment the query counter. 597 $this->count++; 598 599 // Get list of bounded parameters 600 $bounded =& $this->sql->getBounded(); 601 602 // If there is a monitor registered, let it know we are starting this query 603 if ($this->monitor) 604 { 605 $this->monitor->startQuery($sql, $bounded); 606 } 607 608 // Execute the query. 609 $this->executed = false; 610 611 // Bind the variables 612 foreach ($bounded as $key => $obj) 613 { 614 $this->statement->bindParam($key, $obj->value, $obj->dataType); 615 } 616 617 try 618 { 619 $this->executed = $this->statement->execute(); 620 621 // If there is a monitor registered, let it know we have finished this query 622 if ($this->monitor) 623 { 624 $this->monitor->stopQuery(); 625 } 626 627 return true; 628 } 629 catch (ExecutionFailureException $exception) 630 { 631 // If there is a monitor registered, let it know we have finished this query 632 if ($this->monitor) 633 { 634 $this->monitor->stopQuery(); 635 } 636 637 // Check if the server was disconnected. 638 if (!$this->connected()) 639 { 640 try 641 { 642 // Attempt to reconnect. 643 $this->connection = null; 644 $this->connect(); 645 } 646 catch (ConnectionFailureException $e) 647 { 648 // If connect fails, ignore that exception and throw the normal exception. 649 throw $exception; 650 } 651 652 // Since we were able to reconnect, run the query again. 653 return $this->execute(); 654 } 655 656 // Throw the normal query exception. 657 throw $exception; 658 } 659 } 660 661 /** 662 * This function replaces a string identifier with the configured table prefix. 663 * 664 * @param string $sql The SQL statement to prepare. 665 * @param string $prefix The table prefix. 666 * 667 * @return string The processed SQL statement. 668 * 669 * @since 1.0 670 */ 671 public function replacePrefix($sql, $prefix = '#__') 672 { 673 $escaped = false; 674 $startPos = 0; 675 $quoteChar = ''; 676 $literal = ''; 677 678 $sql = trim($sql); 679 $n = \strlen($sql); 680 681 while ($startPos < $n) 682 { 683 $ip = strpos($sql, $prefix, $startPos); 684 685 if ($ip === false) 686 { 687 break; 688 } 689 690 $j = strpos($sql, "N'", $startPos); 691 $k = strpos($sql, '"', $startPos); 692 693 if (($k !== false) && (($k < $j) || ($j === false))) 694 { 695 $quoteChar = '"'; 696 $j = $k; 697 } 698 else 699 { 700 $quoteChar = "'"; 701 } 702 703 if ($j === false) 704 { 705 $j = $n; 706 } 707 708 $literal .= str_replace($prefix, $this->tablePrefix, substr($sql, $startPos, $j - $startPos)); 709 $startPos = $j; 710 711 $j = $startPos + 1; 712 713 if ($j >= $n) 714 { 715 break; 716 } 717 718 // Quote comes first, find end of quote 719 while (true) 720 { 721 $k = strpos($sql, $quoteChar, $j); 722 $escaped = false; 723 724 if ($k === false) 725 { 726 break; 727 } 728 729 $l = $k - 1; 730 731 while ($l >= 0 && $sql[$l] === '\\') 732 { 733 $l--; 734 $escaped = !$escaped; 735 } 736 737 if ($escaped) 738 { 739 $j = $k + 1; 740 741 continue; 742 } 743 744 break; 745 } 746 747 if ($k === false) 748 { 749 // Error in the query - no end quote; ignore it 750 break; 751 } 752 753 $literal .= substr($sql, $startPos, $k - $startPos + 1); 754 $startPos = $k + 1; 755 } 756 757 if ($startPos < $n) 758 { 759 $literal .= substr($sql, $startPos, $n - $startPos); 760 } 761 762 return $literal; 763 } 764 765 /** 766 * Select a database for use. 767 * 768 * @param string $database The name of the database to select for use. 769 * 770 * @return boolean True if the database was successfully selected. 771 * 772 * @since 1.0 773 * @throws ConnectionFailureException 774 */ 775 public function select($database) 776 { 777 $this->connect(); 778 779 if (!$database) 780 { 781 return false; 782 } 783 784 if (!sqlsrv_query($this->connection, 'USE [' . $database . ']', null, ['scrollable' => \SQLSRV_CURSOR_STATIC])) 785 { 786 throw new ConnectionFailureException('Could not connect to database'); 787 } 788 789 return true; 790 } 791 792 /** 793 * Set the connection to use UTF-8 character encoding. 794 * 795 * @return boolean True on success. 796 * 797 * @since 1.0 798 */ 799 public function setUtf() 800 { 801 return true; 802 } 803 804 /** 805 * Method to commit a transaction. 806 * 807 * @param boolean $toSavepoint If true, commit to the last savepoint. 808 * 809 * @return void 810 * 811 * @since 1.0 812 * @throws \RuntimeException 813 */ 814 public function transactionCommit($toSavepoint = false) 815 { 816 $this->connect(); 817 818 if (!$toSavepoint || $this->transactionDepth <= 1) 819 { 820 $this->setQuery('COMMIT TRANSACTION')->execute(); 821 822 $this->transactionDepth = 0; 823 824 return; 825 } 826 827 $this->transactionDepth--; 828 } 829 830 /** 831 * Method to roll back a transaction. 832 * 833 * @param boolean $toSavepoint If true, rollback to the last savepoint. 834 * 835 * @return void 836 * 837 * @since 1.0 838 * @throws \RuntimeException 839 */ 840 public function transactionRollback($toSavepoint = false) 841 { 842 $this->connect(); 843 844 if (!$toSavepoint || $this->transactionDepth <= 1) 845 { 846 $this->setQuery('ROLLBACK TRANSACTION')->execute(); 847 848 $this->transactionDepth = 0; 849 850 return; 851 } 852 853 $savepoint = 'SP_' . ($this->transactionDepth - 1); 854 $this->setQuery('ROLLBACK TRANSACTION ' . $this->quoteName($savepoint))->execute(); 855 856 $this->transactionDepth--; 857 } 858 859 /** 860 * Method to initialize a transaction. 861 * 862 * @param boolean $asSavepoint If true and a transaction is already active, a savepoint will be created. 863 * 864 * @return void 865 * 866 * @since 1.0 867 * @throws \RuntimeException 868 */ 869 public function transactionStart($asSavepoint = false) 870 { 871 $this->connect(); 872 873 if (!$asSavepoint || !$this->transactionDepth) 874 { 875 $this->setQuery('BEGIN TRANSACTION')->execute(); 876 877 $this->transactionDepth = 1; 878 879 return; 880 } 881 882 $savepoint = 'SP_' . $this->transactionDepth; 883 $this->setQuery('BEGIN TRANSACTION ' . $this->quoteName($savepoint))->execute(); 884 885 $this->transactionDepth++; 886 } 887 888 /** 889 * Method to check and see if a field exists in a table. 890 * 891 * @param string $table The table in which to verify the field. 892 * @param string $field The field to verify. 893 * 894 * @return boolean True if the field exists in the table. 895 * 896 * @since 1.0 897 */ 898 protected function checkFieldExists($table, $field) 899 { 900 $this->connect(); 901 902 $table = $this->replacePrefix((string) $table); 903 $this->setQuery( 904 "SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = '$table' AND COLUMN_NAME = '$field' ORDER BY ORDINAL_POSITION" 905 ); 906 907 return (bool) $this->loadResult(); 908 } 909 910 /** 911 * Prepares a SQL statement for execution 912 * 913 * @param string $query The SQL query to be prepared. 914 * 915 * @return StatementInterface 916 * 917 * @since 2.0.0 918 * @throws PrepareStatementFailureException 919 */ 920 protected function prepareStatement(string $query): StatementInterface 921 { 922 return new SqlsrvStatement($this->connection, $query); 923 } 924 925 /** 926 * Renames a table in the database. 927 * 928 * @param string $oldTable The name of the table to be renamed 929 * @param string $newTable The new name for the table. 930 * @param string $backup Table prefix 931 * @param string $prefix For the table - used to rename constraints in non-mysql databases 932 * 933 * @return $this 934 * 935 * @since 1.0 936 * @throws \RuntimeException 937 */ 938 public function renameTable($oldTable, $newTable, $backup = null, $prefix = null) 939 { 940 $constraints = []; 941 942 if ($prefix !== null && $backup !== null) 943 { 944 $constraints = $this->getTableConstraints($oldTable); 945 } 946 947 if (!empty($constraints)) 948 { 949 $this->renameConstraints($constraints, $prefix, $backup); 950 } 951 952 $this->setQuery("sp_rename '" . $oldTable . "', '" . $newTable . "'"); 953 954 $this->execute(); 955 956 return $this; 957 } 958 959 /** 960 * Locks a table in the database. 961 * 962 * @param string $tableName The name of the table to lock. 963 * 964 * @return $this 965 * 966 * @since 1.0 967 * @throws \RuntimeException 968 */ 969 public function lockTable($tableName) 970 { 971 return $this; 972 } 973 974 /** 975 * Unlocks tables in the database. 976 * 977 * @return $this 978 * 979 * @since 1.0 980 * @throws \RuntimeException 981 */ 982 public function unlockTables() 983 { 984 return $this; 985 } 986 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Wed Sep 7 05:41:13 2022 | Chilli.vc Blog - For Webmaster,Blog-Writer,System Admin and Domainer |