Completed
Push — fix-for-add-folder ( c1d355...a17c5c )
by Sam
07:30
created

ChangeSetItem::getTitle()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 7
rs 9.4285
cc 2
eloc 5
nc 2
nop 0
1
<?php
2
3
// namespace SilverStripe\Framework\Model\Versioning
4
5
/**
6
 * A single line in a changeset
7
 *
8
 * @property string $Added
9
 * @property string $ObjectClass
10
 * @property int $ObjectID
11
 * @method ManyManyList ReferencedBy() List of explicit items that require this change
12
 * @method ManyManyList References() List of implicit items required by this change
13
 * @method ChangeSet ChangeSet()
14
 */
15
class ChangeSetItem extends DataObject {
16
17
	const EXPLICITLY = 'explicitly';
18
19
	const IMPLICITLY = 'implicitly';
20
21
	/** Represents an object deleted */
22
	const CHANGE_DELETED = 'deleted';
23
24
	/** Represents an object which was modified */
25
	const CHANGE_MODIFIED = 'modified';
26
27
	/** Represents an object added */
28
	const CHANGE_CREATED = 'created';
29
30
	/** Represents an object which hasn't been changed directly, but owns a modified many_many relationship. */
31
	//const CHANGE_MANYMANY = 'manymany';
32
33
	/**
34
	 * Represents that an object has not yet been changed, but
35
	 * should be included in this changeset as soon as any changes exist
36
	 */
37
	const CHANGE_NONE = 'none';
38
39
	private static $db = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
40
		'VersionBefore' => 'Int',
41
		'VersionAfter'  => 'Int',
42
		'Added'         => "Enum('explicitly, implicitly', 'implicitly')"
43
	);
44
45
	private static $has_one = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
46
		'ChangeSet' => 'ChangeSet',
47
		'Object'      => 'DataObject',
48
	);
49
50
	private static $many_many = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
51
		'ReferencedBy' => 'ChangeSetItem'
52
	);
53
54
	private static $belongs_many_many = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
55
		'References' => 'ChangeSetItem.ReferencedBy'
56
	);
57
58
	private static $indexes = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
59
		'ObjectUniquePerChangeSet' => array(
60
			'type' => 'unique',
61
			'value' => '"ObjectID", "ObjectClass", "ChangeSetID"'
62
		)
63
	);
64
65
	/**
66
	 * Get the type of change: none, created, deleted, modified, manymany
67
	 *
68
	 * @return string
69
	 */
70
	public function getChangeType() {
71
		// Get change versions
72
		if($this->VersionBefore || $this->VersionAfter) {
73
			$draftVersion = $this->VersionAfter; // After publishing draft was written to stage
0 ignored issues
show
Documentation introduced by
The property VersionAfter does not exist on object<ChangeSetItem>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
74
			$liveVersion = $this->VersionBefore; // The live version before the publish
0 ignored issues
show
Documentation introduced by
The property VersionBefore does not exist on object<ChangeSetItem>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
75
		} else {
76
			$draftVersion = Versioned::get_versionnumber_by_stage(
77
				$this->ObjectClass, Versioned::DRAFT, $this->ObjectID, false
78
			);
79
			$liveVersion = Versioned::get_versionnumber_by_stage(
80
				$this->ObjectClass, Versioned::LIVE, $this->ObjectID, false
81
			);
82
		}
83
84
		// Version comparisons
85
		if ($draftVersion == $liveVersion) {
86
			return self::CHANGE_NONE;
87
		} elseif (!$liveVersion) {
88
			return self::CHANGE_CREATED;
89
		} elseif (!$draftVersion) {
90
			return self::CHANGE_DELETED;
91
		} else {
92
			return self::CHANGE_MODIFIED;
93
		}
94
	}
95
96
	/**
97
	 * Find version of this object in the given stage
98
	 *
99
	 * @param string $stage
100
	 * @return Versioned|DataObject
101
	 */
102
	private function getObjectInStage($stage) {
103
		return Versioned::get_by_stage($this->ObjectClass, $stage)->byID($this->ObjectID);
104
	}
105
106
	/**
107
	 * Get all implicit objects for this change
108
	 *
109
	 * @return SS_List
110
	 */
111
	public function findReferenced() {
112
		if($this->getChangeType() === ChangeSetItem::CHANGE_DELETED) {
113
			// If deleted from stage, need to look at live record
114
			return $this->getObjectInStage(Versioned::LIVE)->findOwners(false);
115
		} else {
116
			// If changed on stage, look at owned objects there
117
			return $this->getObjectInStage(Versioned::DRAFT)->findOwned()->filterByCallback(function ($owned) {
118
				/** @var Versioned|DataObject $owned */
119
				return $owned->stagesDiffer(Versioned::DRAFT, Versioned::LIVE);
0 ignored issues
show
Bug introduced by
The method stagesDiffer does only exist in Versioned, but not in DataObject.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
120
			});
121
		}
122
	}
123
124
	/**
125
	 * Publish this item, then close it.
126
	 *
127
	 * Note: Unlike Versioned::doPublish() and Versioned::doUnpublish, this action is not recursive.
128
	 */
