Completed
Pull Request — master (#5247)
by Damian
11:17
created

ChangeSet::removeObject()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 15
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 15
rs 9.4285
cc 2
eloc 9
nc 2
nop 1
1
<?php
2
3
// namespace SilverStripe\Framework\Model\Versioning
4
5
/**
6
 * The ChangeSet model tracks several VersionedAndStaged objects for later publication as a single
7
 * atomic action
8
 *
9
 * @method HasManyList Changes()
10
 * @method Member Owner()
11
 * @property string $Name
12
 * @property string $State
13
 *
14
 * @package framework
15
 * @subpackage model
16
 */
17
class ChangeSet extends DataObject {
18
19
	private static $singular_name = 'Campaign';
20
21
	private static $plural_name = 'Campaigns';
22
23
	/** An active changeset */
24
	const STATE_OPEN = 'open';
25
26
	/** A changeset which is reverted and closed */
27
	const STATE_REVERTED = 'reverted';
28
29
	/** A changeset which is published and closed */
30
	const STATE_PUBLISHED = 'published';
31
32
	private static $db = array(
33
		'Name'  => 'Varchar',
34
		'State' => "Enum('open,published,reverted')",
35
	);
36
37
	private static $has_many = array(
38
		'Changes' => 'ChangeSetItem',
39
	);
40
41
	private static $defaults = array(
42
		'State' => 'open'
43
	);
44
45
	private static $has_one = array(
46
		'Owner' => 'Member',
47
	);
48
49
	/**
50
	 * Default permission to require for publishers.
51
	 * Publishers must either be able to use the campaign admin, or have all admin access.
52
	 *
53
	 * Also used as default permission for ChangeSetItem default permission.
54
	 *
55
	 * @config
56
	 * @var array
57
	 */
58
	private static $required_permission = array('CMS_ACCESS_CampaignAdmin', 'CMS_ACCESS_LeftAndMain');
59
60
	/**
61
	 * Publish this changeset, then closes it.
62
	 *
63
	 * @throws Exception
64
	 */
65
	public function publish() {
66
		// Logical checks prior to publish
67
		if($this->State !== static::STATE_OPEN) {
68
			throw new BadMethodCallException(
69
				"ChangeSet can't be published if it has been already published or reverted."
70
			);
71
		}
72
		if(!$this->isSynced()) {
73
			throw new ValidationException(
74
				"ChangeSet does not include all necessary changes and cannot be published."
75
			);
76
		}
77
		if(!$this->canPublish()) {
78
			throw new Exception("The current member does not have permission to publish this ChangeSet.");
79
		}
80
81
		DB::get_conn()->withTransaction(function(){
82
			foreach($this->Changes() as $change) {
83
				/** @var ChangeSetItem $change */
84
				$change->publish();
85
			}
86
87
			$this->State = static::STATE_PUBLISHED;
88
			$this->write();
89
		});
90
	}
91
92
	/**
93
	 * Add a new change to this changeset. Will automatically include all owned
94
	 * changes as those are dependencies of this item.
95
	 *
96
	 * @param DataObject $object
97
	 */
98
	public function addObject(DataObject $object) {
99
		if(!$this->isInDB()) {
100
			throw new BadMethodCallException("ChangeSet must be saved before adding items");
101
		}
102
103
		$references = [
104
			'ObjectID'    => $object->ID,
105
			'ObjectClass' => $object->ClassName
106
		];
107
108
		// Get existing item in case already added
109
		$item = $this->Changes()->filter($references)->first();
110
111
		if (!$item) {
112
			$item = new ChangeSetItem($references);
113
			$this->Changes()->add($item);
114
		}
115
116
		$item->ReferencedBy()->removeAll();
117
118
		$item->Added = ChangeSetItem::EXPLICITLY;
0 ignored issues
show
Documentation introduced by
The property Added does not exist on object<DataObject>. 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...
119
		$item->write();
120
121
122
		$this->sync();
123
	}
124
125
	/**
126
	 * Remove an item from this changeset. Will automatically remove all changes
127
	 * which own (and thus depend on) the removed item.
128
	 *
129
	 * @param DataObject $object
130
	 */
131
	public function removeObject(DataObject $object) {
132
		$item = ChangeSetItem::get()->filter([
133
				'ObjectID' => $object->ID,
134
				'ObjectClass' => $object->ClassName,
135
				'ChangeSetID' => $this->ID
136
			])->first();
137
138
		if ($item) {
139
			// TODO: Handle case of implicit added item being removed.
140
141
			$item->delete();
142
		}
143
144
		$this->sync();
145
	}
146
147
	protected function implicitKey($item) {
148
		if ($item instanceof ChangeSetItem) return $item->ObjectClass.'.'.$item->ObjectID;
149
		return $item->ClassName.'.'.$item->ID;
150
	}
151
152
	protected function calculateImplicit() {
153
		/** @var string[][] $explicit List of all items that have been explicitly added to this ChangeSet */
154
		$explicit = array();
155
156
		/** @var string[][] $referenced List of all items that are "referenced" by items in $explicit */
157
		$referenced = array();
158
159
		/** @var string[][] $references List of which explicit items reference each thing in referenced */
160
		$references = array();
161
162
		foreach ($this->Changes()->filter(['Added' => ChangeSetItem::EXPLICITLY]) as $item) {
163
			$explicitKey = $this->implicitKey($item);
164
			$explicit[$explicitKey] = true;
165
166
			foreach ($item->findReferenced() as $referee) {
167
				$key = $this->implicitKey($referee);
168
169
				$referenced[$key] = [
170
					'ObjectID' => $referee->ID,
171
					'ObjectClass' => $referee->ClassName
172
				];
173
174
				$references[$key][] = $item->ID;
175
			}
176
		}
177
178
		/** @var string[][] $explicit List of all items that are either in $explicit, $referenced or both */
179
		$all = array_merge($referenced, $explicit);
180
181
		/** @var string[][] $implicit Anything that is in $all, but not in $explicit, is an implicit inclusion */
182
		$implicit = array_diff_key($all, $explicit);
183
184
		foreach($implicit as $key => $object) {
185
			$implicit[$key]['ReferencedBy'] = $references[$key];
186
		}
187
188
		return $implicit;
189
	}
190
191
	/**
192
	 * Add implicit changes that should be included in this changeset
193
	 *
194
	 * When an item is created or changed, all it's owned items which have
195
	 * changes are implicitly added
196
	 *
197
	 * When an item is deleted, it's owner (even if that owner does not have changes)
198
	 * is implicitly added
199
	 */
200
	public function sync() {
201
		// Start a transaction (if we can)
202
		DB::get_conn()->withTransaction(function() {
203
204
			// Get the implicitly included items for this ChangeSet
205
			$implicit = $this->calculateImplicit();
206
207
			// Adjust the existing implicit ChangeSetItems for this ChangeSet
208
			foreach ($this->Changes()->filter(['Added' => ChangeSetItem::IMPLICITLY]) as $item) {
209
				$objectKey = $this->implicitKey($item);
210
211
				// If a ChangeSetItem exists, but isn't in $implicit, it's no longer required, so delete it
212
				if (!array_key_exists($objectKey, $implicit)) {
213
					$item->delete();
214
				}
215
				// Otherwise it is required, so update ReferencedBy and remove from $implicit
216
				else {
217
					$item->ReferencedBy()->setByIDList($implicit[$objectKey]['ReferencedBy']);
218
					unset($implicit[$objectKey]);
219
				}
220
			}
221
222
			// Now $implicit is all those items that are implicitly included, but don't currently have a ChangeSetItem.
223
			// So create new ChangeSetItems to match
224
225
			foreach ($implicit as $key => $props) {
226
				$item = new ChangeSetItem($props);
227
				$item->Added = ChangeSetItem::IMPLICITLY;
228
				$item->ChangeSetID = $this->ID;
0 ignored issues
show
Documentation introduced by
The property ChangeSetID 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...
229
				$item->ReferencedBy()->setByIDList($props['ReferencedBy']);
230
				$item->write();
231
			}
232
		});
233
	}
234
235
	/** Verify that any objects in this changeset include all owned changes */
236
	public function isSynced() {
237
		$implicit = $this->calculateImplicit();
238
239
		// Check the existing implicit ChangeSetItems for this ChangeSet
240
241
		foreach ($this->Changes()->filter(['Added' => ChangeSetItem::IMPLICITLY]) as $item) {
242
			$objectKey = $this->implicitKey($item);
243
244
			// If a ChangeSetItem exists, but isn't in $implicit -> validation failure
245
			if (!array_key_exists($objectKey, $implicit)) return false;
246
			// Exists, remove from $implicit
247
			unset($implicit[$objectKey]);
248
		}
249
250
		// If there's anything left in $implicit -> validation failure
251
		return empty($implicit);
252
	}
253
254
	public function canView($member = null) {
255
		return $this->can(__FUNCTION__, $member);
256
	}
257
258
	public function canEdit($member = null) {
259
		return $this->can(__FUNCTION__, $member);
260
	}
261
262
	public function canCreate($member = null, $context = array()) {
263
		return $this->can(__FUNCTION__, $member, $context);
264
	}
265
266
	public function canDelete($member = null) {
267
		return $this->can(__FUNCTION__, $member);
268
	}
269
270
	/**
271
	 * Check if this item is allowed to be published
272
	 *
273
	 * @param Member $member
274
	 * @return bool
275
	 */
276
	public function canPublish($member = null) {
277
		// All changes must be publishable
278
		foreach($this->Changes() as $change) {
279
			/** @var ChangeSetItem $change */
280
			if(!$change->canPublish($member)) {
281
				return false;
282
			}
283
		}
284
285
		// Default permission
286
		return $this->can(__FUNCTION__, $member);
287
	}
288
289
	/**
290
	 * Check if this changeset (if published) can be reverted
291
	 *
292
	 * @param Member $member
293
	 * @return bool
294
	 */
295
	public function canRevert($member = null) {
296
		// All changes must be publishable
297
		foreach($this->Changes() as $change) {
298
			/** @var ChangeSetItem $change */
299
			if(!$change->canRevert($member)) {
0 ignored issues
show
Bug introduced by
It seems like $member defined by parameter $member on line 295 can be null; however, ChangeSetItem::canRevert() does not accept null, maybe add an additional type check?

It seems like you allow that null is being passed for a parameter, however the function which is called does not seem to accept null.

We recommend to add an additional type check (or disallow null for the parameter):

function notNullable(stdClass $x) { }

// Unsafe
function withoutCheck(stdClass $x = null) {
    notNullable($x);
}

// Safe - Alternative 1: Adding Additional Type-Check
function withCheck(stdClass $x = null) {
    if ($x instanceof stdClass) {
        notNullable($x);
    }
}

// Safe - Alternative 2: Changing Parameter
function withNonNullableParam(stdClass $x) {
    notNullable($x);
}
Loading history...
300
				return false;
301
			}
302
		}
303
304
		// Default permission
305
		return $this->can(__FUNCTION__, $member);
306
	}
307
308
	/**
309
	 * Default permissions for this changeset
310
	 *
311
	 * @param string $perm
312
	 * @param Member $member
313
	 * @param array $context
314
	 * @return bool
315
	 */
316
	public function can($perm, $member = null, $context = array()) {
317
		if(!$member) {
318
			$member = Member::currentUser();
319
		}
320
321
		// Allow extensions to bypass default permissions, but only if
322
		// each change can be individually published.
323
		$extended = $this->extendedCan($perm, $member, $context);
324
		if($extended !== null) {
325
			return $extended;
326
		}
327
328
		// Default permissions
329
		return (bool)Permission::checkMember($member, $this->config()->required_permission);
330
	}
331
}
332