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

src/DB/ChangeTracker.php (3 issues)

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;
0 ignored issues
show
Documentation Bug introduced by Sam Wilson
The property $current_changeset_id was declared of type integer, but false is of type false. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
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 Sam Wilson
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 Sam Wilson
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