Completed
Push — die-dist-conflicts-die ( ed850a...c24201 )
by Ingo
10:57
created

ChangeSet   C

Complexity

Total Complexity 55

Size/Duplication

Total Lines 474
Duplicated Lines 3.16 %

Coupling/Cohesion

Components 1
Dependencies 15

Importance

Changes 0
Metric Value
dl 15
loc 474
rs 6
c 0
b 0
f 0
wmc 55
lcom 1
cbo 15

17 Methods

Rating   Name   Duplication   Size   Complexity  
B publish() 0 26 5
B addObject() 0 26 3
A removeObject() 0 15 2
A implicitKey() 0 6 2
B calculateImplicit() 0 40 4
B sync() 0 35 4
A isSynced() 0 17 3
A canView() 0 3 1
A canEdit() 0 3 1
A canCreate() 0 3 1
A canDelete() 0 3 1
A canPublish() 0 12 3
A canRevert() 0 12 3
A can() 15 15 3
A getCMSFields() 0 9 2
F getDescription() 0 107 16
A fieldLabels() 0 7 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like ChangeSet often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ChangeSet, and based on these observations, apply Extract Interface, too.

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

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...
270
				$item->write();
271
			}
272
		});
273
	}
274
275
	/** Verify that any objects in this changeset include all owned changes */
276
	public function isSynced() {
277
		$implicit = $this->calculateImplicit();
278
279
		// Check the existing implicit ChangeSetItems for this ChangeSet
280
281
		foreach ($this->Changes()->filter(['Added' => ChangeSetItem::IMPLICITLY]) as $item) {
282
			$objectKey = $this->implicitKey($item);
283
284
			// If a ChangeSetItem exists, but isn't in $implicit -> validation failure
285
			if (!array_key_exists($objectKey, $implicit)) return false;
286
			// Exists, remove from $implicit
287
			unset($implicit[$objectKey]);
288
		}
289
290
		// If there's anything left in $implicit -> validation failure
291
		return empty($implicit);
292
	}
293
294
	public function canView($member = null) {
295
		return $this->can(__FUNCTION__, $member);
296
	}
297
298
	public function canEdit($member = null) {
299
		return $this->can(__FUNCTION__, $member);
300
	}
301
302
	public function canCreate($member = null, $context = array()) {
303
		return $this->can(__FUNCTION__, $member, $context);
304
	}
305
306
	public function canDelete($member = null) {
307
		return $this->can(__FUNCTION__, $member);
308
	}
309
310
	/**
311
	 * Check if this item is allowed to be published
312
	 *
313
	 * @param Member $member
314
	 * @return bool
315
	 */
316
	public function canPublish($member = null) {
317
		// All changes must be publishable
318
		foreach($this->Changes() as $change) {
319
			/** @var ChangeSetItem $change */
320
			if(!$change->canPublish($member)) {
321
				return false;
322
			}
323
		}
324
325
		// Default permission
326
		return $this->can(__FUNCTION__, $member);
327
	}
328
329
	/**
330
	 * Check if this changeset (if published) can be reverted
331
	 *
332
	 * @param Member $member
333
	 * @return bool
334
	 */
335
	public function canRevert($member = null) {
336
		// All changes must be publishable
337
		foreach($this->Changes() as $change) {
338
			/** @var ChangeSetItem $change */
339
			if(!$change->canRevert($member)) {
0 ignored issues
show
Bug introduced by
It seems like $member defined by parameter $member on line 335 can be null; however, SilverStripe\ORM\Version...ngeSetItem::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...
340
				return false;
341
			}
342
		}
343
344
		// Default permission
345
		return $this->can(__FUNCTION__, $member);
346
	}
347
348
	/**
349
	 * Default permissions for this changeset
350
	 *
351
	 * @param string $perm
352
	 * @param Member $member
353
	 * @param array $context
354
	 * @return bool
355
	 */
356 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...
357
		if(!$member) {
358
			$member = Member::currentUser();
359
		}
360
361
		// Allow extensions to bypass default permissions, but only if
362
		// each change can be individually published.
363
		$extended = $this->extendedCan($perm, $member, $context);
0 ignored issues
show
Bug introduced by
It seems like $member can be null; however, extendedCan() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
364
		if($extended !== null) {
365
			return $extended;
366
		}
367
368
		// Default permissions
369
		return (bool)Permission::checkMember($member, $this->config()->required_permission);
370
	}
