Completed
Pull Request — master (#5157)
by Damian
11:28
created

Versioned::updateInheritableQueryParams()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 36
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 1
Metric Value
c 2
b 0
f 1
dl 0
loc 36
rs 8.439
cc 5
eloc 20
nc 5
nop 1
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
	{
949
		// Find objects in these relationships
950
		return $this->findRelatedObjects('owned_by', $recursive, $list);
951
	}
952
953
	/**
954
	 * Find objects in the given relationships, merging them into the given list
955
	 *
956
	 * @param array $source Config property to extract relationships from
957
	 * @param bool $recursive True if recursive
958
	 * @param ArrayList $list Optional list to add items to
959
	 * @return ArrayList The list
960
	 */
961
	public function findRelatedObjects($source, $recursive = true, $list = null)
962
	{
963
		if (!$list) {
964
			$list = new ArrayList();
965
		}
966
967
		// Skip search for unsaved records
968
		$owner = $this->owner;
969
		if(!$owner->isInDB()) {
970
			return $list;
971
		}
972
973
		$relationships = $owner->config()->{$source};
974
		foreach($relationships as $relationship) {
975
			// Warn if invalid config
976
			if(!$owner->hasMethod($relationship)) {
977
				trigger_error(sprintf(
978
					"Invalid %s config value \"%s\" on object on class \"%s\"",
979
					$source,
980
					$relationship,
981
					$owner->class
982
				), E_USER_WARNING);
983
				continue;
984
			}
985
986
			// Inspect value of this relationship
987
			$items = $owner->{$relationship}();
988
			if(!$items) {
989
				continue;
990
			}
991
			if($items instanceof DataObject) {
992
				$items = array($items);
993
			}
994
995
			/** @var Versioned|DataObject $item */
996
			foreach($items as $item) {
997
				// Identify item
998
				$itemKey = $item->class . '/' . $item->ID;
999
1000
				// Skip unsaved, unversioned, or already checked objects
1001
				if(!$item->isInDB() || !$item->has_extension('Versioned') || isset($list[$itemKey])) {
1002
					continue;
1003
				}
1004
1005
				// Save record
1006
				$list[$itemKey] = $item;
1007
				if($recursive) {
1008
					$item->findRelatedObjects($source, true, $list);
1009
				};
1010
			}
1011
		}
1012
		return $list;
1013
	}
1014
1015
	/**
1016
	 * This function should return true if the current user can publish this record.
1017
	 * It can be overloaded to customise the security model for an application.
1018
	 *
1019
	 * Denies permission if any of the following conditions is true:
1020
	 * - canPublish() on any extension returns false
1021
	 * - canEdit() returns false
1022
	 *
1023
	 * @param Member $member
1024
	 * @return bool True if the current user can publish this record.
1025
	 */
1026
	public function canPublish($member = null) {
1027
		// Skip if invoked by extendedCan()
1028
		if(func_num_args() > 4) {
1029
			return null;
1030
		}
1031
1032
		if(!$member) {
1033
			$member = Member::currentUser();
1034
		}
1035
1036
		if(Permission::checkMember($member, "ADMIN")) {
1037
			return true;
1038
		}
1039
1040
		// Standard mechanism for accepting permission changes from extensions
1041
		$owner = $this->owner;
1042
		$extended = $owner->extendedCan('canPublish', $member);
1043
		if($extended !== null) {
1044
			return $extended;
1045
		}
1046
1047
		// Default to relying on edit permission
1048
		return $owner->canEdit($member);
1049
	}
1050
1051
	/**
1052
	 * Check if the current user can delete this record from live
1053
	 *
1054
	 * @param null $member
1055
	 * @return mixed
1056
	 */
1057
	public function canUnpublish($member = null) {
1058
		// Skip if invoked by extendedCan()
1059
		if(func_num_args() > 4) {
1060
			return null;
1061
		}
1062
1063
		if(!$member) {
1064
			$member = Member::currentUser();
1065
		}
1066
1067
		if(Permission::checkMember($member, "ADMIN")) {
1068
			return true;
1069
		}
1070
1071
		// Standard mechanism for accepting permission changes from extensions
1072
		$owner = $this->owner;
1073
		$extended = $owner->extendedCan('canUnpublish', $member);
1074
		if($extended !== null) {
1075
			return $extended;
1076
		}
1077
1078
		// Default to relying on canPublish
1079
		return $owner->canPublish($member);
0 ignored issues
show
Bug introduced by
It seems like $member defined by \Member::currentUser() on line 1064 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...
1080
	}
1081
1082
	/**
1083
	 * Check if the current user is allowed to archive this record.
1084
	 * If extended, ensure that both canDelete and canUnpublish are extended also
1085
	 *
1086
	 * @param Member $member
1087
	 * @return bool
1088
	 */
1089
	public function canArchive($member = null) {
1090
		// Skip if invoked by extendedCan()
1091
		if(func_num_args() > 4) {
1092
			return null;
1093
		}
1094
1095
		if(!$member) {
1096
            $member = Member::currentUser();
1097
        }
1098
1099
		if(Permission::checkMember($member, "ADMIN")) {
1100
			return true;
1101
		}
1102
1103
		// Standard mechanism for accepting permission changes from extensions
1104
		$owner = $this->owner;
1105
		$extended = $owner->extendedCan('canArchive', $member);
1106
		if($extended !== null) {
1107
            return $extended;
1108
        }
1109
1110
		// Check if this record can be deleted from stage
1111
        if(!$owner->canDelete($member)) {
1112
            return false;
1113
        }
1114
1115
        // Check if we can delete from live
1116
        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...
1117
            return false;
1118
        }
1119
1120
		return true;
1121
	}
1122
1123
	/**
1124
	 * Check if the user can revert this record to live
1125
	 *
1126
	 * @param Member $member
1127
	 * @return bool
1128
	 */
1129
	public function canRevertToLive($member = null) {
1130
		$owner = $this->owner;
1131
1132
		// Skip if invoked by extendedCan()
1133
		if(func_num_args() > 4) {
1134
			return null;
1135
		}
1136
1137
		// Can't revert if not on live
1138
		if(!$owner->isPublished()) {
1139
			return false;
1140
		}
1141
1142
		if(!$member) {
1143
            $member = Member::currentUser();
1144
        }
1145
1146
		if(Permission::checkMember($member, "ADMIN")) {
1147
			return true;
1148
		}
1149
1150
		// Standard mechanism for accepting permission changes from extensions
1151
		$extended = $owner->extendedCan('canRevertToLive', $member);
1152
		if($extended !== null) {
1153
            return $extended;
1154
        }
1155
1156
		// Default to canEdit
1157
		return $owner->canEdit($member);
1158
	}