129
	public function publish() {
130
		// Logical checks prior to publish
131
		if(!$this->canPublish()) {
132
			throw new Exception("The current member does not have permission to publish this ChangeSetItem.");
133
		}
134
		if($this->VersionBefore || $this->VersionAfter) {
135
			throw new BadMethodCallException("This ChangeSetItem has already been published");
136
		}
137
138
		// Record state changed
139
		$this->VersionAfter = Versioned::get_versionnumber_by_stage(
0 ignored issues
show
Documentation introduced by
The property VersionAfter does not exist on object<ChangeSetItem>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
140
			$this->ObjectClass, Versioned::DRAFT, $this->ObjectID, false
141
		);
142
		$this->VersionBefore = Versioned::get_versionnumber_by_stage(
0 ignored issues
show
Documentation introduced by
The property VersionBefore does not exist on object<ChangeSetItem>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
143
			$this->ObjectClass, Versioned::LIVE, $this->ObjectID, false
144
		);
145
146
		switch($this->getChangeType()) {
147
			case static::CHANGE_NONE: {
148
				break;
149
			}
150
			case static::CHANGE_DELETED: {
151
				// Non-recursive delete
152
				$object = $this->getObjectInStage(Versioned::LIVE);
153
				$object->deleteFromStage(Versioned::LIVE);
154
				break;
155
			}
156
			case static::CHANGE_MODIFIED:
157
			case static::CHANGE_CREATED: {
158
				// Non-recursive publish
159
				$object = $this->getObjectInStage(Versioned::DRAFT);
160
				$object->publishSingle();
161
				break;
162
			}
163
		}
164
165
		$this->write();
166
	}
167
168
	/** Reverts this item, then close it. **/
169
	public function revert() {
170
		user_error('Not implemented', E_USER_ERROR);
171
	}
172
173
	public function canView($member = null) {
174
		return $this->can(__FUNCTION__, $member);
175
	}
176
177
	public function canEdit($member = null) {
178
		return $this->can(__FUNCTION__, $member);
179
	}
180
181
	public function canCreate($member = null, $context = array()) {
182
		return $this->can(__FUNCTION__, $member, $context);
183
	}
184
185
	public function canDelete($member = null) {
186
		return $this->can(__FUNCTION__, $member);
187
	}
188
189
	/**
190
	 * Check if the BeforeVersion of this changeset can be restored to draft
191
	 *
192
	 * @param Member $member
193
	 * @return bool
194
	 */
195
	public function canRevert($member) {
196
		// Just get the best version as this object may not even exist on either stage anymore.
197
		/** @var Versioned|DataObject $object */
198
		$object = Versioned::get_latest_version($this->ObjectClass, $this->ObjectID);
199
		if(!$object) {
200
			return false;
201
		}
202
203
		// Check change type
204
		switch($this->getChangeType()) {
205
			case static::CHANGE_CREATED: {
206
				// Revert creation by deleting from stage
207
				if(!$object->canDelete($member)) {
208
					return false;
209
				}
210
				break;
211
			}
212
			default: {
213
				// All other actions are typically editing draft stage
214
				if(!$object->canEdit($member)) {
215
					return false;
216
				}
217
				break;
218
			}
219
		}
220
221
		// If object can be published/unpublished let extensions deny
222
		return $this->can(__FUNCTION__, $member);
223
	}
224
225
	/**
226
	 * Check if this ChangeSetItem can be published
227
	 *
228
	 * @param Member $member
229
	 * @return bool
230
	 */
231
	public function canPublish($member = null) {
232
		// Check canMethod to invoke on object
233
		switch($this->getChangeType()) {
234 View Code Duplication
			case static::CHANGE_DELETED: {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
235
				/** @var Versioned|DataObject $object */
236
				$object = Versioned::get_by_stage($this->ObjectClass, Versioned::LIVE)->byID($this->ObjectID);
237
				if(!$object || !$object->canUnpublish($member)) {
0 ignored issues
show
Bug introduced by
It seems like $member defined by parameter $member on line 231 can also be of type object<Member>; however, Versioned::canUnpublish() does only seem to accept null, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
Bug introduced by
The method canUnpublish does only exist in Versioned, but not in DataObject.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
238
					return false;
239
				}
240
				break;
241
			}
242 View Code Duplication
			default: {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
243
				/** @var Versioned|DataObject $object */
244
				$object = Versioned::get_by_stage($this->ObjectClass, Versioned::DRAFT)->byID($this->ObjectID);
245
				if(!$object || !$object->canPublish($member)) {
0 ignored issues
show
Bug introduced by
The method canPublish does only exist in Versioned, but not in DataObject.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
246
					return false;
247
				}
248
				break;
249
			}
250
		}
251
252
		// If object can be published/unpublished let extensions deny
253
		return $this->can(__FUNCTION__, $member);
254
	}
255
256
	/**
257
	 * Default permissions for this ChangeSetItem
258
	 *
259
	 * @param string $perm
260
	 * @param Member $member
261
	 * @param array $context
262
	 * @return bool
263
	 */
264 View Code Duplication
	public function can($perm, $member = null, $context = array()) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
265
		if(!$member) {
266
			$member = Member::currentUser();
267
		}
268
269
		// Allow extensions to bypass default permissions, but only if
270
		// each change can be individually published.
271
		$extended = $this->extendedCan($perm, $member, $context);
0 ignored issues
show
Documentation introduced by
$member is of type object<DataObject>|null, but the function expects a object<Member>|integer.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
272
		if($extended !== null) {
273
			return $extended;
274
		}
275
276
		// Default permissions
277
		return (bool)Permission::checkMember($member, ChangeSet::config()->required_permission);
278
	}
279
280
}
281