| 1 |
<?php |
| 2 |
/** |
| 3 |
* A SQL Transaction Handler to assist with starting, commiting and rolling back transactions. |
| 4 |
* This class also closes off an active transaction before shutdown to allow for shutdown processes to write to the database. |
| 5 |
*/ |
| 6 |
|
| 7 |
namespace StoreEngine; |
| 8 |
|
| 9 |
use StoreEngine\Classes\Exceptions\StoreEngineInvalidArgumentException; |
| 10 |
use StoreEngine\Utils\Constants; |
| 11 |
use StoreEngine\Utils\Helper; |
| 12 |
|
| 13 |
if ( ! defined( 'ABSPATH' ) ) { |
| 14 |
exit; |
| 15 |
} |
| 16 |
|
| 17 |
class SqlTransaction { |
| 18 |
|
| 19 |
/** |
| 20 |
* The query to run when a fatal shutdown occurs. |
| 21 |
* |
| 22 |
* @var string |
| 23 |
*/ |
| 24 |
public string $on_fatal = ''; |
| 25 |
|
| 26 |
/** |
| 27 |
* The query to run if the PHP request ends without error. |
| 28 |
* |
| 29 |
* @var string |
| 30 |
*/ |
| 31 |
public string $on_shutdown = ''; |
| 32 |
|
| 33 |
/** |
| 34 |
* Whether there's an active MYSQL transaction. |
| 35 |
* |
| 36 |
* @var bool |
| 37 |
*/ |
| 38 |
public bool $active_transaction = false; |
| 39 |
|
| 40 |
/** |
| 41 |
* Constructor. |
| 42 |
* |
| 43 |
* @param string<"commit"|"rollback"> $on_fatal Optional. The type of query to run on fatal shutdown if this transaction is still active. Can be 'rollback' or 'commit'. Default is 'rollback'. |
| 44 |
* @param string<"commit"|"rollback"> $on_shutdown Optional. The type of query to run if a non-error shutdown occurs but there's still an active transaction. Can be 'rollback' or 'commit'. Default is 'commit'. |
| 45 |
*/ |
| 46 |
public function __construct( string $on_fatal = 'rollback', string $on_shutdown = 'commit' ) { |
| 47 |
|
| 48 |
// Validate the $on_fatal and $on_shutdown parameters. |
| 49 |
if ( 'commit' !== $on_fatal && 'rollback' !== $on_fatal ) { |
| 50 |
_doing_it_wrong( __METHOD__, esc_html__( 'This method was called with an invalid parameter. The first argument ($on_fatal) should be "rollback" or "commit"', 'storeengine' ), '1.0.0' ); |
| 51 |
} |
| 52 |
|
| 53 |
if ( 'commit' !== $on_shutdown && 'rollback' !== $on_shutdown ) { |
| 54 |
_doing_it_wrong( __METHOD__, esc_html__( 'This method was called with an invalid parameter. The second argument ($on_shutdown) should be "rollback" or "commit"', 'storeengine' ), '1.0.0' ); |
| 55 |
} |
| 56 |
|
| 57 |
$this->on_fatal = $on_fatal; |
| 58 |
$this->on_shutdown = $on_shutdown; |
| 59 |
|
| 60 |
// Ensure we close off this transaction on shutdown to allow other shutdown processes to save changes to the DB. |
| 61 |
add_action( 'shutdown', [ $this, 'handle_shutdown' ], - 100 ); |
| 62 |
} |
| 63 |
|
| 64 |
/** |
| 65 |
* Starts a MYSQL Transaction. |
| 66 |
*/ |
| 67 |
public function start() { |
| 68 |
self::transaction_query( 'start' ); |
| 69 |
$this->active_transaction = true; |
| 70 |
} |
| 71 |
|
| 72 |
/** |
| 73 |
* Commits the MYSQL Transaction. |
| 74 |
*/ |
| 75 |
public function commit() { |
| 76 |
self::transaction_query( 'commit' ); |
| 77 |
$this->active_transaction = false; |
| 78 |
} |
| 79 |
|
| 80 |
/** |
| 81 |
* Rolls back any changes made during the MYSQL Transaction. |
| 82 |
*/ |
| 83 |
public function rollback() { |
| 84 |
self::transaction_query( 'rollback' ); |
| 85 |
$this->active_transaction = false; |
| 86 |
} |
| 87 |
|
| 88 |
/** |
| 89 |
* Closes out an active transaction depending on the type of shutdown. |
| 90 |
* |
| 91 |
* Shutdowns caused by a fatal will be rolledback or commited @see $this->on_fatal. |
| 92 |
* Shutdowns caused by a natural PHP termination (no error) will be rolledback or commited. @see $this->on_shutdown. |
| 93 |
*/ |
| 94 |
public function handle_shutdown() { |
| 95 |
if ( ! $this->active_transaction ) { |
| 96 |
return; |
| 97 |
} |
| 98 |
|
| 99 |
$error = error_get_last(); |
| 100 |
$types = [ |
| 101 |
E_ERROR, |
| 102 |
E_PARSE, |
| 103 |
E_COMPILE_ERROR, |
| 104 |
E_USER_ERROR, |
| 105 |
E_RECOVERABLE_ERROR, |
| 106 |
]; |
| 107 |
|
| 108 |
if ( $error && in_array( $error['type'], $types, true ) ) { |
| 109 |
$this->{$this->on_fatal}(); |
| 110 |
} else { |
| 111 |
$this->{$this->on_shutdown}(); |
| 112 |
} |
| 113 |
} |
| 114 |
|
| 115 |
/** |
| 116 |
* Run a MySQL transaction query, if supported. |
| 117 |
* |
| 118 |
* @param string<"start"|"commit"|"rollback"> $type Types: start (default), commit, rollback. |
| 119 |
* @param bool $force use of transactions. |
| 120 |
* |
| 121 |
* @throws StoreEngineInvalidArgumentException |
| 122 |
*/ |
| 123 |
public static function transaction_query( string $type = 'start', bool $force = false ) { |
| 124 |
global $wpdb; |
| 125 |
|
| 126 |
$allowed = [ 'start', 'commit', 'rollback' ]; |
| 127 |
|
| 128 |
if ( ! in_array( $type, $allowed, true ) ) { |
| 129 |
throw StoreEngineInvalidArgumentException::create( 1, 'type', $allowed, $type ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped |
| 130 |
} |
| 131 |
|
| 132 |
$wpdb->hide_errors(); |
| 133 |
|
| 134 |
Helper::maybe_define_constant( 'STOREENGINE_USE_TRANSACTIONS', true ); |
| 135 |
|
| 136 |
if ( Constants::is_true( 'STOREENGINE_USE_TRANSACTIONS' ) || $force ) { |
| 137 |
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching |
| 138 |
switch ( $type ) { |
| 139 |
case 'commit': |
| 140 |
$wpdb->query( 'COMMIT' ); |
| 141 |
break; |
| 142 |
case 'rollback': |
| 143 |
$wpdb->query( 'ROLLBACK' ); |
| 144 |
break; |
| 145 |
case 'start': |
| 146 |
default: |
| 147 |
$wpdb->query( 'START TRANSACTION' ); |
| 148 |
break; |
| 149 |
} |
| 150 |
// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching |
| 151 |
} |
| 152 |
} |
| 153 |
} |
| 154 |
|