1159
1160
	/**
1161
	 * Extend permissions to include additional security for objects that are not published to live.
1162
	 *
1163
	 * @param Member $member
1164
	 * @return bool|null
1165
	 */
1166
	public function canView($member = null) {
1167
		// Invoke default version-gnostic canView
1168
		if ($this->owner->canViewVersioned($member) === false) {
1169
			return false;
1170
		}
1171
	}
1172
1173
	/**
1174
	 * Determine if there are any additional restrictions on this object for the given reading version.
1175
	 *
1176
	 * Override this in a subclass to customise any additional effect that Versioned applies to canView.
1177
	 *
1178
	 * This is expected to be called by canView, and thus is only responsible for denying access if
1179
	 * the default canView would otherwise ALLOW access. Thus it should not be called in isolation
1180
	 * as an authoritative permission check.
1181
	 *
1182
	 * This has the following extension points:
1183
	 *  - canViewDraft is invoked if Mode = stage and Stage = stage
1184
	 *  - canViewArchived is invoked if Mode = archive
1185
	 *
1186
	 * @param Member $member
1187
	 * @return bool False is returned if the current viewing mode denies visibility
1188
	 */
1189
	public function canViewVersioned($member = null) {
1190
		// Bypass when live stage
1191
		$owner = $this->owner;
1192
		$mode = $owner->getSourceQueryParam("Versioned.mode");
1193
		$stage = $owner->getSourceQueryParam("Versioned.stage");
1194
		if ($mode === 'stage' && $stage === static::LIVE) {
1195
			return true;
1196
		}
1197
1198
		// Bypass if site is unsecured
1199
		if (Session::get('unsecuredDraftSite')) {
1200
			return true;
1201
		}
1202
1203
		// Bypass if record doesn't have a live stage
1204
		if(!$this->hasStages()) {
1205
			return true;
1206
		}
1207
1208
		// If we weren't definitely loaded from live, and we can't view non-live content, we need to
1209
		// check to make sure this version is the live version and so can be viewed
1210
		$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...
1211
		if ($latestVersion == $owner->Version) {
1212
			// Even if this is loaded from a non-live stage, this is the live version
1213
			return true;
1214
		}
1215
1216
		// Extend versioned behaviour
1217
		$extended = $owner->extendedCan('canViewNonLive', $member);
1218
		if($extended !== null) {
1219
			return (bool)$extended;
1220
		}
1221
1222
		// Fall back to default permission check
1223
		$permissions = Config::inst()->get($owner->class, 'non_live_permissions', Config::FIRST_SET);
1224
		$check = Permission::checkMember($member, $permissions);
1225
		return (bool)$check;
1226
	}
1227
1228
	/**
1229
	 * Determines canView permissions for the latest version of this object on a specific stage.
1230
	 * Usually the stage is read from {@link Versioned::current_stage()}.
1231
	 *
1232
	 * This method should be invoked by user code to check if a record is visible in the given stage.
1233
	 *
1234
	 * This method should not be called via ->extend('canViewStage'), but rather should be
1235
	 * overridden in the extended class.
1236
	 *
1237
	 * @param string $stage
1238
	 * @param Member $member
1239
	 * @return bool
1240
	 */
1241
	public function canViewStage($stage = 'Live', $member = null) {
1242
		$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...
1243
		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...
1244
1245
		$owner = $this->owner;
1246
		$versionFromStage = DataObject::get($owner->class)->byID($owner->ID);
1247
1248
		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...
1249
		return $versionFromStage ? $versionFromStage->canView($member) : false;
1250
	}
1251
1252
	/**
1253
	 * Determine if a table is supporting the Versioned extensions (e.g.
1254
	 * $table_versions does exists).
1255
	 *
1256
	 * @param string $table Table name
1257
	 * @return boolean
1258
	 */
1259
	public function canBeVersioned($table) {
1260
		return ClassInfo::exists($table)
1261
			&& is_subclass_of($table, 'DataObject')
1262
			&& DataObject::has_own_table($table);
1263
	}
1264
1265
	/**
1266
	 * Check if a certain table has the 'Version' field.
1267
	 *
1268
	 * @param string $table Table name
1269
	 *
1270
	 * @return boolean Returns false if the field isn't in the table, true otherwise
1271
	 */
1272
	public function hasVersionField($table) {
1273
		// Strip "_Live" from end of table
1274
		$live = static::LIVE;
1275
		if($this->hasStages() && preg_match("/^(?<table>.*)_{$live}$/", $table, $matches)) {
1276
			$table = $matches['table'];
1277
		}
1278
1279
		// Base table has version field
1280
		return $table === ClassInfo::baseDataClass($table);
1281
	}
1282
1283
	/**
1284
	 * @param string $table
1285
	 *
1286
	 * @return string
1287
	 */
1288
	public function extendWithSuffix($table) {
1289
		$owner = $this->owner;
1290
		$versionableExtensions = $owner->config()->versionableExtensions;
1291
1292
		if(count($versionableExtensions)){
1293
			foreach ($versionableExtensions as $versionableExtension => $suffixes) {
1294
				if ($owner->hasExtension($versionableExtension)) {
1295
					$ext = $owner->getExtensionInstance($versionableExtension);
1296
					$ext->setOwner($owner);
1297
				$table = $ext->extendWithSuffix($table);
1298
				$ext->clearOwner();
1299
			}
1300
		}
1301
		}
1302
1303
		return $table;
1304
	}
1305
1306
	/**
1307
	 * Get the latest published DataObject.
1308
	 *
1309
	 * @return DataObject
1310
	 */
1311
	public function latestPublished() {
1312
		// Get the root data object class - this will have the version field
1313
		$owner = $this->owner;
1314
		$table1 = ClassInfo::baseDataClass($owner);
1315
		$table2 = $this->stageTable($table1, static::LIVE);
1316
1317
		return DB::prepared_query("SELECT \"$table1\".\"Version\" = \"$table2\".\"Version\" FROM \"$table1\"
1318
			 INNER JOIN \"$table2\" ON \"$table1\".\"ID\" = \"$table2\".\"ID\"
1319
			 WHERE \"$table1\".\"ID\" = ?",
1320
			array($owner->ID)
1321
		)->value();
1322
	}
