\Drupal\strata\Restore LogicalRestore

Puts individual subjects back, with the site up.

The surgical half of rollback: one node, one config object, a set of them, without a maintenance window and without touching anything the operator did not name. A whole-site rewind is a physical restore and a different operation with different risks.

Three rules make it safe to run on a live site.

A snapshot is forced first. Before anything is written, the current state of every subject about to change is flushed and committed, so the rollback itself has a rollback. A restore that cannot take that snapshot refuses; there is no configuration to turn it off, because the one moment an operator needs an undo is right after a restore they did not mean to run.

A degraded subject is skipped, not filled. Writing a partial reconstruction over a live value produces a record that is neither the old one nor the new one. The plan lists them and the operator can opt in per scope.

A subject that fails to write does not abort the rest. Each write is independent, so a validation constraint on one node does not leave the other four hundred unrestored. The result names every failure and the outcome is FAILED even when most of it worked.

Entities are saved through the entity API rather than written to their tables, so hooks, field constraints and cache invalidation all run - and Strata's own capture hooks run too, which is exactly right: a restore is a change to the site and belongs in the history like any other.

Summary

Methods
Properties
Constants
__construct
restore
restoreAll
apply
No public properties found
EXCLUDED_USER_FIELDS
No protected methods found
No protected properties found
No protected constants found
run
attempt
write
writeConfig
writeState
writeKeyValue
writeRow
stored
writeEntity
save
snapshot
label
actor
preflight
replayer
flusher
audit
entityTypeManager
configFactory
state
keyValue
currentUser
logger
notifier
No private constants found

Constant

EXCLUDED_USER_FIELDS

EXCLUDED_USER_FIELDS = \Drupal\strata\Capture\EntityDelta::ACCESS_FIELDS

Fields never written back to a user.

Rolling a user back to an old access timestamp would make an active account look dormant, which is corruption rather than recovery, and it would erase the security trail of who signed in when. The same reasoning excluded them from capture in the first place.

Properties

$preflight

$preflight : \Drupal\strata\Restore\Preflight

Type

Preflight

$replayer

$replayer : \Drupal\strata\Restore\Replayer

Type

Replayer

$flusher

$flusher : \Drupal\strata\Flush\Flusher

Type

Flusher

$audit

$audit : \Drupal\strata\Restore\RestoreAudit

Type

RestoreAudit

$entityTypeManager

$entityTypeManager : \Drupal\Core\Entity\EntityTypeManagerInterface

Type

EntityTypeManagerInterface

$configFactory

$configFactory : \Drupal\Core\Config\ConfigFactoryInterface

Type

ConfigFactoryInterface

$state

$state : \Drupal\Core\State\StateInterface

Type

StateInterface

$keyValue

$keyValue : \Drupal\Core\KeyValueStore\KeyValueFactoryInterface

Type

KeyValueFactoryInterface

$currentUser

$currentUser : \Drupal\Core\Session\AccountProxyInterface

Type

AccountProxyInterface

$logger

$logger : \Psr\Log\LoggerInterface

Type

LoggerInterface

$notifier

$notifier : ?\Drupal\strata\Event\Notifier

Type

Notifier|null

Methods

__construct()

__construct(\Drupal\strata\Restore\Preflight  $preflight, \Drupal\strata\Restore\Replayer  $replayer, \Drupal\strata\Flush\Flusher  $flusher, \Drupal\strata\Restore\RestoreAudit  $audit, \Drupal\Core\Entity\EntityTypeManagerInterface  $entityTypeManager, \Drupal\Core\Config\ConfigFactoryInterface  $configFactory, \Drupal\Core\State\StateInterface  $state, \Drupal\Core\KeyValueStore\KeyValueFactoryInterface  $keyValue, \Drupal\Core\Session\AccountProxyInterface  $currentUser, \Psr\Log\LoggerInterface  $logger, \Drupal\strata\Event\Notifier|null  $notifier = null) : mixed