371
372
	public function getCMSFields() {
373
		$fields = new FieldList(new TabSet('Root'));
374
		$fields->addFieldToTab('Root.Main', TextField::create('Name', $this->fieldLabel('Name')));
375
		if ($this->isInDB()) {
376
			$fields->addFieldToTab('Root.Main', ReadonlyField::create('State', $this->fieldLabel('State')));
377
		}
378
		$this->extend('updateCMSFields', $fields);
379
		return $fields;
380
	}
381
382
	/**
383
	 * Gets summary of items in changeset
384
	 *
385
	 * @return string
386
	 */
387
	public function getDescription() {
388
		// Initialise list of items to count
389
		$counted = [];
390
		$countedOther = 0;
391
		foreach($this->config()->important_classes as $type) {
392
			if(class_exists($type)) {
393
				$counted[$type] = 0;
394
			}
395
		}
396
397
		// Check each change item
398
		/** @var ChangeSetItem $change */
399
		foreach($this->Changes() as $change) {
400
			$found = false;
401
			foreach($counted as $class => $num) {
402
				if(is_a($change->ObjectClass, $class, true)) {
403
					$counted[$class]++;
404
					$found = true;
405
					break;
406
				}
407
			}
408
			if(!$found) {
409
				$countedOther++;
410
			}
411
		}
412
413
		// Describe set based on this output
414
		$counted = array_filter($counted);
415
416
		// Empty state
417
		if(empty($counted) && empty($countedOther)) {
418
			return '';
419
		}
420
421
		// Put all parts together
422
		$parts = [];
423
		foreach($counted as $class => $count) {
424
			$parts[] = DataObject::singleton($class)->i18n_pluralise($count);
425
		}
426
427
		// Describe non-important items
428
		if($countedOther) {
429
			if ($counted) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $counted of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
430
				$parts[] = i18n::pluralise(
431
					_t('ChangeSet.DESCRIPTION_OTHER_ITEM', 'other item'),
432
					_t('ChangeSet.DESCRIPTION_OTHER_ITEMS', 'other items'),
433
					$countedOther
434
				);
435
			} else {
436
				$parts[] = i18n::pluralise(
437
					_t('ChangeSet.DESCRIPTION_ITEM', 'item'),
438
					_t('ChangeSet.DESCRIPTION_ITEMS', 'items'),
439
					$countedOther
440
				);
441
			}
442
		}
443
444
		// Figure out how to join everything together
445
		if(empty($parts)) {
446
			return '';
447
		}
448
		if(count($parts) === 1) {
449
			return $parts[0];
450
		}
451
452
		// Non-comma list
453
		if(count($parts) === 2) {
454
			return _t(
455
				'ChangeSet.DESCRIPTION_AND',
456
				'{first} and {second}',
457
				[
0 ignored issues
show
Documentation introduced by
array('first' => $parts[... 'second' => $parts[1]) is of type array<string,?,{"first":"?","second":"?"}>, but the function expects a string.

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...
458
					'first' => $parts[0],
459
					'second' => $parts[1],
460
				]
461
			);
462
		}
463
464
		// First item
465
		$string = _t(
466
			'ChangeSet.DESCRIPTION_LIST_FIRST',
467
			'{item}',
468
			['item' => $parts[0]]
0 ignored issues
show
Documentation introduced by
array('item' => $parts[0]) is of type array<string,?,{"item":"?"}>, but the function expects a string.

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...
469
		);
470
471
		// Middle items
472
		for($i = 1; $i < count($parts) - 1; $i++) {
473
			$string = _t(
474
				'ChangeSet.DESCRIPTION_LIST_MID',
475
				'{list}, {item}',
476
				[
0 ignored issues
show
Documentation introduced by
array('list' => $string, 'item' => $parts[$i]) is of type array<string,?,{"list":"string","item":"?"}>, but the function expects a string.

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...
477
					'list' => $string,
478
					'item' => $parts[$i]
479
				]
480
			);
481
		}
482
483
		// Oxford comma
484
		$string = _t(
485
			'ChangeSet.DESCRIPTION_LIST_LAST',
486
			'{list}, and {item}',
487
			[
0 ignored issues
show
Documentation introduced by
array('list' => $string, 'item' => end($parts)) is of type array<string,?,{"list":"string","item":"?"}>, but the function expects a string.

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...
488
				'list' => $string,
489
				'item' => end($parts)
490
			]
491
		);
492
		return $string;
493
	}
494
495
	public function fieldLabels($includerelations = true) {
496
		$labels = parent::fieldLabels($includerelations);
497
		$labels['Name'] = _t('ChangeSet.NAME', 'Name');
498
		$labels['State'] = _t('ChangeSet.STATE', 'State');
499
500
		return $labels;
501
	}
502
}
503