1323
1324
	/**
1325
	 * Provides a simple doPublish action for Versioned dataobjects
1326
	 *
1327
	 * @return bool True if publish was successful
1328
	 */
1329
	public function doPublish() {
1330
		$owner = $this->owner;
1331
		if(!$owner->canPublish()) {
1332
			return false;
1333
		}
1334
1335
		$owner->invokeWithExtensions('onBeforePublish');
1336
		$owner->write();
1337
		$owner->publish(static::DRAFT, static::LIVE);
1338
		$owner->invokeWithExtensions('onAfterPublish');
1339
		return true;
1340
	}
1341
1342
	/**
1343
	 * Trigger publishing of owned objects
1344
	 */
1345
	public function onAfterPublish() {
1346
		$owner = $this->owner;
1347
1348
		// Publish owned objects
1349
		foreach ($owner->findOwned(false) as $object) {
1350
			/** @var Versioned|DataObject $object */
1351
			$object->doPublish();
0 ignored issues
show
Bug introduced by
The method doPublish 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...
1352
		}
1353
1354
		// Unlink any objects disowned as a result of this action
1355
		// I.e. objects which aren't owned anymore by this record, but are by the old live record
1356
		$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...
1357
	}
1358
1359
	/**
1360
	 * Set foreign keys of has_many objects to 0 where those objects were
1361
	 * disowned as a result of a partial publish / unpublish.
1362
	 * I.e. this object and its owned objects were recently written to $targetStage,
1363
	 * but deleted objects were not.
1364
	 *
1365
	 * Note that this operation does not create any new Versions
1366
	 *
1367
	 * @param string $sourceStage Objects in this stage will not be unlinked.
1368
	 * @param string $targetStage Objects which exist in this stage but not $sourceStage
1369
	 * will be unlinked.
1370
	 */
1371
	public function unlinkDisownedObjects($sourceStage, $targetStage) {
1372
		$owner = $this->owner;
1373
1374
		// after publishing, objects which used to be owned need to be
1375
		// dis-connected from this object (set ForeignKeyID = 0)
1376
		$owns = $owner->config()->owns;
1377
		$hasMany = $owner->config()->has_many;
1378
		if(empty($owns) || empty($hasMany)) {
1379
			return;
1380
		}
1381
1382
		$ownedHasMany = array_intersect($owns, array_keys($hasMany));
1383
		foreach($ownedHasMany as $relationship) {
1384
			// Find metadata on relationship
1385
			$joinClass = $owner->hasManyComponent($relationship);
1386
			$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...
1387
			$idField = $polymorphic ? "{$joinField}ID" : $joinField;
1388
			$joinTable = ClassInfo::table_for_object_field($joinClass, $idField);
1389
1390
			// Generate update query which will unlink disowned objects
1391
			$targetTable = $this->stageTable($joinTable, $targetStage);
1392
			$disowned = new SQLUpdate("\"{$targetTable}\"");
1393
			$disowned->assign("\"{$idField}\"", 0);
1394
			$disowned->addWhere(array(
1395
				"\"{$targetTable}\".\"{$idField}\"" => $owner->ID
1396
			));
1397
1398
			// Build exclusion list (items to owned objects we need to keep)
1399
			$sourceTable = $this->stageTable($joinTable, $sourceStage);
1400
			$owned = new SQLSelect("\"{$sourceTable}\".\"ID\"", "\"{$sourceTable}\"");
1401
			$owned->addWhere(array(
1402
				"\"{$sourceTable}\".\"{$idField}\"" => $owner->ID
1403
			));
1404
1405
			// Apply class condition if querying on polymorphic has_one
1406
			if($polymorphic) {
1407
				$disowned->assign("\"{$joinField}Class\"", null);
1408
				$disowned->addWhere(array(
1409
					"\"{$targetTable}\".\"{$joinField}Class\"" => get_class($owner)
1410
				));
1411
				$owned->addWhere(array(
1412
					"\"{$sourceTable}\".\"{$joinField}Class\"" => get_class($owner)
1413
				));
1414
			}
1415
1416
			// Merge queries and perform unlink
1417
			$ownedSQL = $owned->sql($ownedParams);
1418
			$disowned->addWhere(array(
1419
				"\"{$targetTable}\".\"ID\" NOT IN ({$ownedSQL})" => $ownedParams
1420
			));
1421
1422
			$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...
1423
1424
			$disowned->execute();
1425
		}
1426
	}
1427
1428
	/**
1429
	 * Removes the record from both live and stage
1430
	 *
1431
	 * @return bool Success
1432
	 */
1433
	public function doArchive() {
1434
		$owner = $this->owner;
1435
		if(!$owner->canArchive()) {
1436
			return false;
1437
		}
1438
1439
		$owner->invokeWithExtensions('onBeforeArchive', $this);
1440
		$owner->doUnpublish();
1441
		$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...
1442
		$owner->invokeWithExtensions('onAfterArchive', $this);
1443
1444
		return true;
1445
	}
1446
1447
	/**
1448
	 * Removes this record from the live site
1449
	 *
1450
	 * @return bool Flag whether the unpublish was successful
1451
	 */
1452
	public function doUnpublish() {
1453
		$owner = $this->owner;
1454
		if(!$owner->canUnpublish()) {
1455
			return false;
1456
		}
1457
1458
		// Skip if this record isn't saved
1459
		if(!$owner->isInDB()) {
1460
			return false;
1461
		}
1462
1463
		// Skip if this record isn't on live
1464
		if(!$owner->isPublished()) {
1465
			return false;
1466
		}
1467
1468
		$owner->invokeWithExtensions('onBeforeUnpublish');
1469
1470
		$origStage = static::get_stage();
1471
		static::set_stage(static::LIVE);
1472
1473
		// This way our ID won't be unset
1474
		$clone = clone $owner;
1475
		$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...
1476
1477
		static::set_stage($origStage);
1478
1479
		$owner->invokeWithExtensions('onAfterUnpublish');
1480
		return true;
1481
	}
1482
1483
	/**
1484
	 * Trigger unpublish of owning objects
1485
	 */
1486
	public function onAfterUnpublish() {
1487
		$owner = $this->owner;
1488
1489
		// Any objects which owned (and thus relied on the unpublished object) are now unpublished automatically.
1490
		foreach ($owner->findOwners(false) as $object) {
1491
			/** @var Versioned|DataObject $object */
1492
			$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...
1493
		}
1494
	}