Constructs a logical restore.

Parameters

\Drupal\strata\Restore\Preflight $preflight

Plans the restore.

\Drupal\strata\Restore\Replayer $replayer

Reconstructs each subject.

\Drupal\strata\Flush\Flusher $flusher

Takes the snapshot that makes the restore undoable.

\Drupal\strata\Restore\RestoreAudit $audit

Records what was done.

\Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager

Loads and saves the entities being restored.

\Drupal\Core\Config\ConfigFactoryInterface $configFactory

Writes config objects back.

\Drupal\Core\State\StateInterface $state

Writes state values back.

\Drupal\Core\KeyValueStore\KeyValueFactoryInterface $keyValue

Writes key-value entries back.

\Drupal\Core\Session\AccountProxyInterface $currentUser

Attributes the restore to whoever ran it.

\Psr\Log\LoggerInterface $logger

Records the outcome.

\Drupal\strata\Event\Notifier|null $notifier

Announces the outcome, or NULL to announce nothing.

Returns

mixed —

restore()

restore(string  $target, list  $subjects, bool  $apply = false, bool  $fillDegraded = false) : \Drupal\strata\Restore\RestoreResult

Restores named subjects to their state at a commit.

Parameters

string $target

Commit id to restore to.

list $subjects

Subject paths, such as "entity/node:42".

bool $apply

FALSE to plan and record without writing. The plan is identical either way.

bool $fillDegraded

TRUE to write partial reconstructions as well.

Returns

\Drupal\strata\Restore\RestoreResult —

What was written, skipped, and what failed.

restoreAll()

restoreAll(string  $target, bool  $apply = false, bool  $fillDegraded = false, int|null  $limit = null) : \Drupal\strata\Restore\RestoreResult

Restores every subject a commit covers.

Parameters

string $target

Commit id to restore to.

bool $apply

FALSE to plan and record without writing.

bool $fillDegraded

TRUE to write partial reconstructions as well.

int|null $limit

Most subjects to touch, or NULL for all of them.

Returns

\Drupal\strata\Restore\RestoreResult —

What was written, skipped, and what failed.

apply()

apply(\Drupal\strata\Restore\RestorePlan  $plan, string  $scope, bool  $apply = true) : \Drupal\strata\Restore\RestoreResult

Applies a plan that has already been made.

The path a confirm form takes: the operator was shown a plan, and this applies THAT plan rather than recomputing one that may have drifted since.

Parameters

\Drupal\strata\Restore\RestorePlan $plan

The plan to apply.

string $scope

What the restore was scoped to, as the operator expressed it.

bool $apply

FALSE to record without writing.

Returns

\Drupal\strata\Restore\RestoreResult —

What was written, skipped, and what failed.

run()

run(\Drupal\strata\Restore\RestorePlan  $plan, string  $scope, bool  $apply) : \Drupal\strata\Restore\RestoreResult

Does the work for a plan.

Parameters

\Drupal\strata\Restore\RestorePlan $plan

The plan.

string $scope

What the restore was scoped to.

bool $apply

FALSE to record without writing.

Returns

\Drupal\strata\Restore\RestoreResult —

The result.

attempt()

attempt(\Drupal\strata\Restore\RestorePlan  $plan, string  $scope, bool  $apply) : \Drupal\strata\Restore\RestoreResult

The restore itself, before anything is announced.

Parameters

\Drupal\strata\Restore\RestorePlan $plan

The plan.

string $scope

What the restore was scoped to.

bool $apply

FALSE to record without writing.

Returns

\Drupal\strata\Restore\RestoreResult —

The result.

write()

write(string  $subject, \Drupal\strata\Restore\ReplayResult  $result) : string|null

Writes one subject back.

Parameters

string $subject

Subject path.

\Drupal\strata\Restore\ReplayResult $result

What the replay reconstructed.

Returns

string|null —

NULL on success, or why it did not work.

