MediaWiki  master
SlotRecord.php
Go to the documentation of this file.
1 <?php
23 namespace MediaWiki\Revision;
24 
25 use Content;
26 use InvalidArgumentException;
27 use LogicException;
28 use OutOfBoundsException;
29 use Wikimedia\Assert\Assert;
30 
39 class SlotRecord {
40 
41  const MAIN = 'main';
42 
48  private $row;
49 
53  private $content;
54 
63  public static function newWithSuppressedContent( SlotRecord $slot ) {
64  $row = $slot->row;
65 
66  return new SlotRecord( $row, function () {
67  throw new SuppressedDataException( 'Content suppressed!' );
68  } );
69  }
70 
80  private static function newDerived( SlotRecord $slot, array $overrides = [] ) {
81  $row = clone $slot->row;
82  $row->slot_id = null; // never copy the row ID!
83 
84  foreach ( $overrides as $key => $value ) {
85  $row->$key = $value;
86  }
87 
88  return new SlotRecord( $row, $slot->content );
89  }
90 
103  public static function newInherited( SlotRecord $slot ) {
104  // Sanity check - we can't inherit from a Slot that's not attached to a revision.
105  $slot->getRevision();
106  $slot->getOrigin();
107  $slot->getAddress();
108 
109  // NOTE: slot_origin and content_address are copied from $slot.
110  return self::newDerived( $slot, [
111  'slot_revision_id' => null,
112  ] );
113  }
114 
129  public static function newUnsaved( $role, Content $content ) {
130  Assert::parameterType( 'string', $role, '$role' );
131 
132  $row = [
133  'slot_id' => null, // not yet known
134  'slot_revision_id' => null, // not yet known
135  'slot_origin' => null, // not yet known, will be set in newSaved()
136  'content_size' => null, // compute later
137  'content_sha1' => null, // compute later
138  'slot_content_id' => null, // not yet known, will be set in newSaved()
139  'content_address' => null, // not yet known, will be set in newSaved()
140  'role_name' => $role,
141  'model_name' => $content->getModel(),
142  ];
143 
144  return new SlotRecord( (object)$row, $content );
145  }
146 
164  public static function newSaved(
165  $revisionId,
166  $contentId,
167  $contentAddress,
168  SlotRecord $protoSlot
169  ) {
170  Assert::parameterType( 'integer', $revisionId, '$revisionId' );
171  // TODO once migration is over $contentId must be an integer
172  Assert::parameterType( 'integer|null', $contentId, '$contentId' );
173  Assert::parameterType( 'string', $contentAddress, '$contentAddress' );
174 
175  if ( $protoSlot->hasRevision() && $protoSlot->getRevision() !== $revisionId ) {
176  throw new LogicException(
177  "Mismatching revision ID $revisionId: "
178  . "The slot already belongs to revision {$protoSlot->getRevision()}. "
179  . "Use SlotRecord::newInherited() to re-use content between revisions."
180  );
181  }
182 
183  if ( $protoSlot->hasAddress() && $protoSlot->getAddress() !== $contentAddress ) {
184  throw new LogicException(
185  "Mismatching blob address $contentAddress: "
186  . "The slot already has content at {$protoSlot->getAddress()}."
187  );
188  }
189 
190  if ( $protoSlot->hasContentId() && $protoSlot->getContentId() !== $contentId ) {
191  throw new LogicException(
192  "Mismatching content ID $contentId: "
193  . "The slot already has content row {$protoSlot->getContentId()} associated."
194  );
195  }
196 
197  if ( $protoSlot->isInherited() ) {
198  if ( !$protoSlot->hasAddress() ) {
199  throw new InvalidArgumentException(
200  "An inherited blob should have a content address!"
201  );
202  }
203  if ( !$protoSlot->hasField( 'slot_origin' ) ) {
204  throw new InvalidArgumentException(
205  "A saved inherited slot should have an origin set!"
206  );
207  }
208  $origin = $protoSlot->getOrigin();
209  } else {
210  $origin = $revisionId;
211  }
212 
213  return self::newDerived( $protoSlot, [
214  'slot_revision_id' => $revisionId,
215  'slot_content_id' => $contentId,
216  'slot_origin' => $origin,
217  'content_address' => $contentAddress,
218  ] );
219  }
220 
234  public function __construct( $row, $content ) {
235  Assert::parameterType( 'object', $row, '$row' );
236  Assert::parameterType( 'Content|callable', $content, '$content' );
237 
238  Assert::parameter(
239  property_exists( $row, 'slot_revision_id' ),
240  '$row->slot_revision_id',
241  'must exist'
242  );
243  Assert::parameter(
244  property_exists( $row, 'slot_content_id' ),
245  '$row->slot_content_id',
246  'must exist'
247  );
248  Assert::parameter(
249  property_exists( $row, 'content_address' ),
250  '$row->content_address',
251  'must exist'
252  );
253  Assert::parameter(
254  property_exists( $row, 'model_name' ),
255  '$row->model_name',
256  'must exist'
257  );
258  Assert::parameter(
259  property_exists( $row, 'slot_origin' ),
260  '$row->slot_origin',
261  'must exist'
262  );
263  Assert::parameter(
264  !property_exists( $row, 'slot_inherited' ),
265  '$row->slot_inherited',
266  'must not exist'
267  );
268  Assert::parameter(
269  !property_exists( $row, 'slot_revision' ),
270  '$row->slot_revision',
271  'must not exist'
272  );
273 
274  $this->row = $row;
275  $this->content = $content;
276  }
277 
283  public function __sleep() {
284  throw new LogicException( __CLASS__ . ' is not serializable.' );
285  }
286 
302  public function getContent() {
303  if ( $this->content instanceof Content ) {
304  return $this->content;
305  }
306 
307  $obj = call_user_func( $this->content, $this );
308 
309  Assert::postcondition(
310  $obj instanceof Content,
311  'Slot content callback should return a Content object'
312  );
313 
314  $this->content = $obj;
315 
316  return $this->content;
317  }
318 
329  private function getField( $name ) {
330  if ( !isset( $this->row->$name ) ) {
331  // distinguish between unknown and uninitialized fields
332  if ( property_exists( $this->row, $name ) ) {
333  throw new IncompleteRevisionException( 'Uninitialized field: ' . $name );
334  } else {
335  throw new OutOfBoundsException( 'No such field: ' . $name );
336  }
337  }
338 
339  $value = $this->row->$name;
340 
341  // NOTE: allow callbacks, but don't trust plain string callables from the database!
342  if ( !is_string( $value ) && is_callable( $value ) ) {
343  $value = call_user_func( $value, $this );
344  $this->setField( $name, $value );
345  }
346 
347  return $value;
348  }
349 
359  private function getStringField( $name ) {
360  return strval( $this->getField( $name ) );
361  }
362 
372  private function getIntField( $name ) {
373  return intval( $this->getField( $name ) );
374  }
375 
380  private function hasField( $name ) {
381  if ( isset( $this->row->$name ) ) {
382  // if the field is a callback, resolve first, then re-check
383  if ( !is_string( $this->row->$name ) && is_callable( $this->row->$name ) ) {
384  $this->getField( $name );
385  }
386  }
387 
388  return isset( $this->row->$name );
389  }
390 
396  public function getRevision() {
397  return $this->getIntField( 'slot_revision_id' );
398  }
399 
405  public function getOrigin() {
406  return $this->getIntField( 'slot_origin' );
407  }
408 
420  public function isInherited() {
421  if ( $this->hasRevision() ) {
422  return $this->getRevision() !== $this->getOrigin();
423  } else {
424  return $this->hasAddress();
425  }
426  }
427 
435  public function hasAddress() {
436  return $this->hasField( 'content_address' );
437  }
438 
446  public function hasOrigin() {
447  return $this->hasField( 'slot_origin' );
448  }
449 
469  public function hasContentId() {
470  return $this->hasField( 'slot_content_id' );
471  }
472 
480  public function hasRevision() {
481  return $this->hasField( 'slot_revision_id' );
482  }
483 
489  public function getRole() {
490  return $this->getStringField( 'role_name' );
491  }
492 
499  public function getAddress() {
500  return $this->getStringField( 'content_address' );
501  }
502 
513  public function getContentId() {
514  return $this->getIntField( 'slot_content_id' );
515  }
516 
522  public function getSize() {
523  try {
524  $size = $this->getIntField( 'content_size' );
525  } catch ( IncompleteRevisionException $ex ) {
526  $size = $this->getContent()->getSize();
527  $this->setField( 'content_size', $size );
528  }
529 
530  return $size;
531  }
532 
538  public function getSha1() {
539  try {
540  $sha1 = $this->getStringField( 'content_sha1' );
541  } catch ( IncompleteRevisionException $ex ) {
542  $sha1 = null;
543  }
544 
545  // Compute if missing. Missing could mean null or empty.
546  if ( $sha1 === null || $sha1 === '' ) {
547  $format = $this->hasField( 'format_name' )
548  ? $this->getStringField( 'format_name' )
549  : null;
550 
551  $data = $this->getContent()->serialize( $format );
552  $sha1 = self::base36Sha1( $data );
553  $this->setField( 'content_sha1', $sha1 );
554  }
555 
556  return $sha1;
557  }
558 
566  public function getModel() {
567  try {
568  $model = $this->getStringField( 'model_name' );
569  } catch ( IncompleteRevisionException $ex ) {
570  $model = $this->getContent()->getModel();
571  $this->setField( 'model_name', $model );
572  }
573 
574  return $model;
575  }
576 
586  public function getFormat() {
587  // XXX: we currently do not plan to store the format for each slot!
588 
589  if ( $this->hasField( 'format_name' ) ) {
590  return $this->getStringField( 'format_name' );
591  }
592 
593  return null;
594  }
595 
600  private function setField( $name, $value ) {
601  $this->row->$name = $value;
602  }
603 
612  public static function base36Sha1( $blob ) {
613  return \Wikimedia\base_convert( sha1( $blob ), 16, 36, 31 );
614  }
615 
635  public function hasSameContent( SlotRecord $other ) {
636  if ( $other === $this ) {
637  return true;
638  }
639 
640  if ( $this->getModel() !== $other->getModel() ) {
641  return false;
642  }
643 
644  if ( $this->hasAddress()
645  && $other->hasAddress()
646  && $this->getAddress() == $other->getAddress()
647  ) {
648  return true;
649  }
650 
651  if ( $this->getSize() !== $other->getSize() ) {
652  return false;
653  }
654 
655  if ( $this->getSha1() !== $other->getSha1() ) {
656  return false;
657  }
658 
659  return true;
660  }
661 
662 }
663 
668 class_alias( SlotRecord::class, 'MediaWiki\Storage\SlotRecord' );
Revision\SlotRecord\hasField
hasField( $name)
Definition: SlotRecord.php:380
Revision\IncompleteRevisionException
Exception throw when trying to access undefined fields on an incomplete RevisionRecord.
Definition: IncompleteRevisionException.php:31
Revision\SlotRecord\getContent
getContent()
Returns the Content of the given slot.
Definition: SlotRecord.php:302
Revision\SlotRecord\hasAddress
hasAddress()
Whether this slot has an address.
Definition: SlotRecord.php:435
Revision\SlotRecord\newInherited
static newInherited(SlotRecord $slot)
Constructs a new SlotRecord for a new revision, inheriting the content of the given SlotRecord of a p...
Definition: SlotRecord.php:103
Revision\SuppressedDataException
Exception raised in response to an audience check when attempting to access suppressed information wi...
Definition: SuppressedDataException.php:32
Revision\SlotRecord\getFormat
getFormat()
Returns the blob serialization format as a MIME type.
Definition: SlotRecord.php:586
Revision\SlotRecord\getIntField
getIntField( $name)
Returns the int value of a data field from the database row supplied to the constructor.
Definition: SlotRecord.php:372
Revision\SlotRecord\hasOrigin
hasOrigin()
Whether this slot has an origin (revision ID that originated the slot's content.
Definition: SlotRecord.php:446
Revision\SlotRecord\isInherited
isInherited()
Whether this slot was inherited from an older revision.
Definition: SlotRecord.php:420
Revision\SlotRecord\getRevision
getRevision()
Returns the ID of the revision this slot is associated with.
Definition: SlotRecord.php:396
Revision\SlotRecord\newDerived
static newDerived(SlotRecord $slot, array $overrides=[])
Constructs a new SlotRecord from an existing SlotRecord, overriding some fields.
Definition: SlotRecord.php:80
MediaWiki\Revision
Created by PhpStorm.
Definition: FallbackSlotRoleHandler.php:23
Revision\SlotRecord\getOrigin
getOrigin()
Returns the revision ID of the revision that originated the slot's content.
Definition: SlotRecord.php:405
$blob
$blob
Definition: testCompression.php:67
Revision\SlotRecord\__construct
__construct( $row, $content)
The following fields are supported by the $row parameter:
Definition: SlotRecord.php:234
Revision\SlotRecord\getRole
getRole()
Returns the role of the slot.
Definition: SlotRecord.php:489
Revision\SlotRecord\hasContentId
hasContentId()
Whether this slot has a content ID.
Definition: SlotRecord.php:469
Revision\SlotRecord\getModel
getModel()
Returns the content model.
Definition: SlotRecord.php:566
Revision\SlotRecord\getAddress
getAddress()
Returns the address of this slot's content.
Definition: SlotRecord.php:499
Revision\SlotRecord\base36Sha1
static base36Sha1( $blob)
Get the base 36 SHA-1 value for a string of text.
Definition: SlotRecord.php:612
Revision\SlotRecord\newWithSuppressedContent
static newWithSuppressedContent(SlotRecord $slot)
Returns a new SlotRecord just like the given $slot, except that calling getContent() will fail with a...
Definition: SlotRecord.php:63
Revision\SlotRecord\getSha1
getSha1()
Returns the content size.
Definition: SlotRecord.php:538
Revision\SlotRecord\newUnsaved
static newUnsaved( $role, Content $content)
Constructs a new Slot from a Content object for a new revision.
Definition: SlotRecord.php:129
Revision\SlotRecord\hasRevision
hasRevision()
Whether this slot has revision ID associated.
Definition: SlotRecord.php:480
Revision\SlotRecord\getSize
getSize()
Returns the content size.
Definition: SlotRecord.php:522
Revision\SlotRecord\getField
getField( $name)
Returns the string value of a data field from the database row supplied to the constructor.
Definition: SlotRecord.php:329
Revision\SlotRecord\MAIN
const MAIN
Definition: SlotRecord.php:41
Revision\SlotRecord\getStringField
getStringField( $name)
Returns the string value of a data field from the database row supplied to the constructor.
Definition: SlotRecord.php:359
Content
Base interface for content objects.
Definition: Content.php:34
Revision\SlotRecord\setField
setField( $name, $value)
Definition: SlotRecord.php:600
Revision\SlotRecord\__sleep
__sleep()
Implemented to defy serialization.
Definition: SlotRecord.php:283
Revision\SlotRecord\$row
object $row
database result row, as a raw object.
Definition: SlotRecord.php:48
Revision\SlotRecord\newSaved
static newSaved( $revisionId, $contentId, $contentAddress, SlotRecord $protoSlot)
Constructs a complete SlotRecord for a newly saved revision, based on the incomplete proto-slot.
Definition: SlotRecord.php:164
Content\getModel
getModel()
Returns the ID of the content model used by this Content object.
Revision\SlotRecord\getContentId
getContentId()
Returns the ID of the content meta data row associated with the slot.
Definition: SlotRecord.php:513
Revision\SlotRecord\$content
Content callable $content
Definition: SlotRecord.php:53
Revision\SlotRecord
Value object representing a content slot associated with a page revision.
Definition: SlotRecord.php:39
Revision\SlotRecord\hasSameContent
hasSameContent(SlotRecord $other)
Returns true if $other has the same content as this slot.
Definition: SlotRecord.php:635