1495
1496
1497
	/**
1498
	 * Revert the draft changes: replace the draft content with the content on live
1499
	 *
1500
	 * @return bool True if the revert was successful
1501
	 */
1502
	public function doRevertToLive() {
1503
		$owner = $this->owner;
1504
		if(!$owner->canRevertToLive()) {
1505
			return false;
1506
		}
1507
1508
		$owner->invokeWithExtensions('onBeforeRevertToLive');
1509
		$owner->publish("Live", "Stage", false);
1510
		$owner->invokeWithExtensions('onAfterRevertToLive');
1511
		return true;
1512
	}
1513
1514
	/**
1515
	 * Trigger revert of all owned objects to stage
1516
	 */
1517
	public function onAfterRevertToLive() {
1518
		$owner = $this->owner;
1519
		/** @var Versioned|DataObject $liveOwner */
1520
		$liveOwner = static::get_by_stage(get_class($owner), static::LIVE)
1521
			->byID($owner->ID);
1522
1523
		// Revert any owned objects from the live stage only
1524
		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...
1525
			/** @var Versioned|DataObject $object */
1526
			$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...
1527
		}
1528
1529
		// Unlink any objects disowned as a result of this action
1530
		// I.e. objects which aren't owned anymore by this record, but are by the old draft record
1531
		$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...
1532
	}
1533
1534
	/**
1535
	 * Move a database record from one stage to the other.
1536
	 *
1537
	 * @param int|string $fromStage Place to copy from.  Can be either a stage name or a version number.
1538
	 * @param string $toStage Place to copy to.  Must be a stage name.
1539
	 * @param bool $createNewVersion Set this to true to create a new version number.
1540
	 * By default, the existing version number will be copied over.
1541
	 */
1542
	public function publish($fromStage, $toStage, $createNewVersion = false) {
1543
		$owner = $this->owner;
1544
		$owner->invokeWithExtensions('onBeforeVersionedPublish', $fromStage, $toStage, $createNewVersion);
1545
1546
		$baseClass = ClassInfo::baseDataClass($owner->class);
1547
1548
		/** @var Versioned|DataObject $from */
1549
		if(is_numeric($fromStage)) {
1550
			$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...
1551
		} else {
1552
			$owner->flushCache();
1553
			$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...
1554
				"\"{$baseClass}\".\"ID\" = ?" => $owner->ID
1555
			));
1556
		}
1557
		if(!$from) {
1558
			throw new InvalidArgumentException("Can't find {$baseClass}#{$owner->ID} in stage {$fromStage}");
1559
		}
1560
1561
		$from->forceChange();
1562
		if($createNewVersion) {
1563
			// Clear version to be automatically created on write
1564
			$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...
1565
		} else {
1566
			$from->migrateVersion($from->Version);
1567
1568
			// Mark this version as having been published at some stage
1569
			$publisherID = isset(Member::currentUser()->ID) ? Member::currentUser()->ID : 0;
1570
			$extTable = $this->extendWithSuffix($baseClass);
1571
			DB::prepared_query("UPDATE \"{$extTable}_versions\"
1572
				SET \"WasPublished\" = ?, \"PublisherID\" = ?
1573
				WHERE \"RecordID\" = ? AND \"Version\" = ?",
1574
				array(1, $publisherID, $from->ID, $from->Version)
1575
			);
1576
		}
1577
1578
		// Change to new stage, write, and revert state
1579
		$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...
1580
		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...
1581
1582
		// Migrate stage prior to write
1583
		$from->setSourceQueryParam('Versioned.mode', 'stage');
1584
		$from->setSourceQueryParam('Versioned.stage', $toStage);
1585
1586
		$conn = DB::get_conn();
1587
		if(method_exists($conn, 'allowPrimaryKeyEditing')) {
1588
			$conn->allowPrimaryKeyEditing($baseClass, true);
1589
			$from->write();
1590
			$conn->allowPrimaryKeyEditing($baseClass, false);
1591
		} else {
1592
			$from->write();
1593
		}
1594
1595
		$from->destroy();
1596
1597
		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...
1598
1599
		$owner->invokeWithExtensions('onAfterVersionedPublish', $fromStage, $toStage, $createNewVersion);
1600
	}
1601
1602
	/**
1603
	 * Set the migrating version.
1604
	 *
1605
	 * @param string $version The version.
1606
	 */
1607
	public function migrateVersion($version) {
1608
		$this->migratingVersion = $version;
1609
	}
1610
1611
	/**
1612
	 * Compare two stages to see if they're different.
1613
	 *
1614
	 * Only checks the version numbers, not the actual content.
1615
	 *
1616
	 * @param string $stage1 The first stage to check.
1617
	 * @param string $stage2
1618
	 * @return bool
1619
	 */
1620
	public function stagesDiffer($stage1, $stage2) {
1621
		$table1 = $this->baseTable($stage1);
1622
		$table2 = $this->baseTable($stage2);
1623
1624
		$owner = $this->owner;
1625
		if(!is_numeric($owner->ID)) {
1626
			return true;
1627
		}
1628
1629
		// We test for equality - if one of the versions doesn't exist, this
1630
		// will be false.
1631
1632
		// TODO: DB Abstraction: if statement here:
1633
		$stagesAreEqual = DB::prepared_query(
1634
			"SELECT CASE WHEN \"$table1\".\"Version\"=\"$table2\".\"Version\" THEN 1 ELSE 0 END
1635
			 FROM \"$table1\" INNER JOIN \"$table2\" ON \"$table1\".\"ID\" = \"$table2\".\"ID\"
1636
			 AND \"$table1\".\"ID\" = ?",
1637
			array($owner->ID)
1638
		)->value();
1639
1640
		return !$stagesAreEqual;
1641
	}
1642
1643
	/**
1644
	 * @param string $filter
1645
	 * @param string $sort
1646
	 * @param string $limit
1647
	 * @param string $join Deprecated, use leftJoin($table, $joinClause) instead
1648
	 * @param string $having
1649
	 * @return ArrayList
1650
	 */
1651
	public function Versions($filter = "", $sort = "", $limit = "", $join = "", $having = "") {
1652
		return $this->allVersions($filter, $sort, $limit, $join, $having);
1653
	}