writeConfig()

writeConfig(string  $name, \Drupal\strata\Restore\ReplayResult  $result) : string|null

Writes one config object back.

Set as whole data rather than key by key, because that is how it was captured and because setting keys individually would leave any key added since the target commit in place - a restored config object that is a merge of two points in time is not either of them.

Parameters

string $name

The config object name.

\Drupal\strata\Restore\ReplayResult $result

What the replay reconstructed.

Returns

string|null —

NULL on success, or why it did not work.

writeState()

writeState(string  $key, \Drupal\strata\Restore\ReplayResult  $result) : string|null

Writes one state value back.

Parameters

string $key

The state key.

\Drupal\strata\Restore\ReplayResult $result

What the replay reconstructed.

Returns

string|null —

NULL on success, or why it did not work.

writeKeyValue()

writeKeyValue(string  $subject, \Drupal\strata\Restore\ReplayResult  $result) : string|null

Writes one key-value entry back.

Parameters

string $subject

The subject name, shaped "collection:key".

\Drupal\strata\Restore\ReplayResult $result

What the replay reconstructed.

Returns

string|null —

NULL on success, or why it did not work.

writeRow()

writeRow(string  $name, \Drupal\strata\Restore\ReplayResult  $result) : string

Declines to write a table subject.

Two shapes arrive here. A whole-table marker from the statement tap says a table changed and carries no rows, because a statement names the rows it touched only in its WHERE clause. A row captured by the reconciler does carry its contents, but writing it means an INSERT or UPDATE whose correctness depends on constraints, sequences and triggers this has no view of.

Both are refused with the reason, and both are what a physical restore exists for.

Parameters

string $name

The subject name.

\Drupal\strata\Restore\ReplayResult $result

What the replay reconstructed.

Returns

string —

Why it was not written.

stored()

stored(\Drupal\strata\Restore\ReplayResult  $result) : mixed

The value a state or key-value replay reconstructed.

These stores hold arbitrary PHP values, so capture serializes them rather than encoding them as JSON. A value that round-tripped through JSON would come back as an array and be written back as the wrong type.

Parameters

\Drupal\strata\Restore\ReplayResult $result

What the replay reconstructed.

Returns

mixed —

The value.

writeEntity()

writeEntity(string  $name, \Drupal\strata\Restore\ReplayResult  $result) : string|null

Writes one entity's fields back.

A subject the replay says did not exist at the target is NOT deleted here. Removing content because it postdates a restore point is a destructive interpretation of a rollback, and it is the one thing an operator would least expect from restoring a single node.

Parameters

string $name

The subject name, shaped "node:42".

\Drupal\strata\Restore\ReplayResult $result

What the replay reconstructed.

Returns

string|null —

NULL on success, or why it did not work.

save()

save(\Drupal\Core\Entity\FieldableEntityInterface  $entity, array  $fields) : string|null

Sets the reconstructed fields and saves.

A field the entity no longer has is reported rather than forced: a field removed since the commit was taken cannot be restored, and inventing a value for it would be worse than saying so.

Parameters

\Drupal\Core\Entity\FieldableEntityInterface $entity

The entity to write into.

array $fields

Field name keyed to its reconstructed value.

Returns

string|null —

NULL on success, or why it did not work.

snapshot()

snapshot() : string|null

Seals the site's current state so the restore can be undone.

Returns

string|null —

The snapshot commit id, or NULL when nothing could be sealed. An empty journal is not a failure: with nothing pending, the head already IS the state the restore is about to change, so that commit is the snapshot.

label()

label(\Drupal\Core\Entity\EntityInterface  $entity) : string

A label for an entity in an error message.

Parameters

\Drupal\Core\Entity\EntityInterface $entity

The entity.

Returns

string —

Its label, or its type and id when it has none.

actor()

actor() : int|null

The user the restore is attributed to.

Returns

int|null —

A Drupal user id, or NULL for an unattended run.