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

Versioned::publish()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 0 Features 1
Metric Value
c 4
b 0
f 1
dl 0
loc 4
rs 10
cc 1
eloc 3
nc 1
nop 3
1
<?php
2
3
// namespace SilverStripe\Framework\Model\Versioning
4
5
/**
6
 * The Versioned extension allows your DataObjects to have several versions,
7
 * allowing you to rollback changes and view history. An example of this is
8
 * the pages used in the CMS.
9
 *
10
 * @property int $Version
11
 * @property DataObject|Versioned $owner
12
 *
13
 * @package framework
14
 * @subpackage model
15
 */
16
class Versioned extends DataExtension implements TemplateGlobalProvider {
17
18
	/**
19
	 * Versioning mode for this object.
20
	 * Note: Not related to the current versioning mode in the state / session
21
	 * Will be one of 'StagedVersioned' or 'Versioned';
22
	 *
23
	 * @var string
24
	 */
25
	protected $mode;
26
27
	/**
28
	 * The default reading mode
29
	 */
30
	const DEFAULT_MODE = 'Stage.Live';
31
32
	/**
33
	 * Constructor arg to specify that staging is active on this record.
34
	 * 'Staging' implies that 'Versioning' is also enabled.
35
	 */
36
	const STAGEDVERSIONED = 'StagedVersioned';
37
38
	/**
39
	 * Constructor arg to specify that versioning only is active on this record.
40
	 */
41
	const VERSIONED = 'Versioned';
42
43
	/**
44
	 * The Public stage.
45
	 */
46
	const LIVE = 'Live';
47
48
	/**
49
	 * The draft (default) stage
50
	 */
51
	const DRAFT = 'Stage';
52
53
	/**
54
	 * A version that a DataObject should be when it is 'migrating',
55
	 * that is, when it is in the process of moving from one stage to another.
56
	 * @var string
57
	 */
58
	public $migratingVersion;
59
60
	/**
61
	 * A cache used by get_versionnumber_by_stage().
62
	 * Clear through {@link flushCache()}.
63
	 *
64
	 * @var array
65
	 */
66
	protected static $cache_versionnumber;
67
68
	/**
69
	 * Current reading mode
70
	 *
71
	 * @var string
72
	 */
73
	protected static $reading_mode = null;
74
75
	/**
76
	 * @var Boolean Flag which is temporarily changed during the write() process
77
	 * to influence augmentWrite() behaviour. If set to TRUE, no new version will be created
78
	 * for the following write. Needs to be public as other classes introspect this state
79
	 * during the write process in order to adapt to this versioning behaviour.
80
	 */
81
	public $_nextWriteWithoutVersion = false;
82
83
	/**
84
	 * Additional database columns for the new
85
	 * "_versions" table. Used in {@link augmentDatabase()}
86
	 * and all Versioned calls extending or creating
87
	 * SELECT statements.
88
	 *
89
	 * @var array $db_for_versions_table
90
	 */
91
	private static $db_for_versions_table = array(
92
		"RecordID" => "Int",
93
		"Version" => "Int",
94
		"WasPublished" => "Boolean",
95
		"AuthorID" => "Int",
96
		"PublisherID" => "Int"
97
	);
98
99
	/**
100
	 * @var array
101
	 */
102
	private static $db = array(
103
		'Version' => 'Int'
104
	);
105
106
	/**
107
	 * Used to enable or disable the prepopulation of the version number cache.
108
	 * Defaults to true.
109
	 *
110
	 * @config
111
	 * @var boolean
112
	 */
113
	private static $prepopulate_versionnumber_cache = true;
114
115
	/**
116
	 * Keep track of the archive tables that have been created.
117
	 *
118
	 * @config
119
	 * @var array
120
	 */
121
	private static $archive_tables = array();
122
123
	/**
124
	 * Additional database indexes for the new
125
	 * "_versions" table. Used in {@link augmentDatabase()}.
126
	 *
127
	 * @var array $indexes_for_versions_table
128
	 */
129
	private static $indexes_for_versions_table = array(
130
		'RecordID_Version' => '("RecordID","Version")',
131
		'RecordID' => true,
132
		'Version' => true,
133
		'AuthorID' => true,
134
		'PublisherID' => true,
135
	);
136
137
138
	/**
139
	 * An array of DataObject extensions that may require versioning for extra tables
140
	 * The array value is a set of suffixes to form these table names, assuming a preceding '_'.
141
	 * E.g. if Extension1 creates a new table 'Class_suffix1'
142
	 * and Extension2 the tables 'Class_suffix2' and 'Class_suffix3':
143
	 *
144
	 * 	$versionableExtensions = array(
145
	 * 		'Extension1' => 'suffix1',
146
	 * 		'Extension2' => array('suffix2', 'suffix3'),
147
	 * 	);
148
	 *
149
	 * This can also be manipulated by updating the current loaded config
150
	 *
151
	 * SiteTree:
152
	 *   versionableExtensions:
153
	 *     - Extension1:
154
	 *       - suffix1
155
	 *       - suffix2
156
	 *     - Extension2:
157
	 *       - suffix1
158
	 *       - suffix2
159
	 *
160
	 * or programatically:
161
	 *
162
	 *  Config::inst()->update($this->owner->class, 'versionableExtensions',
163
	 *  array('Extension1' => 'suffix1', 'Extension2' => array('suffix2', 'suffix3')));
164
	 *
165
	 *
166
	 * Make sure your extension has a static $enabled-property that determines if it is
167
	 * processed by Versioned.
168
	 *
169
	 * @config
170
	 * @var array
171
	 */
172
	private static $versionableExtensions = array('Translatable' => 'lang');
173
174
	/**
175
	 * Permissions necessary to view records outside of the live stage (e.g. archive / draft stage).
176
	 *
177
	 * @config
178
	 * @var array
179
	 */
180
	private static $non_live_permissions = array('CMS_ACCESS_LeftAndMain', 'CMS_ACCESS_CMSMain', 'VIEW_DRAFT_CONTENT');
181
182
	/**
183
	 * List of relationships on this object that are "owned" by this object.
184
	 * Owership in the context of versioned objects is a relationship where
185
	 * the publishing of owning objects requires the publishing of owned objects.
186
	 *
187
	 * E.g. A page owns a set of banners, as in order for the page to be published, all
188
	 * banners on this page must also be published for it to be visible.
189
	 *
190
	 * Typically any object and its owned objects should be visible in the same edit view.
191
	 * E.g. a page and {@see GridField} of banners.
192
	 *
193
	 * Page hierarchy is typically not considered an ownership relationship.
194
	 *
195
	 * Ownership is recursive; If A owns B and B owns C then A owns C.
196
	 *
197
	 * @config
198
	 * @var array List of has_many or many_many relationships owned by this object.
199
	 */
200
	private static $owns = array();
201
202
	/**
203
	 * Opposing relationship to owns config; Represents the objects which
204
	 * own the current object.
205
	 *
206
	 * @var array
207
	 */
208
	private static $owned_by = array();
209
210
	/**
211
	 * Reset static configuration variables to their default values.
212
	 */
213
	public static function reset() {
214
		self::$reading_mode = '';
215
		Session::clear('readingMode');
216
	}
217
218
	/**
219
	 * Amend freshly created DataQuery objects with versioned-specific
220
	 * information.
221
	 *
222
	 * @param SQLSelect
223
	 * @param DataQuery
224
	 */
225
	public function augmentDataQueryCreation(SQLSelect &$query, DataQuery &$dataQuery) {
0 ignored issues
show
Unused Code introduced by
The parameter $query is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
226
		$parts = explode('.', Versioned::get_reading_mode());
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
227
228
		if($parts[0] == 'Archive') {
229
			$dataQuery->setQueryParam('Versioned.mode', 'archive');
230
			$dataQuery->setQueryParam('Versioned.date', $parts[1]);
231
		} else if($parts[0] == 'Stage' && $this->hasStages()) {
232
			$dataQuery->setQueryParam('Versioned.mode', 'stage');
233
			$dataQuery->setQueryParam('Versioned.stage', $parts[1]);
234
		}
235
	}
236
237
	/**
238
	 * Construct a new Versioned object.
239
	 *
240
	 * @var string $mode One of "StagedVersioned" or "Versioned".
241
	 */
242
	public function __construct($mode = self::STAGEDVERSIONED) {
243
		parent::__construct();
244
245
		// Handle deprecated behaviour
246
		if($mode === 'Stage' && func_num_args() === 1) {
247
			Deprecation::notice("5.0", "Versioned now takes a mode as a single parameter");
248
			$mode = static::VERSIONED;
249
		} elseif(is_array($mode) || func_num_args() > 1) {
250
			Deprecation::notice("5.0", "Versioned now takes a mode as a single parameter");
251
			$mode = func_num_args() > 1 || count($mode) > 1
252
				? static::STAGEDVERSIONED
253
				: static::VERSIONED;
254
		}
255
256
		if(!in_array($mode, array(static::STAGEDVERSIONED, static::VERSIONED))) {
257
			throw new InvalidArgumentException("Invalid mode: {$mode}");
258
		}
259
260
		$this->mode = $mode;
261
	}
262
263
	/**
264
	 * Cache of version to modified dates for this objects
265
	 *
266
	 * @var array
267
	 */
268
	protected $versionModifiedCache = array();
269
270
	/**
271
	 * Get modified date for the given version
272
	 *
273
	 * @param int $version
274
	 * @return string
275
	 */
276
	protected function getLastEditedForVersion($version) {
277
		// Cache key
278
		$baseTable = ClassInfo::baseDataClass($this->owner);
279
		$id = $this->owner->ID;
0 ignored issues
show
Bug introduced by
The property ID does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
280
		$key = "{$baseTable}#{$id}/{$version}";
281
282
		// Check cache
283
		if(isset($this->versionModifiedCache[$key])) {
284
			return $this->versionModifiedCache[$key];
285
		}
286
287
		// Build query
288
		$table = "\"{$baseTable}_versions\"";
289
		$query = SQLSelect::create('"LastEdited"', $table)
290
			->addWhere([
291
				"{$table}.\"RecordID\"" => $id,
292
				"{$table}.\"Version\"" => $version
293
			]);
294
		$date = $query->execute()->value();
295
		if($date) {
296
			$this->versionModifiedCache[$key] = $date;
297
		}
298
		return $date;
299
	}
300
301
302
	public function updateInheritableQueryParams(&$params) {
303
		// Skip if versioned isn't set
304
		if(!isset($params['Versioned.mode'])) {
305
			return;
306
		}
307
308
		// Adjust query based on original selection criterea
309
		$owner = $this->owner;
310
		switch($params['Versioned.mode']) {
311
			case 'all_versions': {
0 ignored issues
show
Coding Style introduced by
CASE statements must be defined using a colon

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
312
				// Versioned.mode === all_versions doesn't inherit very well, so default to stage
313
				$params['Versioned.mode'] = 'stage';
314
				$params['Versioned.stage'] = static::DRAFT;
315
				break;
316
			}
317
			case 'version': {
0 ignored issues
show
Coding Style introduced by
CASE statements must be defined using a colon

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
318
				// If we selected this object from a specific version, we need
319
				// to find the date this version was published, and ensure
320
				// inherited queries select from that date.
321
				$version = $params['Versioned.version'];
322
				$date = $this->getLastEditedForVersion($version);
323
324
				// Filter related objects at the same date as this version
325
				unset($params['Versioned.version']);
326
				if($date) {
327
					$params['Versioned.mode'] = 'archive';
328
					$params['Versioned.date'] = $date;
329
				} else {
330
					// Fallback to default
331
					$params['Versioned.mode'] = 'stage';
332
					$params['Versioned.stage'] = static::DRAFT;
333
				}
334
				break;
335
			}
336
		}
337
	}
338
339
	/**
340
	 * Augment the the SQLSelect that is created by the DataQuery
341
	 *
342
	 * See {@see augmentLazyLoadFields} for lazy-loading applied prior to this.
343
	 *
344
	 * @param SQLSelect $query
345
	 * @param DataQuery $dataQuery
346
	 * @throws InvalidArgumentException
347
	 */
348
	public function augmentSQL(SQLSelect $query, DataQuery $dataQuery = null) {
349
		if(!$dataQuery || !$dataQuery->getQueryParam('Versioned.mode')) {
350
			return;
351
		}
352
353
		$baseTable = ClassInfo::baseDataClass($dataQuery->dataClass());
354
355
		$versionedMode = $dataQuery->getQueryParam('Versioned.mode');
356
		switch($versionedMode) {
357
		// Reading a specific stage (Stage or Live)
358
		case 'stage':
359
			// Check if we need to rewrite this table
360
			$stage = $dataQuery->getQueryParam('Versioned.stage');
361
			if(!$this->hasStages() || $stage === static::DRAFT) {
362
				break;
363
			}
364
			// Rewrite all tables to select from the live version
365
			foreach($query->getFrom() as $table => $dummy) {
366
				if(!$this->isTableVersioned($table)) {
367
					continue;
368
				}
369
				$stageTable = $this->stageTable($table, $stage);
370
				$query->renameTable($table, $stageTable);
371
			}
372
			break;
373
374
		// Reading a specific stage, but only return items that aren't in any other stage
375
		case 'stage_unique':
376
			if(!$this->hasStages()) {
377
				break;
378
			}
379
380
			$stage = $dataQuery->getQueryParam('Versioned.stage');
381
			// Recurse to do the default stage behavior (must be first, we rely on stage renaming happening before
382
			// below)
383
			$dataQuery->setQueryParam('Versioned.mode', 'stage');
384
			$this->augmentSQL($query, $dataQuery);
385
			$dataQuery->setQueryParam('Versioned.mode', 'stage_unique');
386
387
			// Now exclude any ID from any other stage. Note that we double rename to avoid the regular stage rename
388
			// renaming all subquery references to be Versioned.stage
389
			foreach([static::DRAFT, static::LIVE] as $excluding) {
390
				if ($excluding == $stage) {
391
					continue;
392
				}
393
394
				$tempName = 'ExclusionarySource_'.$excluding;
395
				$excludingTable = $baseTable . ($excluding && $excluding != static::DRAFT ? "_$excluding" : '');
396
397
				$query->addWhere('"'.$baseTable.'"."ID" NOT IN (SELECT "ID" FROM "'.$tempName.'")');
398
				$query->renameTable($tempName, $excludingTable);
399
			}
400
			break;
401
402
		// Return all version instances
403
		case 'archive':
404
		case 'all_versions':
405
		case 'latest_versions':
406
		case 'version':
407
			foreach($query->getFrom() as $alias => $join) {
408
				if(!$this->isTableVersioned($alias)) {
409
					continue;
410
				}
411
412
				if($alias != $baseTable) {
413
					// Make sure join includes version as well
414
					$query->setJoinFilter(
415
						$alias,
416
						"\"{$alias}_versions\".\"RecordID\" = \"{$baseTable}_versions\".\"RecordID\""
417
						. " AND \"{$alias}_versions\".\"Version\" = \"{$baseTable}_versions\".\"Version\""
418
					);
419
				}
420
				$query->renameTable($alias, $alias . '_versions');
421
			}
422
423
			// Add all <basetable>_versions columns
424
			foreach(Config::inst()->get('Versioned', 'db_for_versions_table') as $name => $type) {
0 ignored issues
show
Bug introduced by
The expression \Config::inst()->get('Ve...db_for_versions_table') of type array|integer|double|string|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
425
				$query->selectField(sprintf('"%s_versions"."%s"', $baseTable, $name), $name);
426
			}
427
428
			// Alias the record ID as the row ID, and ensure ID filters are aliased correctly
429
			$query->selectField("\"{$baseTable}_versions\".\"RecordID\"", "ID");
430
			$query->replaceText("\"{$baseTable}_versions\".\"ID\"", "\"{$baseTable}_versions\".\"RecordID\"");
431
432
			// However, if doing count, undo rewrite of "ID" column
433
			$query->replaceText(
434
				"count(DISTINCT \"{$baseTable}_versions\".\"RecordID\")",
435
				"count(DISTINCT \"{$baseTable}_versions\".\"ID\")"
436
			);
437
438
			// Add additional versioning filters
439
			switch($versionedMode) {
440
				case 'archive': {
0 ignored issues
show
Coding Style introduced by
CASE statements must be defined using a colon

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
441
					$date = $dataQuery->getQueryParam('Versioned.date');
442
					if(!$date) {
443
						throw new InvalidArgumentException("Invalid archive date");
444
					}
445
					// Link to the version archived on that date
446
					$query->addWhere([
447
						"\"{$baseTable}_versions\".\"Version\" IN
448
						(SELECT LatestVersion FROM
449
							(SELECT
450
								\"{$baseTable}_versions\".\"RecordID\",
451
								MAX(\"{$baseTable}_versions\".\"Version\") AS LatestVersion
452
								FROM \"{$baseTable}_versions\"
453
								WHERE \"{$baseTable}_versions\".\"LastEdited\" <= ?
454
								GROUP BY \"{$baseTable}_versions\".\"RecordID\"
455
							) AS \"{$baseTable}_versions_latest\"
456
							WHERE \"{$baseTable}_versions_latest\".\"RecordID\" = \"{$baseTable}_versions\".\"RecordID\"
457
						)" => $date
458
					]);
459
					break;
460
				}
461
				case 'latest_versions': {
0 ignored issues
show
Coding Style introduced by
CASE statements must be defined using a colon

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
462
					// Return latest version instances, regardless of whether they are on a particular stage
463
					// This provides "show all, including deleted" functonality
464
					$query->addWhere(
465
						"\"{$baseTable}_versions\".\"Version\" IN
466
						(SELECT LatestVersion FROM
467
							(SELECT
468
								\"{$baseTable}_versions\".\"RecordID\",
469
								MAX(\"{$baseTable}_versions\".\"Version\") AS LatestVersion
470
								FROM \"{$baseTable}_versions\"
471
								GROUP BY \"{$baseTable}_versions\".\"RecordID\"
472
							) AS \"{$baseTable}_versions_latest\"
473
							WHERE \"{$baseTable}_versions_latest\".\"RecordID\" = \"{$baseTable}_versions\".\"RecordID\"
474
						)"
475
					);
476
					break;
477
				}
478
				case 'version': {
0 ignored issues
show
Coding Style introduced by
CASE statements must be defined using a colon

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
479
					// If selecting a specific version, filter it here
480
					$version = $dataQuery->getQueryParam('Versioned.version');
481
					if(!$version) {
482
						throw new InvalidArgumentException("Invalid version");
483
					}
484
					$query->addWhere([
485
						"\"{$baseTable}_versions\".\"Version\"" => $version
486
					]);
487
					break;
488
				}
489
				default: {
0 ignored issues
show
Coding Style introduced by
DEFAULT statements must be defined using a colon

As per the PSR-2 coding standard, default statements should not be wrapped in curly braces.

switch ($expr) {
    default: { //wrong
        doSomething();
        break;
    }
}

switch ($expr) {
    default: //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
490
					// If all versions are requested, ensure that records are sorted by this field
491
					$query->addOrderBy(sprintf('"%s_versions"."%s"', $baseTable, 'Version'));
492
					break;
493
				}
494
			}
495
			break;
496
		default:
497
			throw new InvalidArgumentException("Bad value for query parameter Versioned.mode: "
498
				. $dataQuery->getQueryParam('Versioned.mode'));
499
		}
500
	}
501
502
	/**
503
	 * Determine if the given versioned table is a part of the sub-tree of the current dataobject
504
	 * This helps prevent rewriting of other tables that get joined in, in particular, many_many tables
505
	 *
506
	 * @param string $table
507
	 * @return bool True if this table should be versioned
508
	 */
509
	protected function isTableVersioned($table) {
510
		if(!class_exists($table)) {
511
			return false;
512
		}
513
		$baseClass = ClassInfo::baseDataClass($this->owner);
514
		return is_a($table, $baseClass, true);
515
	}
516
517
	/**
518
	 * For lazy loaded fields requiring extra sql manipulation, ie versioning.
519
	 *
520
	 * @param SQLSelect $query
521
	 * @param DataQuery $dataQuery
522
	 * @param DataObject $dataObject
523
	 */
524
	public function augmentLoadLazyFields(SQLSelect &$query, DataQuery &$dataQuery = null, $dataObject) {
0 ignored issues
show
Unused Code introduced by
The parameter $query is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
525
		// The VersionedMode local variable ensures that this decorator only applies to
526
		// queries that have originated from the Versioned object, and have the Versioned
527
		// metadata set on the query object. This prevents regular queries from
528
		// accidentally querying the *_versions tables.
529
		$versionedMode = $dataObject->getSourceQueryParam('Versioned.mode');
530
		$dataClass = ClassInfo::baseDataClass($dataQuery->dataClass());
0 ignored issues
show
Bug introduced by
It seems like $dataQuery is not always an object, but can also be of type null. Maybe add an additional type check?

If a variable is not always an object, we recommend to add an additional type check to ensure your method call is safe:

function someFunction(A $objectMaybe = null)
{
    if ($objectMaybe instanceof A) {
        $objectMaybe->doSomething();
    }
}
Loading history...
531
		$modesToAllowVersioning = array('all_versions', 'latest_versions', 'archive', 'version');
532
		if(
533
			!empty($dataObject->Version) &&
534
			(!empty($versionedMode) && in_array($versionedMode,$modesToAllowVersioning))
535
		) {
536
			// This will ensure that augmentSQL will select only the same version as the owner,
537
			// regardless of how this object was initially selected
538
			$dataQuery->where([
539
				"\"$dataClass\".\"Version\"" => $dataObject->Version
540
			]);
541
			$dataQuery->setQueryParam('Versioned.mode', 'all_versions');
542
		}
543
	}
544
545
546
	/**
547
	 * Called by {@link SapphireTest} when the database is reset.
548
	 *
549
	 * @todo Reduce the coupling between this and SapphireTest, somehow.
550
	 */
551
	public static function on_db_reset() {
552
		// Drop all temporary tables
553
		$db = DB::get_conn();
554
		foreach(static::$archive_tables as $tableName) {
0 ignored issues
show
Bug introduced by
Since $archive_tables is declared private, accessing it with static will lead to errors in possible sub-classes; consider using self, or increasing the visibility of $archive_tables to at least protected.

Let’s assume you have a class which uses late-static binding:

class YourClass
{
    private static $someVariable;

    public static function getSomeVariable()
    {
        return static::$someVariable;
    }
}

The code above will run fine in your PHP runtime. However, if you now create a sub-class and call the getSomeVariable() on that sub-class, you will receive a runtime error:

class YourSubClass extends YourClass { }

YourSubClass::getSomeVariable(); // Will cause an access error.

In the case above, it makes sense to update SomeClass to use self instead:

class SomeClass
{
    private static $someVariable;

    public static function getSomeVariable()
    {
        return self::$someVariable; // self works fine with private.
    }
}
Loading history...
555
			if(method_exists($db, 'dropTable')) $db->dropTable($tableName);
556
			else $db->query("DROP TABLE \"$tableName\"");
557
		}
558
559
		// Remove references to them
560
		static::$archive_tables = array();
0 ignored issues
show
Bug introduced by
Since $archive_tables is declared private, accessing it with static will lead to errors in possible sub-classes; consider using self, or increasing the visibility of $archive_tables to at least protected.

Let’s assume you have a class which uses late-static binding:

class YourClass
{
    private static $someVariable;

    public static function getSomeVariable()
    {
        return static::$someVariable;
    }
}

The code above will run fine in your PHP runtime. However, if you now create a sub-class and call the getSomeVariable() on that sub-class, you will receive a runtime error:

class YourSubClass extends YourClass { }

YourSubClass::getSomeVariable(); // Will cause an access error.

In the case above, it makes sense to update SomeClass to use self instead:

class SomeClass
{
    private static $someVariable;

    public static function getSomeVariable()
    {
        return self::$someVariable; // self works fine with private.
    }
}
Loading history...
561
	}
562
563
	public function augmentDatabase() {
564
		$owner = $this->owner;
565
		$classTable = $owner->class;
566
567
		$isRootClass = ($owner->class == ClassInfo::baseDataClass($owner->class));
568
569
		// Build a list of suffixes whose tables need versioning
570
		$allSuffixes = array();
571
		$versionableExtensions = $owner->config()->versionableExtensions;
572
		if(count($versionableExtensions)){
573
			foreach ($versionableExtensions as $versionableExtension => $suffixes) {
574
				if ($owner->hasExtension($versionableExtension)) {
575
					$allSuffixes = array_merge($allSuffixes, (array)$suffixes);
576
					foreach ((array)$suffixes as $suffix) {
577
						$allSuffixes[$suffix] = $versionableExtension;
578
					}
579
				}
580
			}
581
		}
582
583
		// Add the default table with an empty suffix to the list (table name = class name)
584
		array_push($allSuffixes,'');
585
586
		foreach ($allSuffixes as $key => $suffix) {
587
			// check that this is a valid suffix
588
			if (!is_int($key)) continue;
589
590
			if ($suffix) $table = "{$classTable}_$suffix";
591
			else $table = $classTable;
592
593
			$fields = DataObject::database_fields($owner->class);
594
			unset($fields['ID']);
595
			if($fields) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $fields 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...
596
				$options = Config::inst()->get($owner->class, 'create_table_options', Config::FIRST_SET);
597
				$indexes = $owner->databaseIndexes();
598
				if ($suffix && ($ext = $owner->getExtensionInstance($allSuffixes[$suffix]))) {
599
					if (!$ext->isVersionedTable($table)) continue;
600
					$ext->setOwner($owner);
601
					$fields = $ext->fieldsInExtraTables($suffix);
602
					$ext->clearOwner();
603
					$indexes = $fields['indexes'];
604
					$fields = $fields['db'];
605
				}
606
607
				// Create tables for other stages
608
				if($this->hasStages()) {
609
					// Extra tables for _Live, etc.
610
					// Change unique indexes to 'index'.  Versioned tables may run into unique indexing difficulties
611
					// otherwise.
612
					$liveTable = $this->stageTable($table, static::LIVE);
613
					$indexes = $this->uniqueToIndex($indexes);
614
					DB::require_table($liveTable, $fields, $indexes, false, $options);
615
				}
616
617
				if($isRootClass) {
618
					// Create table for all versions
619
					$versionFields = array_merge(
620
						Config::inst()->get('Versioned', 'db_for_versions_table'),
621
						(array)$fields
622
					);
623
624
					$versionIndexes = array_merge(
625
						Config::inst()->get('Versioned', 'indexes_for_versions_table'),
626
						(array)$indexes
627
					);
628
				} else {
629
					// Create fields for any tables of subclasses
630
					$versionFields = array_merge(
631
						array(
632
							"RecordID" => "Int",
633
							"Version" => "Int",
634
						),
635
						(array)$fields
636
					);
637
638
					//Unique indexes will not work on versioned tables, so we'll convert them to standard indexes:
639
					$indexes = $this->uniqueToIndex($indexes);
640
					$versionIndexes = array_merge(
641
						array(
642
							'RecordID_Version' => array('type' => 'unique', 'value' => '"RecordID","Version"'),
643
							'RecordID' => true,
644
							'Version' => true,
645
						),
646
						(array)$indexes
647
					);
648
				}
649
650
				if(DB::get_schema()->hasTable("{$table}_versions")) {
651
					// Fix data that lacks the uniqueness constraint (since this was added later and
652
					// bugs meant that the constraint was validated)
653
					$duplications = DB::query("SELECT MIN(\"ID\") AS \"ID\", \"RecordID\", \"Version\"
654
						FROM \"{$table}_versions\" GROUP BY \"RecordID\", \"Version\"
655
						HAVING COUNT(*) > 1");
656
657
					foreach($duplications as $dup) {
658
						DB::alteration_message("Removing {$table}_versions duplicate data for "
659
							."{$dup['RecordID']}/{$dup['Version']}" ,"deleted");
660
						DB::prepared_query(
661
							"DELETE FROM \"{$table}_versions\" WHERE \"RecordID\" = ?
662
							AND \"Version\" = ? AND \"ID\" != ?",
663
							array($dup['RecordID'], $dup['Version'], $dup['ID'])
664
						);
665
					}
666
667
					// Remove junk which has no data in parent classes. Only needs to run the following
668
					// when versioned data is spread over multiple tables
669
					if(!$isRootClass && ($versionedTables = ClassInfo::dataClassesFor($table))) {
670
671
						foreach($versionedTables as $child) {
672
							if($table === $child) break; // only need subclasses
673
674
							// Select all orphaned version records
675
							$orphanedQuery = SQLSelect::create()
676
								->selectField("\"{$table}_versions\".\"ID\"")
677
								->setFrom("\"{$table}_versions\"");
678
679
							// If we have a parent table limit orphaned records
680
							// to only those that exist in this
681
							if(DB::get_schema()->hasTable("{$child}_versions")) {
682
								$orphanedQuery
683
									->addLeftJoin(
684
										"{$child}_versions",
685
										"\"{$child}_versions\".\"RecordID\" = \"{$table}_versions\".\"RecordID\"
686
										AND \"{$child}_versions\".\"Version\" = \"{$table}_versions\".\"Version\""
687
									)
688
									->addWhere("\"{$child}_versions\".\"ID\" IS NULL");
689
							}
690
691
							$count = $orphanedQuery->count();
692
							if($count > 0) {
693
								DB::alteration_message("Removing {$count} orphaned versioned records", "deleted");
694
								$ids = $orphanedQuery->execute()->column();
695
								foreach($ids as $id) {
696
									DB::prepared_query(
697
										"DELETE FROM \"{$table}_versions\" WHERE \"ID\" = ?",
698
										array($id)
699
									);
700
								}
701
							}
702
						}
703
					}
704
				}
705
706
				DB::require_table("{$table}_versions", $versionFields, $versionIndexes, true, $options);
707
			} else {
708
				DB::dont_require_table("{$table}_versions");
709
				if($this->hasStages()) {
710
					$liveTable = $this->stageTable($table, static::LIVE);
711
					DB::dont_require_table($liveTable);
712
				}
713
			}
714
		}
715
	}
716
717
	/**
718
	 * Helper for augmentDatabase() to find unique indexes and convert them to non-unique
719
	 *
720
	 * @param array $indexes The indexes to convert
721
	 * @return array $indexes
722
	 */
723
	private function uniqueToIndex($indexes) {
724
		$unique_regex = '/unique/i';
725
		$results = array();
726
		foreach ($indexes as $key => $index) {
727
			$results[$key] = $index;
728
729
			// support string descriptors
730
			if (is_string($index)) {
731
				if (preg_match($unique_regex, $index)) {
732
					$results[$key] = preg_replace($unique_regex, 'index', $index);
733
				}
734
			}
735
736
			// canonical, array-based descriptors
737
			elseif (is_array($index)) {
738
				if (strtolower($index['type']) == 'unique') {
739
					$results[$key]['type'] = 'index';
740
				}
741
			}
742
		}
743
		return $results;
744
	}
745
746
	/**
747
	 * Generates a ($table)_version DB manipulation and injects it into the current $manipulation
748
	 *
749
	 * @param array $manipulation Source manipulation data
750
	 * @param string $table Name of table
751
	 * @param int $recordID ID of record to version
752
	 */
753
	protected function augmentWriteVersioned(&$manipulation, $table, $recordID) {
754
		$baseDataClass = ClassInfo::baseDataClass($table);
755
756
		// Set up a new entry in (table)_versions
757
		$newManipulation = array(
758
			"command" => "insert",
759
			"fields" => isset($manipulation[$table]['fields']) ? $manipulation[$table]['fields'] : null
760
		);
761
762
		// Add any extra, unchanged fields to the version record.
763
		$data = DB::prepared_query("SELECT * FROM \"$table\" WHERE \"ID\" = ?", array($recordID))->record();
764
765
		if ($data) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $data 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...
766
			$fields = DataObject::database_fields($table);
767
768
			if (is_array($fields)) {
769
				$data = array_intersect_key($data, $fields);
770
771
				foreach ($data as $k => $v) {
772
					if (!isset($newManipulation['fields'][$k])) {
773
						$newManipulation['fields'][$k] = $v;
774
					}
775
				}
776
			}
777
		}
778
779
		// Ensure that the ID is instead written to the RecordID field
780
		$newManipulation['fields']['RecordID'] = $recordID;
781
		unset($newManipulation['fields']['ID']);
782
783
		// Generate next version ID to use
784
		$nextVersion = 0;
785
		if($recordID) {
786
			$nextVersion = DB::prepared_query("SELECT MAX(\"Version\") + 1
787
				FROM \"{$baseDataClass}_versions\" WHERE \"RecordID\" = ?",
788
				array($recordID)
789
			)->value();
790
		}
791
		$nextVersion = $nextVersion ?: 1;
792
793
		if($table === $baseDataClass) {
794
			// Write AuthorID for baseclass
795
			$userID = (Member::currentUser()) ? Member::currentUser()->ID : 0;
796
			$newManipulation['fields']['AuthorID'] = $userID;
797
798
			// Update main table version if not previously known
799
			$manipulation[$table]['fields']['Version'] = $nextVersion;
800
		}
801
802
		// Update _versions table manipulation
803
		$newManipulation['fields']['Version'] = $nextVersion;
804
		$manipulation["{$table}_versions"] = $newManipulation;
805
	}
806
807
	/**
808
	 * Rewrite the given manipulation to update the selected (non-default) stage
809
	 *
810
	 * @param array $manipulation Source manipulation data
811
	 * @param string $table Name of table
812
	 * @param int $recordID ID of record to version
813
	 */
814
	protected function augmentWriteStaged(&$manipulation, $table, $recordID) {
815
		// If the record has already been inserted in the (table), get rid of it.
816
		if($manipulation[$table]['command'] == 'insert') {
817
			DB::prepared_query(
818
				"DELETE FROM \"{$table}\" WHERE \"ID\" = ?",
819
				array($recordID)
820
			);
821
		}
822
823
		$newTable = $this->stageTable($table, Versioned::get_stage());
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
824
		$manipulation[$newTable] = $manipulation[$table];
825
		unset($manipulation[$table]);
826
	}
827
828
829
	public function augmentWrite(&$manipulation) {
830
		// get Version number from base data table on write
831
		$version = null;
832
		$owner = $this->owner;
833
		$baseDataClass = ClassInfo::baseDataClass($owner->class);
834
		if(isset($manipulation[$baseDataClass]['fields'])) {
835
			if ($this->migratingVersion) {
836
				$manipulation[$baseDataClass]['fields']['Version'] = $this->migratingVersion;
837
			}
838
			if (isset($manipulation[$baseDataClass]['fields']['Version'])) {
839
				$version = $manipulation[$baseDataClass]['fields']['Version'];
840
			}
841
		}
842
843
		// Update all tables
844
		$tables = array_keys($manipulation);
845
		foreach($tables as $table) {
846
847
			// Make sure that the augmented write is being applied to a table that can be versioned
848
			if( !$this->canBeVersioned($table) ) {
849
				unset($manipulation[$table]);
850
				continue;
851
			}
852
853
			// Get ID field
854
			$id = $manipulation[$table]['id']
855
				? $manipulation[$table]['id']
856
				: $manipulation[$table]['fields']['ID'];
857
			if(!$id) {
858
				user_error("Couldn't find ID in " . var_export($manipulation[$table], true), E_USER_ERROR);
859
			}
860
861
			if($version < 0 || $this->_nextWriteWithoutVersion) {
862
				// Putting a Version of -1 is a signal to leave the version table alone, despite their being no version
863
				unset($manipulation[$table]['fields']['Version']);
864
			} elseif(empty($version)) {
865
				// If we haven't got a version #, then we're creating a new version.
866
				// Otherwise, we're just copying a version to another table
867
				$this->augmentWriteVersioned($manipulation, $table, $id);
868
			}
869
870
			// Remove "Version" column from subclasses of baseDataClass
871
			if(!$this->hasVersionField($table)) {
872
				unset($manipulation[$table]['fields']['Version']);
873
			}
874
875
			// Grab a version number - it should be the same across all tables.
876
			if(isset($manipulation[$table]['fields']['Version'])) {
877
				$thisVersion = $manipulation[$table]['fields']['Version'];
878
			}
879
880
			// If we're editing Live, then use (table)_Live instead of (table)
881
			if($this->hasStages() && static::get_stage() === static::LIVE) {
882
				$this->augmentWriteStaged($manipulation, $table, $id);
883
			}
884
		}
885
886
		// Clear the migration flag
887
		if($this->migratingVersion) {
888
			$this->migrateVersion(null);
889
		}
890
891
		// Add the new version # back into the data object, for accessing
892
		// after this write
893
		if(isset($thisVersion)) {
894
			$owner->Version = str_replace("'","", $thisVersion);
895
		}
896
	}
897
898
	/**
899
	 * Perform a write without affecting the version table.
900
	 * On objects without versioning.
901
	 *
902
	 * @return int The ID of the record
903
	 */
904
	public function writeWithoutVersion() {
905
		$this->_nextWriteWithoutVersion = true;
906
907
		return $this->owner->write();
908
	}
909
910
	/**
911
	 *
912
	 */
913
	public function onAfterWrite() {
914
		$this->_nextWriteWithoutVersion = false;
915
	}
916
917
	/**
918
	 * If a write was skipped, then we need to ensure that we don't leave a
919
	 * migrateVersion() value lying around for the next write.
920
	 */
921
	public function onAfterSkippedWrite() {
922
		$this->migrateVersion(null);
923
	}
924
925
	/**
926
	 * Find all objects owned by the current object.
927
	 * Note that objects will only be searched in the same stage as the given record.
928
	 *
929
	 * @param bool $recursive True if recursive
930
	 * @param ArrayList $list Optional list to add items to
931
	 * @return ArrayList list of objects
932
	 */
933
	public function findOwned($recursive = true, $list = null)
934
	{
935
		// Find objects in these relationships
936
		return $this->findRelatedObjects('owns', $recursive, $list);
937
	}
938
939
	/**
940
	 * Find objects which own this object.
941
	 * Note that objects will only be searched in the same stage as the given record.
942
	 *
943
	 * @param bool $recursive True if recursive
944
	 * @param ArrayList $list Optional list to add items to
945
	 * @return ArrayList list of objects
946
	 */
947
	public function findOwners($recursive = true, $list = null) {
948
		if (!$list) {
949
			$list = new ArrayList();
950
		}
951
952
		// Build reverse lookup for ownership
953
		// @todo - Cache this more intelligently
954
		$rules = $this->lookupReverseOwners();
955
956
		// Hand off to recursive method
957
		return $this->findOwnersRecursive($recursive, $list, $rules);
958
	}
959
960
	/**
961
	 * Find objects which own this object.
962
	 * Note that objects will only be searched in the same stage as the given record.
963
	 *
964
	 * @param bool $recursive True if recursive
965
	 * @param ArrayList $list List to add items to
966
	 * @param array $lookup List of reverse lookup rules for owned objects
967
	 * @return ArrayList list of objects
968
	 */
969
	public function findOwnersRecursive($recursive, $list, $lookup) {
970
		// First pass: find objects that are explicitly owned_by (e.g. custom relationships)
971
		$owners = $this->findRelatedObjects('owned_by', false);
972
973
		// Second pass: Find owners via reverse lookup list
974
		foreach($lookup as $ownedClass => $classLookups) {
975
			// Skip owners of other objects
976
			if(!is_a($this->owner, $ownedClass)) {
977
				continue;
978
			}
979
			foreach($classLookups as $classLookup) {
980
				// Merge new owners into this object's owners
981
				$ownerClass = $classLookup['class'];
982
				$ownerRelation = $classLookup['relation'];
983
				$result = $this->owner->inferReciprocalComponent($ownerClass, $ownerRelation);
984
				$this->mergeRelatedObjects($owners, $result);
985
			}
986
		}
987
988
		// Merge all objects into the main list
989
		$newItems = $this->mergeRelatedObjects($list, $owners);
990
991
		// If recursing, iterate over all newly added items
992
		if($recursive) {
993
			foreach($newItems as $item) {
994
				/** @var Versioned|DataObject $item */
995
				$item->findOwnersRecursive(true, $list, $lookup);
0 ignored issues
show
Bug introduced by
The method findOwnersRecursive 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...
996
			}
997
		}
998
999
		return $list;
1000
	}
1001
1002
	/**
1003
	 * Find a list of classes, each of which with a list of methods to invoke
1004
	 * to lookup owners.
1005
	 *
1006
	 * @return array
1007
	 */
1008
	protected function lookupReverseOwners() {
1009
		// Find all classes with 'owns' config
1010
		$lookup = array();
1011
		foreach(ClassInfo::subclassesFor(DataObject::class) as $class) {
0 ignored issues
show
Bug introduced by
The expression \ClassInfo::subclassesFor(\DataObject::class) of type null|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
1012
			// Ensure this class is versioned
1013
			if(!Object::has_extension($class, Versioned::class)) {
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
1014
				continue;
1015
			}
1016
1017
			// Check owned objects for this class
1018
			$owns = Config::inst()->get($class, 'owns', Config::UNINHERITED);
1019
			if(empty($owns)) {
1020
				continue;
1021
			}
1022
1023
			/** @var DataObject $instance */
1024
			$instance = $class::singleton();
1025
			foreach($owns as $owned) {
0 ignored issues
show
Bug introduced by
The expression $owns of type array|integer|double|string|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
1026
				// Find owned class
1027
				$ownedClass = $instance->getRelationClass($owned);
1028
				// Skip custom methods that don't have db relationsm
1029
				if(!$ownedClass) {
1030
					continue;
1031
				}
1032
				if($ownedClass === 'DataObject') {
1033
					throw new LogicException(sprintf(
1034
						"Relation %s on class %s cannot be owned as it is polymorphic",
1035
						$owned, $class
1036
					));
1037
				}
1038
1039
				// Add lookup for owned class
1040
				if(!isset($lookup[$ownedClass])) {
1041
					$lookup[$ownedClass] = array();
1042
				}
1043
				$lookup[$ownedClass][] = [
1044
					'class' => $class,
1045
					'relation' => $owned
1046
				];
1047
			}
1048
		}
1049
		return $lookup;
1050
	}
1051
1052
1053
	/**
1054
	 * Find objects in the given relationships, merging them into the given list
1055
	 *
1056
	 * @param array $source Config property to extract relationships from
1057
	 * @param bool $recursive True if recursive
1058
	 * @param ArrayList $list Optional list to add items to
1059
	 * @return ArrayList The list
1060
	 */
1061
	public function findRelatedObjects($source, $recursive = true, $list = null)
1062
	{
1063
		if (!$list) {
1064
			$list = new ArrayList();
1065
		}
1066
1067
		// Skip search for unsaved records
1068
		$owner = $this->owner;
1069
		if(!$owner->isInDB()) {
1070
			return $list;
1071
		}
1072
1073
		$relationships = $owner->config()->{$source};
1074
		foreach($relationships as $relationship) {
1075
			// Warn if invalid config
1076
			if(!$owner->hasMethod($relationship)) {
1077
				trigger_error(sprintf(
1078
					"Invalid %s config value \"%s\" on object on class \"%s\"",
1079
					$source,
1080
					$relationship,
1081
					$owner->class
1082
				), E_USER_WARNING);
1083
				continue;
1084
			}
1085
1086
			// Inspect value of this relationship
1087
			$items = $owner->{$relationship}();
1088
1089
			// Merge any new item
1090
			$newItems = $this->mergeRelatedObjects($list, $items);
1091
1092
			// Recurse if necessary
1093
			if($recursive) {
1094
				foreach($newItems as $item) {
1095
					/** @var Versioned|DataObject $item */
1096
					$item->findRelatedObjects($source, true, $list);
0 ignored issues
show
Bug introduced by
The method findRelatedObjects 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...
1097
				}
1098
			}
1099
		}
1100
		return $list;
1101
	}
1102
1103
	/**
1104
	 * Helper method to merge owned/owning items into a list.
1105
	 * Items already present in the list will be skipped.
1106
	 *
1107
	 * @param ArrayList $list Items to merge into
1108
	 * @param mixed $items List of new items to merge
1109
	 * @return ArrayList List of all newly added items that did not already exist in $list
1110
	 */
1111
	protected function mergeRelatedObjects($list, $items) {
1112
		$added = new ArrayList();
1113
		if(!$items) {
1114
			return $added;
1115
		}
1116
		if($items instanceof DataObject) {
1117
			$items = array($items);
1118
		}
1119
1120
		/** @var Versioned|DataObject $item */
1121
		foreach($items as $item) {
1122
			// Identify item
1123
			$itemKey = $item->class . '/' . $item->ID;
1124
1125
			// Skip unsaved, unversioned, or already checked objects
1126
			if(!$item->isInDB() || !$item->has_extension('Versioned') || isset($list[$itemKey])) {
1127
				continue;
1128
			}
1129
1130
			// Save record
1131
			$list[$itemKey] = $item;
1132
			$added[$itemKey] = $item;
1133
		}
1134
		return $added;
1135
	}
1136
1137
	/**
1138
	 * This function should return true if the current user can publish this record.
1139
	 * It can be overloaded to customise the security model for an application.
1140
	 *
1141
	 * Denies permission if any of the following conditions is true:
1142
	 * - canPublish() on any extension returns false
1143
	 * - canEdit() returns false
1144
	 *
1145
	 * @param Member $member
1146
	 * @return bool True if the current user can publish this record.
1147
	 */
1148
	public function canPublish($member = null) {
1149
		// Skip if invoked by extendedCan()
1150
		if(func_num_args() > 4) {
1151
			return null;
1152
		}
1153
1154
		if(!$member) {
1155
			$member = Member::currentUser();
1156
		}
1157
1158
		if(Permission::checkMember($member, "ADMIN")) {
1159
			return true;
1160
		}
1161
1162
		// Standard mechanism for accepting permission changes from extensions
1163
		$owner = $this->owner;
1164
		$extended = $owner->extendedCan('canPublish', $member);
1165
		if($extended !== null) {
1166
			return $extended;
1167
		}
1168
1169
		// Default to relying on edit permission
1170
		return $owner->canEdit($member);
1171
	}
1172
1173
	/**
1174
	 * Check if the current user can delete this record from live
1175
	 *
1176
	 * @param null $member
1177
	 * @return mixed
1178
	 */
1179
	public function canUnpublish($member = null) {
1180
		// Skip if invoked by extendedCan()
1181
		if(func_num_args() > 4) {
1182
			return null;
1183
		}
1184
1185
		if(!$member) {
1186
			$member = Member::currentUser();
1187
		}
1188
1189
		if(Permission::checkMember($member, "ADMIN")) {
1190
			return true;
1191
		}
1192
1193
		// Standard mechanism for accepting permission changes from extensions
1194
		$owner = $this->owner;
1195
		$extended = $owner->extendedCan('canUnpublish', $member);
1196
		if($extended !== null) {
1197
			return $extended;
1198
		}
1199
1200
		// Default to relying on canPublish
1201
		return $owner->canPublish($member);
0 ignored issues
show
Bug introduced by
It seems like $member defined by \Member::currentUser() on line 1186 can also be of type object<DataObject>; however, Versioned::canPublish() does only seem to accept object<Member>|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1202
	}
1203
1204
	/**
1205
	 * Check if the current user is allowed to archive this record.
1206
	 * If extended, ensure that both canDelete and canUnpublish are extended also
1207
	 *
1208
	 * @param Member $member
1209
	 * @return bool
1210
	 */
1211
	public function canArchive($member = null) {
1212
		// Skip if invoked by extendedCan()
1213
		if(func_num_args() > 4) {
1214
			return null;
1215
		}
1216
1217
		if(!$member) {
1218
            $member = Member::currentUser();
1219
        }
1220
1221
		if(Permission::checkMember($member, "ADMIN")) {
1222
			return true;
1223
		}
1224
1225
		// Standard mechanism for accepting permission changes from extensions
1226
		$owner = $this->owner;
1227
		$extended = $owner->extendedCan('canArchive', $member);
1228
		if($extended !== null) {
1229
            return $extended;
1230
        }
1231
1232
		// Check if this record can be deleted from stage
1233
        if(!$owner->canDelete($member)) {
1234
            return false;
1235
        }
1236
1237
        // Check if we can delete from live
1238
        if(!$owner->canUnpublish($member)) {
0 ignored issues
show
Bug introduced by
It seems like $member can also be of type object<DataObject>; however, Versioned::canUnpublish() does only seem to accept null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1239
            return false;
1240
        }
1241
1242
		return true;
1243
	}
1244
1245
	/**
1246
	 * Check if the user can revert this record to live
1247
	 *
1248
	 * @param Member $member
1249
	 * @return bool
1250
	 */
1251
	public function canRevertToLive($member = null) {
1252
		$owner = $this->owner;
1253
1254
		// Skip if invoked by extendedCan()
1255
		if(func_num_args() > 4) {
1256
			return null;
1257
		}
1258
1259
		// Can't revert if not on live
1260
		if(!$owner->isPublished()) {
1261
			return false;
1262
		}
1263
1264
		if(!$member) {
1265
            $member = Member::currentUser();
1266
        }
1267
1268
		if(Permission::checkMember($member, "ADMIN")) {
1269
			return true;
1270
		}
1271
1272
		// Standard mechanism for accepting permission changes from extensions
1273
		$extended = $owner->extendedCan('canRevertToLive', $member);
1274
		if($extended !== null) {
1275
            return $extended;
1276
        }
1277
1278
		// Default to canEdit
1279
		return $owner->canEdit($member);
1280
	}
1281
1282
	/**
1283
	 * Extend permissions to include additional security for objects that are not published to live.
1284
	 *
1285
	 * @param Member $member
1286
	 * @return bool|null
1287
	 */
1288
	public function canView($member = null) {
1289
		// Invoke default version-gnostic canView
1290
		if ($this->owner->canViewVersioned($member) === false) {
1291
			return false;
1292
		}
1293
	}
1294
1295
	/**
1296
	 * Determine if there are any additional restrictions on this object for the given reading version.
1297
	 *
1298
	 * Override this in a subclass to customise any additional effect that Versioned applies to canView.
1299
	 *
1300
	 * This is expected to be called by canView, and thus is only responsible for denying access if
1301
	 * the default canView would otherwise ALLOW access. Thus it should not be called in isolation
1302
	 * as an authoritative permission check.
1303
	 *
1304
	 * This has the following extension points:
1305
	 *  - canViewDraft is invoked if Mode = stage and Stage = stage
1306
	 *  - canViewArchived is invoked if Mode = archive
1307
	 *
1308
	 * @param Member $member
1309
	 * @return bool False is returned if the current viewing mode denies visibility
1310
	 */
1311
	public function canViewVersioned($member = null) {
1312
		// Bypass when live stage
1313
		$owner = $this->owner;
1314
		$mode = $owner->getSourceQueryParam("Versioned.mode");
1315
		$stage = $owner->getSourceQueryParam("Versioned.stage");
1316
		if ($mode === 'stage' && $stage === static::LIVE) {
1317
			return true;
1318
		}
1319
1320
		// Bypass if site is unsecured
1321
		if (Session::get('unsecuredDraftSite')) {
1322
			return true;
1323
		}
1324
1325
		// Bypass if record doesn't have a live stage
1326
		if(!$this->hasStages()) {
1327
			return true;
1328
		}
1329
1330
		// If we weren't definitely loaded from live, and we can't view non-live content, we need to
1331
		// check to make sure this version is the live version and so can be viewed
1332
		$latestVersion = Versioned::get_versionnumber_by_stage($owner->class, static::LIVE, $owner->ID);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
1333
		if ($latestVersion == $owner->Version) {
1334
			// Even if this is loaded from a non-live stage, this is the live version
1335
			return true;
1336
		}
1337
1338
		// Extend versioned behaviour
1339
		$extended = $owner->extendedCan('canViewNonLive', $member);
1340
		if($extended !== null) {
1341
			return (bool)$extended;
1342
		}
1343
1344
		// Fall back to default permission check
1345
		$permissions = Config::inst()->get($owner->class, 'non_live_permissions', Config::FIRST_SET);
1346
		$check = Permission::checkMember($member, $permissions);
1347
		return (bool)$check;
1348
	}
1349
1350
	/**
1351
	 * Determines canView permissions for the latest version of this object on a specific stage.
1352
	 * Usually the stage is read from {@link Versioned::current_stage()}.
1353
	 *
1354
	 * This method should be invoked by user code to check if a record is visible in the given stage.
1355
	 *
1356
	 * This method should not be called via ->extend('canViewStage'), but rather should be
1357
	 * overridden in the extended class.
1358
	 *
1359
	 * @param string $stage
1360
	 * @param Member $member
1361
	 * @return bool
1362
	 */
1363
	public function canViewStage($stage = 'Live', $member = null) {
1364
		$oldMode = Versioned::get_reading_mode();
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
1365
		Versioned::set_stage($stage);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
1366
1367
		$owner = $this->owner;
1368
		$versionFromStage = DataObject::get($owner->class)->byID($owner->ID);
1369
1370
		Versioned::set_reading_mode($oldMode);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
1371
		return $versionFromStage ? $versionFromStage->canView($member) : false;
1372
	}
1373
1374
	/**
1375
	 * Determine if a table is supporting the Versioned extensions (e.g.
1376
	 * $table_versions does exists).
1377
	 *
1378
	 * @param string $table Table name
1379
	 * @return boolean
1380
	 */
1381
	public function canBeVersioned($table) {
1382
		return ClassInfo::exists($table)
1383
			&& is_subclass_of($table, 'DataObject')
1384
			&& DataObject::has_own_table($table);
1385
	}
1386
1387
	/**
1388
	 * Check if a certain table has the 'Version' field.
1389
	 *
1390
	 * @param string $table Table name
1391
	 *
1392
	 * @return boolean Returns false if the field isn't in the table, true otherwise
1393
	 */
1394
	public function hasVersionField($table) {
1395
		// Strip "_Live" from end of table
1396
		$live = static::LIVE;
1397
		if($this->hasStages() && preg_match("/^(?<table>.*)_{$live}$/", $table, $matches)) {
1398
			$table = $matches['table'];
1399
		}
1400
1401
		// Base table has version field
1402
		return $table === ClassInfo::baseDataClass($table);
1403
	}
1404
1405
	/**
1406
	 * @param string $table
1407
	 *
1408
	 * @return string
1409
	 */
1410
	public function extendWithSuffix($table) {
1411
		$owner = $this->owner;
1412
		$versionableExtensions = $owner->config()->versionableExtensions;
1413
1414
		if(count($versionableExtensions)){
1415
			foreach ($versionableExtensions as $versionableExtension => $suffixes) {
1416
				if ($owner->hasExtension($versionableExtension)) {
1417
					$ext = $owner->getExtensionInstance($versionableExtension);
1418
					$ext->setOwner($owner);
1419
				$table = $ext->extendWithSuffix($table);
1420
				$ext->clearOwner();
1421
			}
1422
		}
1423
		}
1424
1425
		return $table;
1426
	}
1427
1428
	/**
1429
	 * Get the latest published DataObject.
1430
	 *
1431
	 * @return DataObject
1432
	 */
1433
	public function latestPublished() {
1434
		// Get the root data object class - this will have the version field
1435
		$owner = $this->owner;
1436
		$table1 = ClassInfo::baseDataClass($owner);
1437
		$table2 = $this->stageTable($table1, static::LIVE);
1438
1439
		return DB::prepared_query("SELECT \"$table1\".\"Version\" = \"$table2\".\"Version\" FROM \"$table1\"
1440
			 INNER JOIN \"$table2\" ON \"$table1\".\"ID\" = \"$table2\".\"ID\"
1441
			 WHERE \"$table1\".\"ID\" = ?",
1442
			array($owner->ID)
1443
		)->value();
1444
	}
1445
1446
	/**
1447
	 * @deprecated 4.0..5.0
1448
	 */
1449
	public function doPublish() {
1450
		Deprecation::notice('5.0', 'Use publishRecursive instead');
1451
		return $this->owner->publishRecursive();
1452
	}
1453
1454
	/**
1455
	 * Publish this object and all owned objects to Live
1456
	 *
1457
	 * @return bool
1458
	 */
1459
	public function publishRecursive() {
1460
		$owner = $this->owner;
1461
		if(!$owner->publishSingle()) {
1462
			return false;
1463
		}
1464
1465
		// Publish owned objects
1466
		foreach ($owner->findOwned(false) as $object) {
1467
			/** @var Versioned|DataObject $object */
1468
			$object->publishRecursive();
0 ignored issues
show
Bug introduced by
The method publishRecursive 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...
1469
		}
1470
1471
		// Unlink any objects disowned as a result of this action
1472
		// I.e. objects which aren't owned anymore by this record, but are by the old live record
1473
		$owner->unlinkDisownedObjects(Versioned::DRAFT, Versioned::LIVE);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
1474
1475
		return true;
1476
	}
1477
1478
	/**
1479
	 * Publishes this object to Live, but doesn't publish owned objects.
1480
	 *
1481
	 * @return bool True if publish was successful
1482
	 */
1483
	public function publishSingle() {
1484
		$owner = $this->owner;
1485
		if(!$owner->canPublish()) {
1486
			return false;
1487
		}
1488
1489
		$owner->invokeWithExtensions('onBeforePublish');
1490
		$owner->write();
1491
		$owner->copyVersionToStage(static::DRAFT, static::LIVE);
1492
		$owner->invokeWithExtensions('onAfterPublish');
1493
		return true;
1494
	}
1495
1496
	/**
1497
	 * Set foreign keys of has_many objects to 0 where those objects were
1498
	 * disowned as a result of a partial publish / unpublish.
1499
	 * I.e. this object and its owned objects were recently written to $targetStage,
1500
	 * but deleted objects were not.
1501
	 *
1502
	 * Note that this operation does not create any new Versions
1503
	 *
1504
	 * @param string $sourceStage Objects in this stage will not be unlinked.
1505
	 * @param string $targetStage Objects which exist in this stage but not $sourceStage
1506
	 * will be unlinked.
1507
	 */
1508
	public function unlinkDisownedObjects($sourceStage, $targetStage) {
1509
		$owner = $this->owner;
1510
1511
		// after publishing, objects which used to be owned need to be
1512
		// dis-connected from this object (set ForeignKeyID = 0)
1513
		$owns = $owner->config()->owns;
1514
		$hasMany = $owner->config()->has_many;
1515
		if(empty($owns) || empty($hasMany)) {
1516
			return;
1517
		}
1518
1519
		$ownedHasMany = array_intersect($owns, array_keys($hasMany));
1520
		foreach($ownedHasMany as $relationship) {
1521
			// Find metadata on relationship
1522
			$joinClass = $owner->hasManyComponent($relationship);
1523
			$joinField = $owner->getRemoteJoinField($relationship, 'has_many', $polymorphic);
0 ignored issues
show
Bug introduced by
The variable $polymorphic does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
1524
			$idField = $polymorphic ? "{$joinField}ID" : $joinField;
1525
			$joinTable = ClassInfo::table_for_object_field($joinClass, $idField);
1526
1527
			// Generate update query which will unlink disowned objects
1528
			$targetTable = $this->stageTable($joinTable, $targetStage);
1529
			$disowned = new SQLUpdate("\"{$targetTable}\"");
1530
			$disowned->assign("\"{$idField}\"", 0);
1531
			$disowned->addWhere(array(
1532
				"\"{$targetTable}\".\"{$idField}\"" => $owner->ID
1533
			));
1534
1535
			// Build exclusion list (items to owned objects we need to keep)
1536
			$sourceTable = $this->stageTable($joinTable, $sourceStage);
1537
			$owned = new SQLSelect("\"{$sourceTable}\".\"ID\"", "\"{$sourceTable}\"");
1538
			$owned->addWhere(array(
1539
				"\"{$sourceTable}\".\"{$idField}\"" => $owner->ID
1540
			));
1541
1542
			// Apply class condition if querying on polymorphic has_one
1543
			if($polymorphic) {
1544
				$disowned->assign("\"{$joinField}Class\"", null);
1545
				$disowned->addWhere(array(
1546
					"\"{$targetTable}\".\"{$joinField}Class\"" => get_class($owner)
1547
				));
1548
				$owned->addWhere(array(
1549
					"\"{$sourceTable}\".\"{$joinField}Class\"" => get_class($owner)
1550
				));
1551
			}
1552
1553
			// Merge queries and perform unlink
1554
			$ownedSQL = $owned->sql($ownedParams);
1555
			$disowned->addWhere(array(
1556
				"\"{$targetTable}\".\"ID\" NOT IN ({$ownedSQL})" => $ownedParams
1557
			));
1558
1559
			$owner->extend('updateDisownershipQuery', $disowned, $sourceStage, $targetStage, $relationship);
0 ignored issues
show
Bug introduced by
The method extend() does not exist on Versioned. Did you maybe mean extendWithSuffix()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
1560
1561
			$disowned->execute();
1562
		}
1563
	}
1564
1565
	/**
1566
	 * Removes the record from both live and stage
1567
	 *
1568
	 * @return bool Success
1569
	 */
1570
	public function doArchive() {
1571
		$owner = $this->owner;
1572
		if(!$owner->canArchive()) {
1573
			return false;
1574
		}
1575
1576
		$owner->invokeWithExtensions('onBeforeArchive', $this);
1577
		$owner->doUnpublish();
1578
		$owner->delete();
0 ignored issues
show
Bug introduced by
The method delete() does not exist on Versioned. Did you maybe mean deleteFromStage()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
1579
		$owner->invokeWithExtensions('onAfterArchive', $this);
1580
1581
		return true;
1582
	}
1583
1584
	/**
1585
	 * Removes this record from the live site
1586
	 *
1587
	 * @return bool Flag whether the unpublish was successful
1588
	 */
1589
	public function doUnpublish() {
1590
		$owner = $this->owner;
1591
		if(!$owner->canUnpublish()) {
1592
			return false;
1593
		}
1594
1595
		// Skip if this record isn't saved
1596
		if(!$owner->isInDB()) {
1597
			return false;
1598
		}
1599
1600
		// Skip if this record isn't on live
1601
		if(!$owner->isPublished()) {
1602
			return false;
1603
		}
1604
1605
		$owner->invokeWithExtensions('onBeforeUnpublish');
1606
1607
		$origStage = static::get_stage();
1608
		static::set_stage(static::LIVE);
1609
1610
		// This way our ID won't be unset
1611
		$clone = clone $owner;
1612
		$clone->delete();
0 ignored issues
show
Bug introduced by
The method delete() does not exist on Versioned. Did you maybe mean deleteFromStage()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
1613
1614
		static::set_stage($origStage);
1615
1616
		$owner->invokeWithExtensions('onAfterUnpublish');
1617
		return true;
1618
	}
1619
1620
	/**
1621
	 * Trigger unpublish of owning objects
1622
	 */
1623
	public function onAfterUnpublish() {
1624
		$owner = $this->owner;
1625
1626
		// Any objects which owned (and thus relied on the unpublished object) are now unpublished automatically.
1627
		foreach ($owner->findOwners(false) as $object) {
1628
			/** @var Versioned|DataObject $object */
1629
			$object->doUnpublish();
0 ignored issues
show
Bug introduced by
The method doUnpublish 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...
1630
		}
1631
	}
1632
1633
1634
	/**
1635
	 * Revert the draft changes: replace the draft content with the content on live
1636
	 *
1637
	 * @return bool True if the revert was successful
1638
	 */
1639
	public function doRevertToLive() {
1640
		$owner = $this->owner;
1641
		if(!$owner->canRevertToLive()) {
1642
			return false;
1643
		}
1644
1645
		$owner->invokeWithExtensions('onBeforeRevertToLive');
1646
		$owner->copyVersionToStage(static::LIVE, static::DRAFT, false);
1647
		$owner->invokeWithExtensions('onAfterRevertToLive');
1648
		return true;
1649
	}
1650
1651
	/**
1652
	 * Trigger revert of all owned objects to stage
1653
	 */
1654
	public function onAfterRevertToLive() {
1655
		$owner = $this->owner;
1656
		/** @var Versioned|DataObject $liveOwner */
1657
		$liveOwner = static::get_by_stage(get_class($owner), static::LIVE)
1658
			->byID($owner->ID);
1659
1660
		// Revert any owned objects from the live stage only
1661
		foreach ($liveOwner->findOwned(false) as $object) {
0 ignored issues
show
Bug introduced by
The method findOwned 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...
1662
			/** @var Versioned|DataObject $object */
1663
			$object->doRevertToLive();
0 ignored issues
show
Bug introduced by
The method doRevertToLive 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...
1664
		}
1665
1666
		// Unlink any objects disowned as a result of this action
1667
		// I.e. objects which aren't owned anymore by this record, but are by the old draft record
1668
		$owner->unlinkDisownedObjects(Versioned::LIVE, Versioned::DRAFT);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
1669
	}
1670
1671
	/**
1672
	 * @deprecated 4.0..5.0
1673
	 */
1674
	public function publish($fromStage, $toStage, $createNewVersion = false) {
1675
		Deprecation::notice('5.0', 'Use copyVersionToStage instead');
1676
		$this->owner->copyVersionToStage($fromStage, $toStage, $createNewVersion);
1677
	}
1678
1679
	/**
1680
	 * Move a database record from one stage to the other.
1681
	 *
1682
	 * @param int|string $fromStage Place to copy from.  Can be either a stage name or a version number.
1683
	 * @param string $toStage Place to copy to.  Must be a stage name.
1684
	 * @param bool $createNewVersion Set this to true to create a new version number.
1685
	 * By default, the existing version number will be copied over.
1686
	 */
1687
	public function copyVersionToStage($fromStage, $toStage, $createNewVersion = false) {
1688
		$owner = $this->owner;
1689
		$owner->invokeWithExtensions('onBeforeVersionedPublish', $fromStage, $toStage, $createNewVersion);
1690
1691
		$baseClass = ClassInfo::baseDataClass($owner->class);
1692
1693
		/** @var Versioned|DataObject $from */
1694
		if(is_numeric($fromStage)) {
1695
			$from = Versioned::get_version($baseClass, $owner->ID, $fromStage);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
1696
		} else {
1697
			$owner->flushCache();
1698
			$from = Versioned::get_one_by_stage($baseClass, $fromStage, array(
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
1699
				"\"{$baseClass}\".\"ID\" = ?" => $owner->ID
1700
			));
1701
		}
1702
		if(!$from) {
1703
			throw new InvalidArgumentException("Can't find {$baseClass}#{$owner->ID} in stage {$fromStage}");
1704
		}
1705
1706
		$from->forceChange();
1707
		if($createNewVersion) {
1708
			// Clear version to be automatically created on write
1709
			$from->Version = null;
0 ignored issues
show
Documentation introduced by
The property Version 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...
1710
		} else {
1711
			$from->migrateVersion($from->Version);
1712
1713
			// Mark this version as having been published at some stage
1714
			$publisherID = isset(Member::currentUser()->ID) ? Member::currentUser()->ID : 0;
1715
			$extTable = $this->extendWithSuffix($baseClass);
1716
			DB::prepared_query("UPDATE \"{$extTable}_versions\"
1717
				SET \"WasPublished\" = ?, \"PublisherID\" = ?
1718
				WHERE \"RecordID\" = ? AND \"Version\" = ?",
1719
				array(1, $publisherID, $from->ID, $from->Version)
1720
			);
1721
		}
1722
1723
		// Change to new stage, write, and revert state
1724
		$oldMode = Versioned::get_reading_mode();
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
1725
		Versioned::set_stage($toStage);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
1726
1727
		// Migrate stage prior to write
1728
		$from->setSourceQueryParam('Versioned.mode', 'stage');
1729
		$from->setSourceQueryParam('Versioned.stage', $toStage);
1730
1731
		$conn = DB::get_conn();
1732
		if(method_exists($conn, 'allowPrimaryKeyEditing')) {
1733
			$conn->allowPrimaryKeyEditing($baseClass, true);
1734
			$from->write();
1735
			$conn->allowPrimaryKeyEditing($baseClass, false);
1736
		} else {
1737
			$from->write();
1738
		}
1739
1740
		$from->destroy();
1741
1742
		Versioned::set_reading_mode($oldMode);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
1743
1744
		$owner->invokeWithExtensions('onAfterVersionedPublish', $fromStage, $toStage, $createNewVersion);
1745
	}
1746
1747
	/**
1748
	 * Set the migrating version.
1749
	 *
1750
	 * @param string $version The version.
1751
	 */
1752
	public function migrateVersion($version) {
1753
		$this->migratingVersion = $version;
1754
	}
1755
1756
	/**
1757
	 * Compare two stages to see if they're different.
1758
	 *
1759
	 * Only checks the version numbers, not the actual content.
1760
	 *
1761
	 * @param string $stage1 The first stage to check.
1762
	 * @param string $stage2
1763
	 * @return bool
1764
	 */
1765
	public function stagesDiffer($stage1, $stage2) {
1766
		$table1 = $this->baseTable($stage1);
1767
		$table2 = $this->baseTable($stage2);
1768
1769
		$owner = $this->owner;
1770
		if(!is_numeric($owner->ID)) {
1771
			return true;
1772
		}
1773
1774
		// We test for equality - if one of the versions doesn't exist, this
1775
		// will be false.
1776
1777
		// TODO: DB Abstraction: if statement here:
1778
		$stagesAreEqual = DB::prepared_query(
1779
			"SELECT CASE WHEN \"$table1\".\"Version\"=\"$table2\".\"Version\" THEN 1 ELSE 0 END
1780
			 FROM \"$table1\" INNER JOIN \"$table2\" ON \"$table1\".\"ID\" = \"$table2\".\"ID\"
1781
			 AND \"$table1\".\"ID\" = ?",
1782
			array($owner->ID)
1783
		)->value();
1784
1785
		return !$stagesAreEqual;
1786
	}
1787
1788
	/**
1789
	 * @param string $filter
1790
	 * @param string $sort
1791
	 * @param string $limit
1792
	 * @param string $join Deprecated, use leftJoin($table, $joinClause) instead
1793
	 * @param string $having
1794
	 * @return ArrayList
1795
	 */
1796
	public function Versions($filter = "", $sort = "", $limit = "", $join = "", $having = "") {
1797
		return $this->allVersions($filter, $sort, $limit, $join, $having);
1798
	}
1799
1800
	/**
1801
	 * Return a list of all the versions available.
1802
	 *
1803
	 * @param  string $filter
1804
	 * @param  string $sort
1805
	 * @param  string $limit
1806
	 * @param  string $join   Deprecated, use leftJoin($table, $joinClause) instead
1807
	 * @param  string $having
1808
	 * @return ArrayList
1809
	 */
1810
	public function allVersions($filter = "", $sort = "", $limit = "", $join = "", $having = "") {
1811
		// Make sure the table names are not postfixed (e.g. _Live)
1812
		$oldMode = static::get_reading_mode();
1813
		static::set_stage(static::DRAFT);
1814
1815
		$owner = $this->owner;
1816
		$list = DataObject::get(get_class($owner), $filter, $sort, $join, $limit);
1817
		if($having) {
1818
			$list->having($having);
1819
		}
1820
1821
		$query = $list->dataQuery()->query();
1822
1823
		foreach($query->getFrom() as $table => $tableJoin) {
1824
			if(is_string($tableJoin) && $tableJoin[0] == '"') {
1825
				$baseTable = str_replace('"','',$tableJoin);
1826
			} elseif(is_string($tableJoin) && substr($tableJoin,0,5) != 'INNER') {
1827
				$query->setFrom(array(
1828
					$table => "LEFT JOIN \"$table\" ON \"$table\".\"RecordID\"=\"{$baseTable}_versions\".\"RecordID\""
0 ignored issues
show
Bug introduced by
The variable $baseTable does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
1829
						. " AND \"$table\".\"Version\" = \"{$baseTable}_versions\".\"Version\""
1830
				));
1831
			}
1832
			$query->renameTable($table, $table . '_versions');
1833
		}
1834
1835
		// Add all <basetable>_versions columns
1836
		foreach(Config::inst()->get('Versioned', 'db_for_versions_table') as $name => $type) {
0 ignored issues
show
Bug introduced by
The expression \Config::inst()->get('Ve...db_for_versions_table') of type array|integer|double|string|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
1837
			$query->selectField(sprintf('"%s_versions"."%s"', $baseTable, $name), $name);
1838
		}
1839
1840
		$query->addWhere(array(
1841
			"\"{$baseTable}_versions\".\"RecordID\" = ?" => $owner->ID
1842
		));
1843
		$query->setOrderBy(($sort) ? $sort
1844
			: "\"{$baseTable}_versions\".\"LastEdited\" DESC, \"{$baseTable}_versions\".\"Version\" DESC");
1845
1846
		$records = $query->execute();
1847
		$versions = new ArrayList();
1848
1849
		foreach($records as $record) {
1850
			$versions->push(new Versioned_Version($record));
1851
		}
1852
1853
		Versioned::set_reading_mode($oldMode);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
1854
		return $versions;
1855
	}
1856
1857
	/**
1858
	 * Compare two version, and return the diff between them.
1859
	 *
1860
	 * @param string $from The version to compare from.
1861
	 * @param string $to The version to compare to.
1862
	 *
1863
	 * @return DataObject
1864
	 */
1865
	public function compareVersions($from, $to) {
1866
		$owner = $this->owner;
1867
		$fromRecord = Versioned::get_version($owner->class, $owner->ID, $from);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
1868
		$toRecord = Versioned::get_version($owner->class, $owner->ID, $to);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
1869
1870
		$diff = new DataDifferencer($fromRecord, $toRecord);
0 ignored issues
show
Bug introduced by
It seems like $toRecord defined by \Versioned::get_version(...class, $owner->ID, $to) on line 1868 can be null; however, DataDifferencer::__construct() 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...
1871
1872
		return $diff->diffedData();
1873
	}
1874
1875
	/**
1876
	 * Return the base table - the class that directly extends DataObject.
1877
	 *
1878
	 * @param string $stage
1879
	 * @return string
1880
	 */
1881
	public function baseTable($stage = null) {
1882
		$baseClass = ClassInfo::baseDataClass($this->owner);
1883
		return $this->stageTable($baseClass, $stage);
1884
	}
1885
1886
	/**
1887
	 * Given a class and stage determine the table name.
1888
	 *
1889
	 * Note: Stages this asset does not exist in will default to the draft table.
1890
	 *
1891
	 * @param string $class
1892
	 * @param string $stage
1893
	 * @return string Table name
1894
	 */
1895
	public function stageTable($class, $stage) {
1896
		if($this->hasStages() && $stage === static::LIVE) {
1897
			return "{$class}_{$stage}";
1898
		}
1899
		return $class;
1900
	}
1901
1902
	//-----------------------------------------------------------------------------------------------//
1903
1904
1905
	/**
1906
	 * Determine if the current user is able to set the given site stage / archive
1907
	 *
1908
	 * @param SS_HTTPRequest $request
1909
	 * @return bool
1910
	 */
1911
	public static function can_choose_site_stage($request) {
1912
		// Request is allowed if stage isn't being modified
1913
		if((!$request->getVar('stage') || $request->getVar('stage') === static::LIVE)
1914
			&& !$request->getVar('archiveDate')
1915
		) {
1916
			return true;
1917
		}
1918
1919
		// Check permissions with member ID in session.
1920
		$member = Member::currentUser();
1921
		$permissions = Config::inst()->get(get_called_class(), 'non_live_permissions');
1922
		return $member && Permission::checkMember($member, $permissions);
1923
	}
1924
1925
	/**
1926
	 * Choose the stage the site is currently on.
1927
	 *
1928
	 * If $_GET['stage'] is set, then it will use that stage, and store it in
1929
	 * the session.
1930
	 *
1931
	 * if $_GET['archiveDate'] is set, it will use that date, and store it in
1932
	 * the session.
1933
	 *
1934
	 * If neither of these are set, it checks the session, otherwise the stage
1935
	 * is set to 'Live'.
1936
	 */
1937
	public static function choose_site_stage() {
0 ignored issues
show
Coding Style introduced by
choose_site_stage uses the super-global variable $_GET which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
1938
		// Check any pre-existing session mode
1939
		$preexistingMode = Session::get('readingMode');
1940
1941
		// Determine the reading mode
1942
		if(isset($_GET['stage'])) {
1943
			$stage = ucfirst(strtolower($_GET['stage']));
1944
			if(!in_array($stage, array(static::DRAFT, static::LIVE))) {
1945
				$stage = static::LIVE;
1946
			}
1947
			$mode = 'Stage.' . $stage;
1948
		} elseif (isset($_GET['archiveDate']) && strtotime($_GET['archiveDate'])) {
1949
			$mode = 'Archive.' . $_GET['archiveDate'];
1950
		} elseif($preexistingMode) {
1951
			$mode = $preexistingMode;
1952
		} else {
1953
			$mode = static::DEFAULT_MODE;
1954
		}
1955
1956
		// Save reading mode
1957
		Versioned::set_reading_mode($mode);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
1958
1959
		// Try not to store the mode in the session if not needed
1960
		if(($preexistingMode && $preexistingMode !== $mode)
1961
			|| (!$preexistingMode && $mode !== static::DEFAULT_MODE)
1962
		) {
1963
			Session::set('readingMode', $mode);
1964
		}
1965
1966
		if(!headers_sent() && !Director::is_cli()) {
1967
			if(Versioned::get_stage() == 'Live') {
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
1968
				// clear the cookie if it's set
1969
				if(Cookie::get('bypassStaticCache')) {
0 ignored issues
show
Bug Best Practice introduced by
The expression \Cookie::get('bypassStaticCache') of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1970
					Cookie::force_expiry('bypassStaticCache', null, null, false, true /* httponly */);
1971
				}
1972
			} else {
1973
				// set the cookie if it's cleared
1974
				if(!Cookie::get('bypassStaticCache')) {
0 ignored issues
show
Bug Best Practice introduced by
The expression \Cookie::get('bypassStaticCache') of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1975
					Cookie::set('bypassStaticCache', '1', 0, null, null, false, true /* httponly */);
1976
				}
1977
			}
1978
		}
1979
	}
1980
1981
	/**
1982
	 * Set the current reading mode.
1983
	 *
1984
	 * @param string $mode
1985
	 */
1986
	public static function set_reading_mode($mode) {
1987
		self::$reading_mode = $mode;
1988
	}
1989
1990
	/**
1991
	 * Get the current reading mode.
1992
	 *
1993
	 * @return string
1994
	 */
1995
	public static function get_reading_mode() {
1996
		return self::$reading_mode;
1997
	}
1998
1999
	/**
2000
	 * Get the current reading stage.
2001
	 *
2002
	 * @return string
2003
	 */
2004
	public static function get_stage() {
2005
		$parts = explode('.', Versioned::get_reading_mode());
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
2006
2007
		if($parts[0] == 'Stage') {
2008
			return $parts[1];
2009
		}
2010
	}
2011
2012
	/**
2013
	 * Get the current archive date.
2014
	 *
2015
	 * @return string
2016
	 */
2017
	public static function current_archived_date() {
2018
		$parts = explode('.', Versioned::get_reading_mode());
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
2019
		if($parts[0] == 'Archive') {
2020
			return $parts[1];
2021
		}
2022
	}
2023
2024
	/**
2025
	 * Set the reading stage.
2026
	 *
2027
	 * @param string $stage New reading stage.
2028
	 */
2029
	public static function set_stage($stage) {
2030
		static::set_reading_mode('Stage.' . $stage);
2031
	}
2032
2033
	/**
2034
	 * Set the reading archive date.
2035
	 *
2036
	 * @param string $date New reading archived date.
2037
	 */
2038
	public static function reading_archived_date($date) {
2039
		Versioned::set_reading_mode('Archive.' . $date);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
2040
	}
2041
2042
2043
	/**
2044
	 * Get a singleton instance of a class in the given stage.
2045
	 *
2046
	 * @param string $class The name of the class.
2047
	 * @param string $stage The name of the stage.
2048
	 * @param string $filter A filter to be inserted into the WHERE clause.
2049
	 * @param boolean $cache Use caching.
2050
	 * @param string $sort A sort expression to be inserted into the ORDER BY clause.
2051
	 *
2052
	 * @return DataObject
2053
	 */
2054
	public static function get_one_by_stage($class, $stage, $filter = '', $cache = true, $sort = '') {
0 ignored issues
show
Unused Code introduced by
The parameter $cache is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
2055
		// TODO: No identity cache operating
2056
		$items = static::get_by_stage($class, $stage, $filter, $sort, null, 1);
2057
2058
		return $items->First();
2059
	}
2060
2061
	/**
2062
	 * Gets the current version number of a specific record.
2063
	 *
2064
	 * @param string $class
2065
	 * @param string $stage
2066
	 * @param int $id
2067
	 * @param boolean $cache
2068
	 *
2069
	 * @return int
2070
	 */
2071
	public static function get_versionnumber_by_stage($class, $stage, $id, $cache = true) {
2072
		$baseClass = ClassInfo::baseDataClass($class);
2073
		$stageTable = ($stage == static::DRAFT) ? $baseClass : "{$baseClass}_{$stage}";
2074
2075
		// cached call
2076
		if($cache && isset(self::$cache_versionnumber[$baseClass][$stage][$id])) {
2077
			return self::$cache_versionnumber[$baseClass][$stage][$id];
2078
		}
2079
2080
		// get version as performance-optimized SQL query (gets called for each record in the sitetree)
2081
		$version = DB::prepared_query(
2082
			"SELECT \"Version\" FROM \"$stageTable\" WHERE \"ID\" = ?",
2083
			array($id)
2084
		)->value();
2085
2086
		// cache value (if required)
2087
		if($cache) {
2088
			if(!isset(self::$cache_versionnumber[$baseClass])) {
2089
				self::$cache_versionnumber[$baseClass] = array();
2090
			}
2091
2092
			if(!isset(self::$cache_versionnumber[$baseClass][$stage])) {
2093
				self::$cache_versionnumber[$baseClass][$stage] = array();
2094
			}
2095
2096
			self::$cache_versionnumber[$baseClass][$stage][$id] = $version;
2097
		}
2098
2099
		return $version;
2100
	}
2101
2102
	/**
2103
	 * Pre-populate the cache for Versioned::get_versionnumber_by_stage() for
2104
	 * a list of record IDs, for more efficient database querying.  If $idList
2105
	 * is null, then every record will be pre-cached.
2106
	 *
2107
	 * @param string $class
2108
	 * @param string $stage
2109
	 * @param array $idList
2110
	 */
2111
	public static function prepopulate_versionnumber_cache($class, $stage, $idList = null) {
2112
		if (!Config::inst()->get('Versioned', 'prepopulate_versionnumber_cache')) {
2113
			return;
2114
		}
2115
		$filter = "";
2116
		$parameters = array();
2117
		if($idList) {
2118
			// Validate the ID list
2119
			foreach($idList as $id) {
2120
				if(!is_numeric($id)) {
2121
					user_error("Bad ID passed to Versioned::prepopulate_versionnumber_cache() in \$idList: " . $id,
2122
					E_USER_ERROR);
2123
				}
2124
			}
2125
			$filter = 'WHERE "ID" IN ('.DB::placeholders($idList).')';
2126
			$parameters = $idList;
2127
		}
2128
2129
		$baseClass = ClassInfo::baseDataClass($class);
2130
		$stageTable = ($stage == static::DRAFT) ? $baseClass : "{$baseClass}_{$stage}";
2131
2132
		$versions = DB::prepared_query("SELECT \"ID\", \"Version\" FROM \"$stageTable\" $filter", $parameters)->map();
2133
2134
		foreach($versions as $id => $version) {
2135
			self::$cache_versionnumber[$baseClass][$stage][$id] = $version;
2136
		}
2137
	}
2138
2139
	/**
2140
	 * Get a set of class instances by the given stage.
2141
	 *
2142
	 * @param string $class The name of the class.
2143
	 * @param string $stage The name of the stage.
2144
	 * @param string $filter A filter to be inserted into the WHERE clause.
2145
	 * @param string $sort A sort expression to be inserted into the ORDER BY clause.
2146
	 * @param string $join Deprecated, use leftJoin($table, $joinClause) instead
2147
	 * @param int $limit A limit on the number of records returned from the database.
2148
	 * @param string $containerClass The container class for the result set (default is DataList)
2149
	 *
2150
	 * @return DataList A modified DataList designated to the specified stage
2151
	 */
2152
	public static function get_by_stage(
2153
		$class, $stage, $filter = '', $sort = '', $join = '', $limit = null, $containerClass = 'DataList'
2154
	) {
2155
		$result = DataObject::get($class, $filter, $sort, $join, $limit, $containerClass);
2156
		return $result->setDataQueryParam(array(
2157
			'Versioned.mode' => 'stage',
2158
			'Versioned.stage' => $stage
2159
		));
2160
	}
2161
2162
	/**
2163
	 * Delete this record from the given stage
2164
	 *
2165
	 * @param string $stage
2166
	 */
2167
	public function deleteFromStage($stage) {
2168
		$oldMode = Versioned::get_reading_mode();
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
2169
		Versioned::set_stage($stage);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
2170
		$owner = $this->owner;
2171
		$clone = clone $owner;
2172
		$clone->delete();
0 ignored issues
show
Bug introduced by
The method delete() does not exist on Versioned. Did you maybe mean deleteFromStage()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
2173
		Versioned::set_reading_mode($oldMode);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
2174
2175
		// Fix the version number cache (in case you go delete from stage and then check ExistsOnLive)
2176
		$baseClass = ClassInfo::baseDataClass($owner->class);
2177
		self::$cache_versionnumber[$baseClass][$stage][$owner->ID] = null;
2178
	}
2179
2180
	/**
2181
	 * Write the given record to the draft stage
2182
	 *
2183
	 * @param string $stage
2184
	 * @param boolean $forceInsert
2185
	 * @return int The ID of the record
2186
	 */
2187
	public function writeToStage($stage, $forceInsert = false) {
2188
		$oldMode = Versioned::get_reading_mode();
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
2189
		Versioned::set_stage($stage);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
2190
2191
		$owner = $this->owner;
2192
		$owner->forceChange();
2193
		$result = $owner->write(false, $forceInsert);
2194
		Versioned::set_reading_mode($oldMode);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
2195
2196
		return $result;
2197
	}
2198
2199
	/**
2200
	 * Roll the draft version of this record to match the published record.
2201
	 * Caution: Doesn't overwrite the object properties with the rolled back version.
2202
	 *
2203
	 * {@see doRevertToLive()} to reollback to live
2204
	 *
2205
	 * @param int $version Version number
2206
	 */
2207
	public function doRollbackTo($version) {
2208
		$owner = $this->owner;
2209
		$owner->extend('onBeforeRollback', $version);
0 ignored issues
show
Bug introduced by
The method extend() does not exist on Versioned. Did you maybe mean extendWithSuffix()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
2210
		$owner->copyVersionToStage($version, static::DRAFT, true);
2211
		$owner->writeWithoutVersion();
2212
		$owner->extend('onAfterRollback', $version);
0 ignored issues
show
Bug introduced by
The method extend() does not exist on Versioned. Did you maybe mean extendWithSuffix()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
2213
	}
2214
2215
	public function onAfterRollback($version) {
2216
		// Find record at this version
2217
		$baseClass = ClassInfo::baseDataClass($this->owner);
2218
		/** @var Versioned|DataObject $recordVersion */
2219
		$recordVersion = static::get_version($baseClass, $this->owner->ID, $version);
2220
2221
		// Note that unlike other publishing actions, rollback is NOT recursive;
2222
		// The owner collects all objects and writes them back using writeToStage();
2223
		foreach ($recordVersion->findOwned() as $object) {
0 ignored issues
show
Bug introduced by
The method findOwned 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...
2224
			/** @var Versioned|DataObject $object */
2225
			$object->writeToStage(static::DRAFT);
0 ignored issues
show
Bug introduced by
The method writeToStage 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...
2226
		}
2227
	}
2228
2229
	/**
2230
	 * Return the latest version of the given record.
2231
	 *
2232
	 * @param string $class
2233
	 * @param int $id
2234
	 * @return DataObject
2235
	 */
2236
	public static function get_latest_version($class, $id) {
2237
		$baseClass = ClassInfo::baseDataClass($class);
2238
		$list = DataList::create($baseClass)
2239
			->setDataQueryParam("Versioned.mode", "latest_versions");
2240
2241
		return $list->byID($id);
2242
	}
2243
2244
	/**
2245
	 * Returns whether the current record is the latest one.
2246
	 *
2247
	 * @todo Performance - could do this directly via SQL.
2248
	 *
2249
	 * @see get_latest_version()
2250
	 * @see latestPublished
2251
	 *
2252
	 * @return boolean
2253
	 */
2254
	public function isLatestVersion() {
2255
		$owner = $this->owner;
2256
		if(!$owner->isInDB()) {
2257
			return false;
2258
		}
2259
2260
		$version = static::get_latest_version($owner->class, $owner->ID);
2261
		return ($version->Version == $owner->Version);
2262
	}
2263
2264
	/**
2265
	 * Check if this record exists on live
2266
	 *
2267
	 * @return bool
2268
	 */
2269
	public function isPublished() {
2270
		$owner = $this->owner;
2271
		if(!$owner->isInDB()) {
2272
			return false;
2273
		}
2274
2275
		// Non-staged objects are considered "published" if saved
2276
		if(!$this->hasStages()) {
2277
			return true;
2278
		}
2279
2280
		$baseClass = ClassInfo::baseDataClass($owner->class);
2281
		$table = $this->stageTable($baseClass, static::LIVE);
2282
		$result = DB::prepared_query(
2283
			"SELECT COUNT(*) FROM \"{$table}\" WHERE \"{$table}\".\"ID\" = ?",
2284
			array($owner->ID)
2285
		);
2286
		return (bool)$result->value();
2287
	}
2288
2289
	/**
2290
	 * Check if this record exists on the draft stage
2291
	 *
2292
	 * @return bool
2293
	 */
2294
	public function isOnDraft() {
2295
		$owner = $this->owner;
2296
		if(!$owner->isInDB()) {
2297
			return false;
2298
		}
2299
2300
		$table = ClassInfo::baseDataClass($owner->class);
2301
		$result = DB::prepared_query(
2302
			"SELECT COUNT(*) FROM \"{$table}\" WHERE \"{$table}\".\"ID\" = ?",
2303
			array($owner->ID)
2304
		);
2305
		return (bool)$result->value();
2306
	}
2307
2308
2309
2310
	/**
2311
	 * Return the equivalent of a DataList::create() call, querying the latest
2312
	 * version of each record stored in the (class)_versions tables.
2313
	 *
2314
	 * In particular, this will query deleted records as well as active ones.
2315
	 *
2316
	 * @param string $class
2317
	 * @param string $filter
2318
	 * @param string $sort
2319
	 * @return DataList
2320
	 */
2321
	public static function get_including_deleted($class, $filter = "", $sort = "") {
2322
		$list = DataList::create($class)
2323
			->where($filter)
2324
			->sort($sort)
2325
			->setDataQueryParam("Versioned.mode", "latest_versions");
2326
2327
		return $list;
2328
	}
2329
2330
	/**
2331
	 * Return the specific version of the given id.
2332
	 *
2333
	 * Caution: The record is retrieved as a DataObject, but saving back
2334
	 * modifications via write() will create a new version, rather than
2335
	 * modifying the existing one.
2336
	 *
2337
	 * @param string $class
2338
	 * @param int $id
2339
	 * @param int $version
2340
	 *
2341
	 * @return DataObject
2342
	 */
2343
	public static function get_version($class, $id, $version) {
2344
		$baseClass = ClassInfo::baseDataClass($class);
2345
		$list = DataList::create($baseClass)
2346
			->setDataQueryParam([
2347
				"Versioned.mode" => 'version',
2348
				"Versioned.version" => $version
2349
			]);
2350
2351
		return $list->byID($id);
2352
	}
2353
2354
	/**
2355
	 * Return a list of all versions for a given id.
2356
	 *
2357
	 * @param string $class
2358
	 * @param int $id
2359
	 *
2360
	 * @return DataList
2361
	 */
2362
	public static function get_all_versions($class, $id) {
2363
		$list = DataList::create($class)
2364
			->filter('ID', $id)
2365
			->setDataQueryParam('Versioned.mode', 'all_versions');
2366
2367
		return $list;
2368
	}
2369
2370
	/**
2371
	 * @param array $labels
2372
	 */
2373
	public function updateFieldLabels(&$labels) {
2374
		$labels['Versions'] = _t('Versioned.has_many_Versions', 'Versions', 'Past Versions of this record');
2375
	}
2376
2377
	/**
2378
	 * @param FieldList
2379
	 */
2380
	public function updateCMSFields(FieldList $fields) {
2381
		// remove the version field from the CMS as this should be left
2382
		// entirely up to the extension (not the cms user).
2383
		$fields->removeByName('Version');
2384
	}
2385
2386
	/**
2387
	 * Ensure version ID is reset to 0 on duplicate
2388
	 *
2389
	 * @param DataObject $source Record this was duplicated from
2390
	 * @param bool $doWrite
2391
	 */
2392
	public function onBeforeDuplicate($source, $doWrite) {
0 ignored issues
show
Unused Code introduced by
The parameter $source is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $doWrite is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
2393
		$this->owner->Version = 0;
2394
	}
2395
2396
	public function flushCache() {
2397
		self::$cache_versionnumber = array();
2398
	}
2399
2400
	/**
2401
	 * Return a piece of text to keep DataObject cache keys appropriately specific.
2402
	 *
2403
	 * @return string
2404
	 */
2405
	public function cacheKeyComponent() {
2406
		return 'versionedmode-'.static::get_reading_mode();
2407
	}
2408
2409
	/**
2410
	 * Returns an array of possible stages.
2411
	 *
2412
	 * @return array
2413
	 */
2414
	public function getVersionedStages() {
2415
		if($this->hasStages()) {
2416
			return [static::DRAFT, static::LIVE];
2417
		} else {
2418
			return [static::DRAFT];
2419
		}
2420
	}
2421
2422
	public static function get_template_global_variables() {
2423
		return array(
2424
			'CurrentReadingMode' => 'get_reading_mode'
2425
		);
2426
	}
2427
2428
	/**
2429
	 * Check if this object has stages
2430
	 *
2431
	 * @return bool True if this object is staged
2432
	 */
2433
	public function hasStages() {
2434
		return $this->mode === static::STAGEDVERSIONED;
2435
	}
2436
}
2437
2438
/**
2439
 * Represents a single version of a record.
2440
 *
2441
 * @package framework
2442
 * @subpackage model
2443
 *
2444
 * @see Versioned
2445
 */
2446
class Versioned_Version extends ViewableData {
2447
	/**
2448
	 * @var array
2449
	 */
2450
	protected $record;
2451
2452
	/**
2453
	 * @var DataObject
2454
	 */
2455
	protected $object;
2456
2457
	/**
2458
	 * Create a new version from a database row
2459
	 *
2460
	 * @param array $record
2461
	 */
2462
	public function __construct($record) {
2463
		$this->record = $record;
2464
		$record['ID'] = $record['RecordID'];
2465
		$className = $record['ClassName'];
2466
2467
		$this->object = ClassInfo::exists($className) ? new $className($record) : new DataObject($record);
2468
		$this->failover = $this->object;
2469
2470
		parent::__construct();
2471
	}
2472
2473
	/**
2474
	 * Either 'published' if published, or 'internal' if not.
2475
	 *
2476
	 * @return string
2477
	 */
2478
	public function PublishedClass() {
2479
		return $this->record['WasPublished'] ? 'published' : 'internal';
2480
	}
2481
2482
	/**
2483
	 * Author of this DataObject
2484
	 *
2485
	 * @return Member
2486
	 */
2487
	public function Author() {
2488
		return Member::get()->byId($this->record['AuthorID']);
2489
	}
2490
2491
	/**
2492
	 * Member object of the person who last published this record
2493
	 *
2494
	 * @return Member
2495
	 */
2496
	public function Publisher() {
2497
		if (!$this->record['WasPublished']) {
2498
			return null;
2499
		}
2500
2501
		return Member::get()->byId($this->record['PublisherID']);
2502
	}
2503
2504
	/**
2505
	 * True if this record is published via publish() method
2506
	 *
2507
	 * @return boolean
2508
	 */
2509
	public function Published() {
2510
		return !empty($this->record['WasPublished']);
2511
	}
2512
2513
	/**
2514
	 * Traverses to a field referenced by relationships between data objects, returning the value
2515
	 * The path to the related field is specified with dot separated syntax (eg: Parent.Child.Child.FieldName)
2516
	 *
2517
	 * @param $fieldName string
2518
	 * @return string | null - will return null on a missing value
2519
	 */
2520
	public function relField($fieldName) {
2521
		$component = $this;
2522
2523
		// We're dealing with relations here so we traverse the dot syntax
2524
		if(strpos($fieldName, '.') !== false) {
2525
			$relations = explode('.', $fieldName);
2526
			$fieldName = array_pop($relations);
2527
			foreach($relations as $relation) {
2528
				// Inspect $component for element $relation
2529
				if($component->hasMethod($relation)) {
2530
					// Check nested method
2531
						$component = $component->$relation();
2532
				} elseif($component instanceof SS_List) {
2533
					// Select adjacent relation from DataList
2534
						$component = $component->relation($relation);
2535
				} elseif($component instanceof DataObject
2536
					&& ($dbObject = $component->dbObject($relation))
2537
				) {
2538
					// Select db object
2539
					$component = $dbObject;
2540
				} else {
2541
					user_error("$relation is not a relation/field on ".get_class($component), E_USER_ERROR);
2542
				}
2543
			}
2544
		}
2545
2546
		// Bail if the component is null
2547
		if(!$component) {
2548
			return null;
2549
		}
2550
		if ($component->hasMethod($fieldName)) {
2551
			return $component->$fieldName();
2552
		}
2553
		return $component->$fieldName;
2554
	}
2555
}
2556