1654
1655
	/**
1656
	 * Return a list of all the versions available.
1657
	 *
1658
	 * @param  string $filter
1659
	 * @param  string $sort
1660
	 * @param  string $limit
1661
	 * @param  string $join   Deprecated, use leftJoin($table, $joinClause) instead
1662
	 * @param  string $having
1663
	 * @return ArrayList
1664
	 */
1665
	public function allVersions($filter = "", $sort = "", $limit = "", $join = "", $having = "") {
1666
		// Make sure the table names are not postfixed (e.g. _Live)
1667
		$oldMode = static::get_reading_mode();
1668
		static::set_stage('Stage');
1669
1670
		$owner = $this->owner;
1671
		$list = DataObject::get(get_class($owner), $filter, $sort, $join, $limit);
1672
		if($having) {
1673
			$list->having($having);
1674
		}
1675
1676
		$query = $list->dataQuery()->query();
1677
1678
		foreach($query->getFrom() as $table => $tableJoin) {
1679
			if(is_string($tableJoin) && $tableJoin[0] == '"') {
1680
				$baseTable = str_replace('"','',$tableJoin);
1681
			} elseif(is_string($tableJoin) && substr($tableJoin,0,5) != 'INNER') {
1682
				$query->setFrom(array(
1683
					$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...
1684
						. " AND \"$table\".\"Version\" = \"{$baseTable}_versions\".\"Version\""
1685
				));
1686
			}
1687
			$query->renameTable($table, $table . '_versions');
1688
		}
1689
1690
		// Add all <basetable>_versions columns
1691
		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...
1692
			$query->selectField(sprintf('"%s_versions"."%s"', $baseTable, $name), $name);
1693
		}
1694
1695
		$query->addWhere(array(
1696
			"\"{$baseTable}_versions\".\"RecordID\" = ?" => $owner->ID
1697
		));
1698
		$query->setOrderBy(($sort) ? $sort
1699
			: "\"{$baseTable}_versions\".\"LastEdited\" DESC, \"{$baseTable}_versions\".\"Version\" DESC");
1700
1701
		$records = $query->execute();
1702
		$versions = new ArrayList();
1703
1704
		foreach($records as $record) {
1705
			$versions->push(new Versioned_Version($record));
1706
		}
1707
1708
		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...
1709
		return $versions;
1710
	}
1711
1712
	/**
1713
	 * Compare two version, and return the diff between them.
1714
	 *
1715
	 * @param string $from The version to compare from.
1716
	 * @param string $to The version to compare to.
1717
	 *
1718
	 * @return DataObject
1719
	 */
1720
	public function compareVersions($from, $to) {
1721
		$owner = $this->owner;
1722
		$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...
1723
		$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...
1724
1725
		$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 1723 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...
1726
1727
		return $diff->diffedData();
1728
	}
1729
1730
	/**
1731
	 * Return the base table - the class that directly extends DataObject.
1732
	 *
1733
	 * @param string $stage
1734
	 * @return string
1735
	 */
1736
	public function baseTable($stage = null) {
1737
		$baseClass = ClassInfo::baseDataClass($this->owner);
1738
		return $this->stageTable($baseClass, $stage);
1739
	}
1740
1741
	/**
1742
	 * Given a class and stage determine the table name.
1743
	 *
1744
	 * Note: Stages this asset does not exist in will default to the draft table.
1745
	 *
1746
	 * @param string $class
1747
	 * @param string $stage
1748
	 * @return string Table name
1749
	 */
1750
	public function stageTable($class, $stage) {
1751
		if($this->hasStages() && $stage === static::LIVE) {
1752
			return "{$class}_{$stage}";
1753
		}
1754
		return $class;
1755
	}
1756
1757
	//-----------------------------------------------------------------------------------------------//
1758
1759
1760
	/**
1761
	 * Determine if the current user is able to set the given site stage / archive
1762
	 *
1763
	 * @param SS_HTTPRequest $request
1764
	 * @return bool
1765
	 */
1766
	public static function can_choose_site_stage($request) {
1767
		// Request is allowed if stage isn't being modified
1768
		if((!$request->getVar('stage') || $request->getVar('stage') === static::LIVE)
1769
			&& !$request->getVar('archiveDate')
1770
		) {
1771
			return true;
1772
		}
1773
1774
		// Check permissions with member ID in session.
1775
		$member = Member::currentUser();
1776
		$permissions = Config::inst()->get(get_called_class(), 'non_live_permissions');
1777
		return $member && Permission::checkMember($member, $permissions);
1778
	}
1779
1780
	/**
1781
	 * Choose the stage the site is currently on.
1782
	 *
1783
	 * If $_GET['stage'] is set, then it will use that stage, and store it in
1784
	 * the session.
1785
	 *
1786
	 * if $_GET['archiveDate'] is set, it will use that date, and store it in
1787
	 * the session.
1788
	 *
1789
	 * If neither of these are set, it checks the session, otherwise the stage
1790
	 * is set to 'Live'.
1791
	 */
1792
	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...
1793
		// Check any pre-existing session mode
1794
		$preexistingMode = Session::get('readingMode');
1795
1796
		// Determine the reading mode
1797
		if(isset($_GET['stage'])) {
1798
			$stage = ucfirst(strtolower($_GET['stage']));
1799
			if(!in_array($stage, array(static::DRAFT, static::LIVE))) {
1800
				$stage = static::LIVE;
1801
			}
1802
			$mode = 'Stage.' . $stage;
1803
		} elseif (isset($_GET['archiveDate']) && strtotime($_GET['archiveDate'])) {
1804
			$mode = 'Archive.' . $_GET['archiveDate'];
1805
		} elseif($preexistingMode) {
1806
			$mode = $preexistingMode;
1807
		} else {
1808
			$mode = static::DEFAULT_MODE;
1809
		}
1810
1811
		// Save reading mode
1812
		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...
1813
1814
		// Try not to store the mode in the session if not needed
1815
		if(($preexistingMode && $preexistingMode !== $mode)
1816
			|| (!$preexistingMode && $mode !== static::DEFAULT_MODE)
1817
		) {
1818
			Session::set('readingMode', $mode);
1819
		}
1820
1821
		if(!headers_sent() && !Director::is_cli()) {
1822
			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...
1823
				// clear the cookie if it's set
1824
				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...
1825
					Cookie::force_expiry('bypassStaticCache', null, null, false, true /* httponly */);
1826
				}
