silverstripe-framework/model/DataDifferencer.php
Sam Minnee 3ee8f505b7 MINORE: Remove training whitespace.
The main benefit of this is so that authors who make use of
.editorconfig don't end up with whitespace changes in their PRs.

Spaces vs. tabs has been left alone, although that could do with a
tidy-up in SS4 after the switch to PSR-1/2.

The command used was this:

for match in '*.ss' '*.css' '*.scss' '*.html' '*.yml' '*.php' '*.js' '*.csv' '*.inc' '*.php5'; do
	find . -path ./thirdparty -not -prune -o -path ./admin/thirdparty -not -prune -o -type f -name "$match" -exec sed -E -i '' 's/[[:space:]]+$//' {} \+
	find . -path ./thirdparty -not -prune -o -path ./admin/thirdparty -not -prune -o -type f -name "$match" | xargs perl -pi -e 's/ +$//'
done
2016-01-07 10:15:54 +13:00

243 lines
7.3 KiB
PHP

<?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->hasOne(), $this->toRecord->hasOne());
// 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;
}
}