DataDifferencer.php
7.36 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
<?php
/**
* Utility class to render views of the differences between two data objects (or two versions of the
* same data object).
*
* Construcing a diff object is done as follows:
* <code>
* $fromRecord = Versioned::get_version('SiteTree', $pageID, $fromVersion);
* $toRecord = Versioned::get_version('SiteTree, $pageID, $toVersion);
* $diff = new DataDifferencer($fromRecord, $toRecord);
* </code>
*
* And then it can be used in a number of ways. You can use the ChangedFields() method in a template:
* <pre>
* <dl class="diff">
* <% with Diff %>
* <% loop ChangedFields %>
* <dt>$Title</dt>
* <dd>$Diff</dd>
* <% end_loop %>
* <% end_with %>
* </dl>
* </pre>
*
* Or you can get the diff'ed content as another DataObject, that you can insert into a form.
* <code>
* $form->loadDataFrom($diff->diffedData());
* </code>
*
* If there are fields whose changes you aren't interested in, you can ignore them like so:
* <code>
* $diff->ignoreFields('AuthorID', 'Status');
* </code>
*
* @package framework
* @subpackage misc
*/
class DataDifferencer extends ViewableData {
protected $fromRecord;
protected $toRecord;
protected $ignoredFields = array("ID","Version","RecordID");
/**
* Construct a DataDifferencer to show the changes between $fromRecord and $toRecord.
* If $fromRecord is null, this will represent a "creation".
*
* @param DataObject (Optional)
* @param DataObject
*/
public function __construct($fromRecord, DataObject $toRecord) {
if(!$toRecord) user_error("DataDifferencer constructed without a toRecord", E_USER_WARNING);
$this->fromRecord = $fromRecord;
$this->toRecord = $toRecord;
parent::__construct();
}
/**
* Specify some fields to ignore changes from. Repeated calls are cumulative.
* @param $ignoredFields An array of field names to ignore. Alternatively, pass the field names as
* separate args.
*/
public function ignoreFields($ignoredFields) {
if(!is_array($ignoredFields)) $ignoredFields = func_get_args();
$this->ignoredFields = array_merge($this->ignoredFields, $ignoredFields);
}
/**
* Get a DataObject with altered values replaced with HTML diff strings, incorporating
* <ins> and <del> tags.
*/
public function diffedData() {
if($this->fromRecord) {
$diffed = clone $this->fromRecord;
$fields = array_keys($diffed->toMap() + $this->toRecord->toMap());
} else {
$diffed = clone $this->toRecord;
$fields = array_keys($this->toRecord->toMap());
}
$hasOnes = array_merge($this->fromRecord->has_one(), $this->toRecord->has_one());
// Loop through properties
foreach($fields as $field) {
if(in_array($field, $this->ignoredFields)) continue;
if(in_array($field, array_keys($hasOnes))) continue;
// Check if a field from-value is comparable
$toField = $this->toRecord->obj($field);
if(!($toField instanceof DBField)) continue;
$toValue = $toField->forTemplate();
// Show only to value
if(!$this->fromRecord) {
$diffed->setField($field, "<ins>{$toValue}</ins>");
continue;
}
// Check if a field to-value is comparable
$fromField = $this->fromRecord->obj($field);
if(!($fromField instanceof DBField)) continue;
$fromValue = $fromField->forTemplate();
// Show changes between the two, if any exist
if($fromValue != $toValue) {
$diffed->setField($field, Diff::compareHTML($fromValue, $toValue));
}
}
// Loop through has_one
foreach($hasOnes as $relName => $relSpec) {
if(in_array($relName, $this->ignoredFields)) continue;
// Create the actual column name
$relField = "{$relName}ID";
$toTitle = '';
if($this->toRecord->hasMethod($relName)) {
$relObjTo = $this->toRecord->$relName();
if($relObjTo) {
$toTitle = ($relObjTo->hasMethod('Title') || $relObjTo->hasField('Title')) ? $relObjTo->Title : '';
} else {
$toTitle = '';
}
}
if(!$this->fromRecord) {
if($relObjTo) {
if($relObjTo instanceof Image) {
// Using relation name instead of database column name, because of FileField etc.
// TODO Use CMSThumbnail instead to limit max size, blocked by DataDifferencerTest and GC
// not playing nice with mocked images
$diffed->setField($relName, "<ins>" . $relObjTo->getTag() . "</ins>");
} else {
$diffed->setField($relField, "<ins>" . $toTitle . "</ins>");
}
}
} else if($this->fromRecord->$relField != $this->toRecord->$relField) {
$fromTitle = '';
if($this->fromRecord->hasMethod($relName)) {
$relObjFrom = $this->fromRecord->$relName();
if($relObjFrom) {
$fromTitle = ($relObjFrom->hasMethod('Title') || $relObjFrom->hasField('Title')) ? $relObjFrom->Title : '';
} else {
$fromTitle = '';
}
}
if(isset($relObjFrom) && $relObjFrom instanceof Image) {
// TODO Use CMSThumbnail (see above)
$diffed->setField(
// Using relation name instead of database column name, because of FileField etc.
$relName,
Diff::compareHTML($relObjFrom->getTag(), $relObjTo->getTag())
);
} else {
// Set the field.
$diffed->setField(
$relField,
Diff::compareHTML($fromTitle, $toTitle)
);
}
}
}
return $diffed;
}
/**
* Get a SS_List of the changed fields.
* Each element is an array data containing
* - Name: The field name
* - Title: The human-readable field title
* - Diff: An HTML diff showing the changes
* - From: The older version of the field
* - To: The newer version of the field
*/
public function ChangedFields() {
$changedFields = new ArrayList();
if($this->fromRecord) {
$base = $this->fromRecord;
$fields = array_keys($this->fromRecord->toMap());
} else {
$base = $this->toRecord;
$fields = array_keys($this->toRecord->toMap());
}
foreach($fields as $field) {
if(in_array($field, $this->ignoredFields)) continue;
if(!$this->fromRecord || $this->fromRecord->$field != $this->toRecord->$field) {
// Only show HTML diffs for fields which allow HTML values in the first place
$fieldObj = $this->toRecord->dbObject($field);
if($this->fromRecord) {
$fieldDiff = Diff::compareHTML(
$this->fromRecord->$field,
$this->toRecord->$field,
(!$fieldObj || $fieldObj->stat('escape_type') != 'xml')
);
} else {
if($fieldObj && $fieldObj->stat('escape_type') == 'xml') {
$fieldDiff = "<ins>" . $this->toRecord->$field . "</ins>";
} else {
$fieldDiff = "<ins>" . Convert::raw2xml($this->toRecord->$field) . "</ins>";
}
}
$changedFields->push(new ArrayData(array(
'Name' => $field,
'Title' => $base->fieldLabel($field),
'Diff' => $this->fromRecord
? Diff::compareHTML($this->fromRecord->$field, $this->toRecord->$field)
: "<ins>" . $this->toRecord->$field . "</ins>",
'From' => $this->fromRecord ? $this->fromRecord->$field : null,
'To' => $this->toRecord ? $this->toRecord->$field : null,
)));
}
}
return $changedFields;
}
/**
* Get an array of the names of every fields that has changed.
* This is simpler than {@link ChangedFields()}
*/
public function changedFieldNames() {
$diffed = clone $this->fromRecord;
$fields = array_keys($diffed->toMap());
$changedFields = array();
foreach($fields as $field) {
if(in_array($field, $this->ignoredFields)) continue;
if($this->fromRecord->$field != $this->toRecord->$field) {
$changedFields[] = $field;
}
}
return $changedFields;
}
}