1827
			} else {
1828
				// set the cookie if it's cleared
1829
				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...
1830
					Cookie::set('bypassStaticCache', '1', 0, null, null, false, true /* httponly */);
1831
				}
1832
			}
1833
		}
1834
	}
1835
1836
	/**
1837
	 * Set the current reading mode.
1838
	 *
1839
	 * @param string $mode
1840
	 */
1841
	public static function set_reading_mode($mode) {
1842
		self::$reading_mode = $mode;
1843
	}
1844
1845
	/**
1846
	 * Get the current reading mode.
1847
	 *
1848
	 * @return string
1849
	 */
1850
	public static function get_reading_mode() {
1851
		return self::$reading_mode;
1852
	}
1853
1854
	/**
1855
	 * Get the current reading stage.
1856
	 *
1857
	 * @return string
1858
	 */
1859
	public static function get_stage() {
1860
		$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...
1861
1862
		if($parts[0] == 'Stage') {
1863
			return $parts[1];
1864
		}
1865
	}
1866
1867
	/**
1868
	 * Get the current archive date.
1869
	 *
1870
	 * @return string
1871
	 */
1872
	public static function current_archived_date() {
1873
		$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...
1874
		if($parts[0] == 'Archive') {
1875
			return $parts[1];
1876
		}
1877
	}
1878
1879
	/**
1880
	 * Set the reading stage.
1881
	 *
1882
	 * @param string $stage New reading stage.
1883
	 */
1884
	public static function set_stage($stage) {
1885
		static::set_reading_mode('Stage.' . $stage);
1886
	}
1887
1888
	/**
1889
	 * Set the reading archive date.
1890
	 *
1891
	 * @param string $date New reading archived date.
1892
	 */
1893
	public static function reading_archived_date($date) {
1894
		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...
1895
	}
1896
1897
1898
	/**
1899
	 * Get a singleton instance of a class in the given stage.
1900
	 *
1901
	 * @param string $class The name of the class.
1902
	 * @param string $stage The name of the stage.
1903
	 * @param string $filter A filter to be inserted into the WHERE clause.
1904
	 * @param boolean $cache Use caching.
1905
	 * @param string $sort A sort expression to be inserted into the ORDER BY clause.
1906
	 *
1907
	 * @return DataObject
1908
	 */
1909
	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...
1910
		// TODO: No identity cache operating
1911
		$items = static::get_by_stage($class, $stage, $filter, $sort, null, 1);
1912
1913
		return $items->First();
1914
	}
1915
1916
	/**
1917
	 * Gets the current version number of a specific record.
1918
	 *
1919
	 * @param string $class
1920
	 * @param string $stage
1921
	 * @param int $id
1922
	 * @param boolean $cache
1923
	 *
1924
	 * @return int
1925
	 */
1926
	public static function get_versionnumber_by_stage($class, $stage, $id, $cache = true) {
1927
		$baseClass = ClassInfo::baseDataClass($class);
1928
		$stageTable = ($stage == 'Stage') ? $baseClass : "{$baseClass}_{$stage}";
1929
1930
		// cached call
1931
		if($cache && isset(self::$cache_versionnumber[$baseClass][$stage][$id])) {
1932
			return self::$cache_versionnumber[$baseClass][$stage][$id];
1933
		}
1934
1935
		// get version as performance-optimized SQL query (gets called for each record in the sitetree)
1936
		$version = DB::prepared_query(
1937
			"SELECT \"Version\" FROM \"$stageTable\" WHERE \"ID\" = ?",
1938
			array($id)
1939
		)->value();
1940
1941
		// cache value (if required)
1942
		if($cache) {
1943
			if(!isset(self::$cache_versionnumber[$baseClass])) {
1944
				self::$cache_versionnumber[$baseClass] = array();
1945
			}
1946
1947
			if(!isset(self::$cache_versionnumber[$baseClass][$stage])) {
1948
				self::$cache_versionnumber[$baseClass][$stage] = array();
1949
			}
1950
1951
			self::$cache_versionnumber[$baseClass][$stage][$id] = $version;
1952
		}
1953
1954
		return $version;
1955
	}
1956
1957
	/**
1958
	 * Pre-populate the cache for Versioned::get_versionnumber_by_stage() for
1959
	 * a list of record IDs, for more efficient database querying.  If $idList
1960
	 * is null, then every record will be pre-cached.
1961
	 *
1962
	 * @param string $class
1963
	 * @param string $stage
1964
	 * @param array $idList
1965
	 */
1966
	public static function prepopulate_versionnumber_cache($class, $stage, $idList = null) {
1967
		if (!Config::inst()->get('Versioned', 'prepopulate_versionnumber_cache')) {
1968
			return;
1969
		}
1970
		$filter = "";
1971
		$parameters = array();
1972
		if($idList) {
1973
			// Validate the ID list
1974
			foreach($idList as $id) {
1975
				if(!is_numeric($id)) {
1976
					user_error("Bad ID passed to Versioned::prepopulate_versionnumber_cache() in \$idList: " . $id,
1977
					E_USER_ERROR);
1978
				}
1979
			}
1980
			$filter = 'WHERE "ID" IN ('.DB::placeholders($idList).')';
1981
			$parameters = $idList;
1982
		}
1983
1984
		$baseClass = ClassInfo::baseDataClass($class);
1985
		$stageTable = ($stage == 'Stage') ? $baseClass : "{$baseClass}_{$stage}";
1986
1987
		$versions = DB::prepared_query("SELECT \"ID\", \"Version\" FROM \"$stageTable\" $filter", $parameters)->map();
1988
1989
		foreach($versions as $id => $version) {
1990
			self::$cache_versionnumber[$baseClass][$stage][$id] = $version;
1991
		}
1992
	}
1993
1994
	/**
1995
	 * Get a set of class instances by the given stage.
1996
	 *
1997
	 * @param string $class The name of the class.
1998
	 * @param string $stage The name of the stage.
1999
	 * @param string $filter A filter to be inserted into the WHERE clause.
2000
	 * @param string $sort A sort expression to be inserted into the ORDER BY clause.
2001
	 * @param string $join Deprecated, use leftJoin($table, $joinClause) instead
2002
	 * @param int $limit A limit on the number of records returned from the database.
2003
	 * @param string $containerClass The container class for the result set (default is DataList)
2004
	 *
2005
	 * @return DataList A modified DataList designated to the specified stage
2006
	 */
