Completed
Push — master ( 6cab57...287bd7 )
by Sam
02:34
created

src/DB/ChangeTracker.php (2 issues)

Checks global keyword not allowed

Best Practice Compatibility Minor

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/**
3
 * This file contains only a single class.
4
 *
5
 * @file
6
 * @package Tabulate
7
 */
8
9
namespace WordPress\Tabulate\DB;
10
11
use WordPress\Tabulate\DB\Database;
12
use WordPress\Tabulate\DB\Table;
13
use WordPress\Tabulate\DB\Record;
14
15
/**
16
 * The Change Tracker keeps a log of every data modification made through Tabulate.
17
 */
18
class ChangeTracker {
19
20
	/**
21
	 * The global wpdb object.
22
	 *
23
	 * @var \wpdb
24
	 */
25
	protected $wpdb;
26
27
	/**
28
	 * The ID of the currently-open changeset.
29
	 *
30
	 * @var integer
31
	 */
32
	private static $current_changeset_id = false;
33
34
	/**
35
	 * The user comment on the currently-open changeset.
36
	 *
37
	 * @var string
38
	 */
39
	private $current_changeset_comment = null;
40
41
	/**
42
	 * The record prior to modification.
43
	 *
44
	 * @var \WordPress\Tabulate\DB\Record|false
45
	 */
46
	private $old_record = false;
47
48
	/**
49
	 * Whether the changeset should be closed after the first after_save() call.
50
	 *
51
	 * @var boolean
52
	 */
53
	private static $keep_changeset_open = false;
54
55
	/**
56
	 * Create a new change tracker.
57
	 *
58
	 * @param \wpdb  $wpdb The global wpdb object.
59
	 * @param string $comment The user's comment about the change.
60
	 */
61
	public function __construct( $wpdb, $comment = null ) {
62
		$this->wpdb = $wpdb;
63
		$this->current_changeset_comment = $comment;
64
	}
65
66
	/**
67
	 * When destroying a ChangeTracker object, close the current changeset
68
	 * unless it has specifically been requested to be kept open.
69
	 */
70
	public function __destruct() {
71
		if ( ! self::$keep_changeset_open ) {
72
			$this->close_changeset();
73
		}
74
	}
75
76
	/**
77
	 * Open a new changeset. If one is already open, this does nothing.
78
	 *
79
	 * @global \WP_User $current_user
80
	 * @param string  $comment The user's comment on the changeset.
81
	 * @param boolean $keep_open Whether the changeset should be kept open (and manually closed) after after_save() is called.
82
	 * @throws Exception If the changeset row could not be saved.
83
	 */
84
	public function open_changeset( $comment, $keep_open = null ) {
85
		global $current_user;
86
		if ( ! is_null( $keep_open ) ) {
87
			self::$keep_changeset_open = $keep_open;
88
		}
89
		if ( ! self::$current_changeset_id ) {
90
			$data = array(
91
				'date_and_time' => date( 'Y-m-d H:i:s' ),
92
				'user_id' => $current_user->ID,
93
				'comment' => $comment,
94
			);
95
			$ret = $this->wpdb->insert( self::changesets_name(), $data );
96
			if ( false === $ret ) {
97
				throw new Exception( $this->wpdb->last_error . ' -- Unable to open changeset' );
98
			}
99
			self::$current_changeset_id = $this->wpdb->insert_id;
100
		}
101
	}
102
103
	/**
104
	 * Close the current changeset.
105
	 *
106
	 * @return void
107
	 */
108
	public function close_changeset() {
109
		self::$current_changeset_id = false;
110
		$this->current_changeset_comment = null;
111
	}
112
113
	/**
114
	 * This method is called prior to a record being saved, and will open a new
115
	 * changeset if required, and save the old record for later use.
116
	 *
117
	 * @param Table  $table The table into which the record is being saved.
118
	 * @param string $pk_value The primary key of the record being saved. May be null.
119
	 * @return boolean
120
	 */
121
	public function before_save( Table $table, $pk_value ) {
122
		// Don't save changes to the changes tables.
123
		if ( in_array( $table->get_name(), $this->table_names(), true ) ) {
124
			return false;
125
		}
126
127
		// Open a changeset if required.
128
		$this->open_changeset( $this->current_changeset_comment );
129
130
		// Get the current (i.e. soon-to-be-old) data for later use.
131
		$this->old_record = $table->get_record( $pk_value );
132
	}
133
134
	/**
135
	 * This method is called after a record has been saved, and is responsible
136
	 * for creating the actual change-tracking rows in the database.
137
	 *
138
	 * @param Table  $table The table the record is being saved in.
139
	 * @param Record $new_record The record, after being saved.
140
	 * @return boolean
141
	 */
142
	public function after_save( Table $table, Record $new_record ) {
143
		// Don't save changes to the changes tables.
144
		if ( in_array( $table->get_name(), self::table_names(), true ) ) {
145
			return false;
146
		}
147
148
		// Save a change for each changed column.
149
		foreach ( $table->get_columns() as $column ) {
150
			$col_name = ( $column->is_foreign_key() ) ? $column->get_name() . Record::FKTITLE : $column->get_name();
151
			$old_val = ( is_callable( array( $this->old_record, $col_name ) ) ) ? $this->old_record->$col_name() : null;
152
			$new_val = $new_record->$col_name();
153
			if ( $new_val === $old_val ) {
154
				// Ignore unchanged columns.
155
				continue;
156
			}
157
			$data = array(
158
				'changeset_id' => self::$current_changeset_id,
159
				'change_type' => 'field',
160
				'table_name' => $table->get_name(),
161
				'column_name' => $column->get_name(),
162
				'record_ident' => $new_record->get_primary_key(),
163
			);
164
			// Daft workaround for https://core.trac.wordpress.org/ticket/15158 .
165
			if ( ! is_null( $old_val ) ) {
166
				$data['old_value'] = $old_val;
167
			}
168
			if ( ! is_null( $new_val ) ) {
169
				$data['new_value'] = $new_val;
170
			}
171
			// Save the change record.
172
			$this->wpdb->insert( $this->changes_name(), $data );
173
		}
174
175
		// Close the changeset if required.
176
		if ( ! self::$keep_changeset_open ) {
177
			$this->close_changeset();
178
		}
179
	}
180
181
	/**
182
	 * On plugin activation, create two new database tables.
183
	 *
184
	 * @param \wpdb $wpdb The global database object.
185
	 */
186
	public static function activate( \wpdb $wpdb ) {
187
		$db = new Database( $wpdb );
188 View Code Duplication
		if ( ! $db->get_table( self::changesets_name() ) ) {
189
			$sql = "CREATE TABLE IF NOT EXISTS `" . self::changesets_name() . "` (
190
			`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
191
			`date_and_time` DATETIME NOT NULL,
192
			`user_id` BIGINT(20) UNSIGNED NOT NULL,
193
			FOREIGN KEY (`user_id`) REFERENCES `{$wpdb->prefix}users` (`ID`),
194
			`comment` TEXT NULL DEFAULT NULL
195
			);";
196
			$wpdb->query( $sql );
197
		}
198 View Code Duplication
		if ( ! $db->get_table( self::changes_name() ) ) {
199
			$sql = "CREATE TABLE IF NOT EXISTS `" . self::changes_name() . "` (
200
			`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
201
			`changeset_id` INT(10) UNSIGNED NOT NULL,
202
			FOREIGN KEY (`changeset_id`) REFERENCES `" . self::changesets_name() . "` (`id`),
203
			`change_type` ENUM('field', 'file', 'foreign_key') NOT NULL DEFAULT 'field',
204
			`table_name` TEXT(65) NOT NULL,
205
			`record_ident` TEXT(65) NOT NULL,
206
			`column_name` TEXT(65) NOT NULL,
207
			`old_value` LONGTEXT NULL DEFAULT NULL,
208
			`new_value` LONGTEXT NULL DEFAULT NULL
209
			);";
210
			$wpdb->query( $sql );
211
		}
212
	}
213
214
	/**
215
	 * Get the name of the changesets table.
216
	 *
217
	 * @global \WordPress\Tabulate\DB\wpdb $wpdb
218
	 * @return string
219
	 */
220
	public static function changesets_name() {
221
		global $wpdb;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
222
		return $wpdb->prefix . TABULATE_SLUG . '_changesets';
223
	}
224
225
	/**
226
	 * Get the name of the changes table.
227
	 *
228
	 * @global \WordPress\Tabulate\DB\wpdb $wpdb
229
	 * @return string
230
	 */
231
	public static function changes_name() {
232
		global $wpdb;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
233
		return $wpdb->prefix . TABULATE_SLUG . '_changes';
234
	}
235
236
	/**
237
	 * Get a list of the names used by the change-tracking subsystem.
238
	 *
239
	 * @global wpdb $wpdb
240
	 * @return array|string
241
	 */
242
	public static function table_names() {
243
		return array( self::changesets_name(), self::changes_name() );
244
	}
245
}
246