silverstripe-framework/src/ORM/Versioning/DataDifferencer.php

274 lines
9.4 KiB
PHP

<?php
namespace SilverStripe\ORM\Versioning;
use SilverStripe\Assets\Image;
use SilverStripe\Core\Convert;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\ArrayList;
use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\View\ArrayData;
use SilverStripe\View\Parsers\Diff;
use SilverStripe\View\ViewableData;
/**
* 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>
*/
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 $fromRecord
* @param DataObject $toRecord
*/
public function __construct(DataObject $fromRecord = null, DataObject $toRecord = null)
{
$this->fromRecord = $fromRecord;
$this->toRecord = $toRecord;
parent::__construct();
}
/**
* Specify some fields to ignore changes from. Repeated calls are cumulative.
* @param array $ignoredFields An array of field names to ignore. Alternatively, pass the field names as
* separate args.
* @return $this
*/
public function ignoreFields($ignoredFields)
{
if (!is_array($ignoredFields)) {
$ignoredFields = func_get_args();
}
$this->ignoredFields = array_merge($this->ignoredFields, $ignoredFields);
return $this;
}
/**
* 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>");
}
}
} elseif ($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' => $fieldDiff,
'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;
}
}