2007
	public static function get_by_stage(
2008
		$class, $stage, $filter = '', $sort = '', $join = '', $limit = null, $containerClass = 'DataList'
2009
	) {
2010
		$result = DataObject::get($class, $filter, $sort, $join, $limit, $containerClass);
2011
		return $result->setDataQueryParam(array(
2012
			'Versioned.mode' => 'stage',
2013
			'Versioned.stage' => $stage
2014
		));
2015
	}
2016
2017
	/**
2018
	 * Delete this record from the given stage
2019
	 *
2020
	 * @param string $stage
2021
	 */
2022
	public function deleteFromStage($stage) {
2023
		$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...
2024
		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...
2025
		$owner = $this->owner;
2026
		$clone = clone $owner;
2027
		$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...
2028
		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...
2029
2030
		// Fix the version number cache (in case you go delete from stage and then check ExistsOnLive)
2031
		$baseClass = ClassInfo::baseDataClass($owner->class);
2032
		self::$cache_versionnumber[$baseClass][$stage][$owner->ID] = null;
2033
	}
2034
2035
	/**
2036
	 * Write the given record to the draft stage
2037
	 *
2038
	 * @param string $stage
2039
	 * @param boolean $forceInsert
2040
	 * @return int The ID of the record
2041
	 */
2042
	public function writeToStage($stage, $forceInsert = false) {
2043
		$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...
2044
		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...
2045
2046
		$owner = $this->owner;
2047
		$owner->forceChange();
2048
		$result = $owner->write(false, $forceInsert);
2049
		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...
2050
2051
		return $result;
2052
	}
2053
2054
	/**
2055
	 * Roll the draft version of this record to match the published record.
2056
	 * Caution: Doesn't overwrite the object properties with the rolled back version.
2057
	 *
2058
	 * {@see doRevertToLive()} to reollback to live
2059
	 *
2060
	 * @param int $version Version number
2061
	 */
2062
	public function doRollbackTo($version) {
2063
		$owner = $this->owner;
2064
		$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...
2065
		$owner->publish($version, "Stage", true);
2066
		$owner->writeWithoutVersion();
2067
		$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...
2068
	}
2069
2070
	public function onAfterRollback($version) {
2071
		// Find record at this version
2072
		$baseClass = ClassInfo::baseDataClass($this->owner);
2073
		$recordVersion = static::get_version($baseClass, $this->owner->ID, $version);
2074
2075
		// Note that unlike other publishing actions, rollback is NOT recursive;
2076
		// The owner collects all objects and writes them back using writeToStage();
2077
		foreach ($recordVersion->findOwned() as $object) {
2078
			/** @var Versioned|DataObject $object */
2079
			$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...
2080
		}
2081
	}
2082
2083
	/**
2084
	 * Return the latest version of the given record.
2085
	 *
2086
	 * @param string $class
2087
	 * @param int $id
2088
	 * @return DataObject
2089
	 */
2090
	public static function get_latest_version($class, $id) {
2091
		$baseClass = ClassInfo::baseDataClass($class);
2092
		$list = DataList::create($baseClass)
2093
			->setDataQueryParam("Versioned.mode", "latest_versions");
2094
2095
		return $list->byID($id);
2096
	}
2097
2098
	/**
2099
	 * Returns whether the current record is the latest one.
2100
	 *
2101
	 * @todo Performance - could do this directly via SQL.
2102
	 *
2103
	 * @see get_latest_version()
2104
	 * @see latestPublished
2105
	 *
2106
	 * @return boolean
2107
	 */
2108
	public function isLatestVersion() {
2109
		$owner = $this->owner;
2110
		if(!$owner->isInDB()) {
2111
			return false;
2112
		}
2113
2114
		$version = static::get_latest_version($owner->class, $owner->ID);
2115
		return ($version->Version == $owner->Version);
2116
	}
2117
2118
	/**
2119
	 * Check if this record exists on live
2120
	 *
2121
	 * @return bool
2122
	 */
2123
	public function isPublished() {
2124
		$owner = $this->owner;
2125
		if(!$owner->isInDB()) {
2126
			return false;
2127
		}
2128
2129
		// Non-staged objects are considered "published" if saved
2130
		if(!$this->hasStages()) {
2131
			return true;
2132
		}
2133
2134
		$baseClass = ClassInfo::baseDataClass($owner->class);
2135
		$table = $this->stageTable($baseClass, static::LIVE);
2136
		$result = DB::prepared_query(
2137
			"SELECT COUNT(*) FROM \"{$table}\" WHERE \"{$table}\".\"ID\" = ?",
2138
			array($owner->ID)
2139
		);
2140
		return (bool)$result->value();
2141
	}
2142
2143
	/**
2144
	 * Check if this record exists on the draft stage
2145
	 *
2146
	 * @return bool
2147
	 */
2148
	public function isOnDraft() {
2149
		$owner = $this->owner;
2150
		if(!$owner->isInDB()) {
2151
			return false;
2152
		}
2153
2154
		$table = ClassInfo::baseDataClass($owner->class);
2155
		$result = DB::prepared_query(
2156
			"SELECT COUNT(*) FROM \"{$table}\" WHERE \"{$table}\".\"ID\" = ?",
2157
			array($owner->ID)
2158
		);
2159
		return (bool)$result->value();
2160
	}
2161
2162
2163
2164
	/**
2165
	 * Return the equivalent of a DataList::create() call, querying the latest
2166
	 * version of each record stored in the (class)_versions tables.
2167
	 *
2168
	 * In particular, this will query deleted records as well as active ones.
2169
	 *
2170
	 * @param string $class
2171
	 * @param string $filter
2172
	 * @param string $sort
2173
	 * @return DataList
2174
	 */
2175
	public static function get_including_deleted($class, $filter = "", $sort = "") {
2176
		$list = DataList::create($class)
2177
			->where($filter)
2178
			->sort($sort)
2179
			->setDataQueryParam("Versioned.mode", "latest_versions");
2180
2181
		return $list;
2182
	}
2183
2184
	/**
2185
	 * Return the specific version of the given id.
2186
	 *
2187
	 * Caution: The record is retrieved as a DataObject, but saving back
2188
	 * modifications via write() will create a new version, rather than
2189
	 * modifying the existing one.
2190
	 *
2191
	 * @param string $class
2192
	 * @param int $id
2193
	 * @param int $version
2194
	 *
2195
	 * @return DataObject
2196
	 */
2197
	public static function get_version($class, $id, $version) {
2198
		$baseClass = ClassInfo::baseDataClass($class);
2199
		$list = DataList::create($baseClass)
2200
			->setDataQueryParam([
2201
				"Versioned.mode" => 'version',
2202
				"Versioned.version" => $version
2203
			]);
2204
2205
		return $list->byID($id);
2206
	}
2207
2208
	/**
2209
	 * Return a list of all versions for a given id.
2210
	 *
2211
	 * @param string $class
2212
	 * @param int $id
2213
	 *
2214
	 * @return DataList
2215
	 */
2216
	public static function get_all_versions($class, $id) {
2217
		$list = DataList::create($class)
2218
			->filter('ID', $id)
2219
			->setDataQueryParam('Versioned.mode', 'all_versions');
2220
2221
		return $list;
2222
	}
2223
2224
	/**
2225
	 * @param array $labels
2226
	 */
2227
	public function updateFieldLabels(&$labels) {
2228
		$labels['Versions'] = _t('Versioned.has_many_Versions', 'Versions', 'Past Versions of this record');
2229
	}
2230
2231
	/**
2232
	 * @param FieldList
2233
	 */
2234
	public function updateCMSFields(FieldList $fields) {
2235
		// remove the version field from the CMS as this should be left
2236
		// entirely up to the extension (not the cms user).
2237
		$fields->removeByName('Version');
2238
	}
2239
2240
	/**
2241
	 * Ensure version ID is reset to 0 on duplicate
2242
	 *
2243
	 * @param DataObject $source Record this was duplicated from
2244
	 * @param bool $doWrite
2245
	 */
2246
	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...
2247
		$this->owner->Version = 0;
2248
	}
2249
2250
	public function flushCache() {
2251
		self::$cache_versionnumber = array();
2252
	}
2253
2254
	/**
2255
	 * Return a piece of text to keep DataObject cache keys appropriately specific.
2256
	 *
2257
	 * @return string
2258
	 */
2259
	public function cacheKeyComponent() {
2260
		return 'versionedmode-'.static::get_reading_mode();
2261
	}
2262
2263
	/**
2264
	 * Returns an array of possible stages.
2265
	 *
2266
	 * @return array
2267
	 */
2268
	public function getVersionedStages() {
2269
		if($this->hasStages()) {
2270
			return [static::DRAFT, static::LIVE];
2271
		} else {
2272
			return [static::DRAFT];
2273
		}
2274
	}
2275
2276
	public static function get_template_global_variables() {
2277
		return array(
2278
			'CurrentReadingMode' => 'get_reading_mode'
2279
		);
2280
	}
2281
2282
	/**
2283
	 * Check if this object has stages
2284
	 *
2285
	 * @return bool True if this object is staged
2286
	 */
2287
	public function hasStages() {
2288
		return $this->mode === static::STAGEDVERSIONED;
2289
	}
2290
}
2291
2292
/**
2293
 * Represents a single version of a record.
2294
 *
2295
 * @package framework
2296
 * @subpackage model
2297
 *
2298
 * @see Versioned
2299
 */
2300
class Versioned_Version extends ViewableData {
2301
	/**
2302
	 * @var array
2303
	 */
2304
	protected $record;
2305
2306
	/**
2307
	 * @var DataObject
2308
	 */
2309
	protected $object;
2310
2311
	/**
2312
	 * Create a new version from a database row
2313
	 *
2314
	 * @param array $record
2315
	 */
2316
	public function __construct($record) {
2317
		$this->record = $record;
2318
		$record['ID'] = $record['RecordID'];
2319
		$className = $record['ClassName'];
2320
2321
		$this->object = ClassInfo::exists($className) ? new $className($record) : new DataObject($record);
2322
		$this->failover = $this->object;
2323
2324
		parent::__construct();
2325
	}
2326
2327
	/**
2328
	 * Either 'published' if published, or 'internal' if not.
2329
	 *
2330
	 * @return string
2331
	 */
2332
	public function PublishedClass() {
2333
		return $this->record['WasPublished'] ? 'published' : 'internal';
2334
	}
2335
2336
	/**
2337
	 * Author of this DataObject
2338
	 *
2339
	 * @return Member
2340
	 */
2341
	public function Author() {
2342
		return Member::get()->byId($this->record['AuthorID']);
2343
	}
2344
2345
	/**
2346
	 * Member object of the person who last published this record
2347
	 *
2348
	 * @return Member
2349
	 */
2350
	public function Publisher() {
2351
		if (!$this->record['WasPublished']) {
2352
			return null;
2353
		}
2354
2355
		return Member::get()->byId($this->record['PublisherID']);
2356
	}
2357
2358
	/**
2359
	 * True if this record is published via publish() method
2360
	 *
2361
	 * @return boolean
2362
	 */
2363
	public function Published() {
2364
		return !empty($this->record['WasPublished']);
2365
	}
2366
2367
	/**
2368
	 * Traverses to a field referenced by relationships between data objects, returning the value
2369
	 * The path to the related field is specified with dot separated syntax (eg: Parent.Child.Child.FieldName)
2370
	 *
2371
	 * @param $fieldName string
2372
	 * @return string | null - will return null on a missing value
2373
	 */
2374
	public function relField($fieldName) {
2375
		$component = $this;
2376
2377
		// We're dealing with relations here so we traverse the dot syntax
2378
		if(strpos($fieldName, '.') !== false) {
2379
			$relations = explode('.', $fieldName);
2380
			$fieldName = array_pop($relations);
2381
			foreach($relations as $relation) {
2382
				// Inspect $component for element $relation
2383
				if($component->hasMethod($relation)) {
2384
					// Check nested method
2385
						$component = $component->$relation();
2386
				} elseif($component instanceof SS_List) {
2387
					// Select adjacent relation from DataList
2388
						$component = $component->relation($relation);
2389
				} elseif($component instanceof DataObject
2390
					&& ($dbObject = $component->dbObject($relation))
2391
				) {
2392
					// Select db object
2393
					$component = $dbObject;
2394
				} else {
2395
					user_error("$relation is not a relation/field on ".get_class($component), E_USER_ERROR);
2396
				}
2397
			}
2398
		}
2399
2400
		// Bail if the component is null
2401
		if(!$component) {
2402
			return null;
2403
		}
2404
		if ($component->hasMethod($fieldName)) {
2405
			return $component->$fieldName();
2406
		}
2407
		return $component->$fieldName;
2408
	}
2409
}
2410