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 use Wikimedia\NonSerializable\NonSerializableTrait;
31 
40 class SlotRecord {
41  use NonSerializableTrait;
42 
43  public const MAIN = 'main';
44 
50  private $row;
51 
55  private $content;
56 
60  private $derived;
61 
70  public static function newWithSuppressedContent( SlotRecord $slot ) {
71  $row = $slot->row;
72 
73  return new SlotRecord( $row, static function () {
74  throw new SuppressedDataException( 'Content suppressed!' );
75  } );
76  }
77 
87  public static function newDerived( string $role, Content $content ) {
88  return self::newUnsaved( $role, $content, true );
89  }
90 
100  private static function newFromSlotRecord( SlotRecord $slot, array $overrides = [] ) {
101  $row = clone $slot->row;
102  $row->slot_id = null; // never copy the row ID!
103 
104  foreach ( $overrides as $key => $value ) {
105  $row->$key = $value;
106  }
107 
108  return new SlotRecord( $row, $slot->content, $slot->isDerived() );
109  }
110 
123  public static function newInherited( SlotRecord $slot ) {
124  // Sanity check - we can't inherit from a Slot that's not attached to a revision.
125  $slot->getRevision();
126  $slot->getOrigin();
127  $slot->getAddress();
128 
129  // NOTE: slot_origin and content_address are copied from $slot.
130  return self::newFromSlotRecord( $slot, [
131  'slot_revision_id' => null,
132  ] );
133  }
134 
149  public static function newUnsaved( $role, Content $content, bool $derived = false ) {
150  Assert::parameterType( 'string', $role, '$role' );
151 
152  $row = [
153  'slot_id' => null, // not yet known
154  'slot_revision_id' => null, // not yet known
155  'slot_origin' => null, // not yet known, will be set in newSaved()
156  'content_size' => null, // compute later
157  'content_sha1' => null, // compute later
158  'slot_content_id' => null, // not yet known, will be set in newSaved()
159  'content_address' => null, // not yet known, will be set in newSaved()
160  'role_name' => $role,
161  'model_name' => $content->getModel(),
162  ];
163 
164  return new SlotRecord( (object)$row, $content, $derived );
165  }
166 
184  public static function newSaved(
185  $revisionId,
186  $contentId,
187  $contentAddress,
188  SlotRecord $protoSlot
189  ) {
190  Assert::parameterType( 'integer', $revisionId, '$revisionId' );
191  // TODO once migration is over $contentId must be an integer
192  Assert::parameterType( 'integer|null', $contentId, '$contentId' );
193  Assert::parameterType( 'string', $contentAddress, '$contentAddress' );
194 
195  if ( $protoSlot->hasRevision() && $protoSlot->getRevision() !== $revisionId ) {
196  throw new LogicException(
197  "Mismatching revision ID $revisionId: "
198  . "The slot already belongs to revision {$protoSlot->getRevision()}. "
199  . "Use SlotRecord::newInherited() to re-use content between revisions."
200  );
201  }
202 
203  if ( $protoSlot->hasAddress() && $protoSlot->getAddress() !== $contentAddress ) {
204  throw new LogicException(
205  "Mismatching blob address $contentAddress: "
206  . "The slot already has content at {$protoSlot->getAddress()}."
207  );
208  }
209 
210  if ( $protoSlot->hasContentId() && $protoSlot->getContentId() !== $contentId ) {
211  throw new LogicException(
212  "Mismatching content ID $contentId: "
213  . "The slot already has content row {$protoSlot->getContentId()} associated."
214  );
215  }
216 
217  if ( $protoSlot->isInherited() ) {
218  if ( !$protoSlot->hasAddress() ) {
219  throw new InvalidArgumentException(
220  "An inherited blob should have a content address!"
221  );
222  }
223  if ( !$protoSlot->hasField( 'slot_origin' ) ) {
224  throw new InvalidArgumentException(
225  "A saved inherited slot should have an origin set!"
226  );
227  }
228  $origin = $protoSlot->getOrigin();
229  } else {
230  $origin = $revisionId;
231  }
232 
233  return self::newFromSlotRecord( $protoSlot, [
234  'slot_revision_id' => $revisionId,
235  'slot_content_id' => $contentId,
236  'slot_origin' => $origin,
237  'content_address' => $contentAddress,
238  ] );
239  }
240 
259  public function __construct( $row, $content, bool $derived = false ) {
260  Assert::parameterType( \stdClass::class, $row, '$row' );
261  Assert::parameterType( 'Content|callable', $content, '$content' );
262 
263  Assert::parameter(
264  property_exists( $row, 'slot_revision_id' ),
265  '$row->slot_revision_id',
266  'must exist'
267  );
268  Assert::parameter(
269  property_exists( $row, 'slot_content_id' ),
270  '$row->slot_content_id',
271  'must exist'
272  );
273  Assert::parameter(
274  property_exists( $row, 'content_address' ),
275  '$row->content_address',
276  'must exist'
277  );
278  Assert::parameter(
279  property_exists( $row, 'model_name' ),
280  '$row->model_name',
281  'must exist'
282  );
283  Assert::parameter(
284  property_exists( $row, 'slot_origin' ),
285  '$row->slot_origin',
286  'must exist'
287  );
288  Assert::parameter(
289  !property_exists( $row, 'slot_inherited' ),
290  '$row->slot_inherited',
291  'must not exist'
292  );
293  Assert::parameter(
294  !property_exists( $row, 'slot_revision' ),
295  '$row->slot_revision',
296  'must not exist'
297  );
298 
299  $this->row = $row;
300  $this->content = $content;
301  $this->derived = $derived;
302  }
303 
319  public function getContent() {
320  if ( $this->content instanceof Content ) {
321  return $this->content;
322  }
323 
324  $obj = call_user_func( $this->content, $this );
325 
326  Assert::postcondition(
327  $obj instanceof Content,
328  'Slot content callback should return a Content object'
329  );
330 
331  $this->content = $obj;
332 
333  return $this->content;
334  }
335 
346  private function getField( $name ) {
347  if ( !isset( $this->row->$name ) ) {
348  // distinguish between unknown and uninitialized fields
349  if ( property_exists( $this->row, $name ) ) {
350  throw new IncompleteRevisionException( 'Uninitialized field: ' . $name );
351  } else {
352  throw new OutOfBoundsException( 'No such field: ' . $name );
353  }
354  }
355 
356  $value = $this->row->$name;
357 
358  // NOTE: allow callbacks, but don't trust plain string callables from the database!
359  if ( !is_string( $value ) && is_callable( $value ) ) {
360  $value = call_user_func( $value, $this );
361  $this->setField( $name, $value );
362  }
363 
364  return $value;
365  }
366 
376  private function getStringField( $name ) {
377  return strval( $this->getField( $name ) );
378  }
379 
389  private function getIntField( $name ) {
390  return intval( $this->getField( $name ) );
391  }
392 
397  private function hasField( $name ) {
398  if ( isset( $this->row->$name ) ) {
399  // if the field is a callback, resolve first, then re-check
400  if ( !is_string( $this->row->$name ) && is_callable( $this->row->$name ) ) {
401  $this->getField( $name );
402  }
403  }
404 
405  return isset( $this->row->$name );
406  }
407 
413  public function getRevision() {
414  return $this->getIntField( 'slot_revision_id' );
415  }
416 
422  public function getOrigin() {
423  return $this->getIntField( 'slot_origin' );
424  }
425 
437  public function isInherited() {
438  if ( $this->hasRevision() ) {
439  return $this->getRevision() !== $this->getOrigin();
440  } else {
441  return $this->hasAddress();
442  }
443  }
444 
452  public function hasAddress() {
453  return $this->hasField( 'content_address' );
454  }
455 
463  public function hasOrigin() {
464  return $this->hasField( 'slot_origin' );
465  }
466 
486  public function hasContentId() {
487  return $this->hasField( 'slot_content_id' );
488  }
489 
497  public function hasRevision() {
498  return $this->hasField( 'slot_revision_id' );
499  }
500 
506  public function getRole() {
507  return $this->getStringField( 'role_name' );
508  }
509 
516  public function getAddress() {
517  return $this->getStringField( 'content_address' );
518  }
519 
530  public function getContentId() {
531  return $this->getIntField( 'slot_content_id' );
532  }
533 
539  public function getSize() {
540  try {
541  $size = $this->getIntField( 'content_size' );
542  } catch ( IncompleteRevisionException $ex ) {
543  $size = $this->getContent()->getSize();
544  $this->setField( 'content_size', $size );
545  }
546 
547  return $size;
548  }
549 
555  public function getSha1() {
556  try {
557  $sha1 = $this->getStringField( 'content_sha1' );
558  } catch ( IncompleteRevisionException $ex ) {
559  $sha1 = null;
560  }
561 
562  // Compute if missing. Missing could mean null or empty.
563  if ( $sha1 === null || $sha1 === '' ) {
564  $format = $this->hasField( 'format_name' )
565  ? $this->getStringField( 'format_name' )
566  : null;
567 
568  $data = $this->getContent()->serialize( $format );
569  $sha1 = self::base36Sha1( $data );
570  $this->setField( 'content_sha1', $sha1 );
571  }
572 
573  return $sha1;
574  }
575 
583  public function getModel() {
584  try {
585  $model = $this->getStringField( 'model_name' );
586  } catch ( IncompleteRevisionException $ex ) {
587  $model = $this->getContent()->getModel();
588  $this->setField( 'model_name', $model );
589  }
590 
591  return $model;
592  }
593 
603  public function getFormat() {
604  // XXX: we currently do not plan to store the format for each slot!
605 
606  if ( $this->hasField( 'format_name' ) ) {
607  return $this->getStringField( 'format_name' );
608  }
609 
610  return null;
611  }
612 
617  private function setField( $name, $value ) {
618  $this->row->$name = $value;
619  }
620 
629  public static function base36Sha1( $blob ) {
630  return \Wikimedia\base_convert( sha1( $blob ), 16, 36, 31 );
631  }
632 
652  public function hasSameContent( SlotRecord $other ) {
653  if ( $other === $this ) {
654  return true;
655  }
656 
657  if ( $this->getModel() !== $other->getModel() ) {
658  return false;
659  }
660 
661  if ( $this->hasAddress()
662  && $other->hasAddress()
663  && $this->getAddress() == $other->getAddress()
664  ) {
665  return true;
666  }
667 
668  if ( $this->getSize() !== $other->getSize() ) {
669  return false;
670  }
671 
672  if ( $this->getSha1() !== $other->getSha1() ) {
673  return false;
674  }
675 
676  return true;
677  }
678 
683  public function isDerived() : bool {
684  return $this->derived;
685  }
686 
687 }
688 
693 class_alias( SlotRecord::class, 'MediaWiki\Storage\SlotRecord' );
MediaWiki\Revision\SlotRecord\newUnsaved
static newUnsaved( $role, Content $content, bool $derived=false)
Constructs a new Slot from a Content object for a new revision.
Definition: SlotRecord.php:149
MediaWiki\Revision\SlotRecord\getFormat
getFormat()
Returns the blob serialization format as a MIME type.
Definition: SlotRecord.php:603
MediaWiki\Revision\IncompleteRevisionException
Exception throw when trying to access undefined fields on an incomplete RevisionRecord.
Definition: IncompleteRevisionException.php:32
MediaWiki\Revision\SlotRecord\getField
getField( $name)
Returns the string value of a data field from the database row supplied to the constructor.
Definition: SlotRecord.php:346
MediaWiki\Revision\SlotRecord\hasOrigin
hasOrigin()
Whether this slot has an origin (revision ID that originated the slot's content.
Definition: SlotRecord.php:463
MediaWiki\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:70
MediaWiki\Revision\SlotRecord\getIntField
getIntField( $name)
Returns the int value of a data field from the database row supplied to the constructor.
Definition: SlotRecord.php:389
MediaWiki\Revision\SuppressedDataException
Exception raised in response to an audience check when attempting to access suppressed information wi...
Definition: SuppressedDataException.php:33
MediaWiki\Revision\SlotRecord\$row
stdClass $row
database result row, as a raw object.
Definition: SlotRecord.php:50
MediaWiki\Revision\SlotRecord\getStringField
getStringField( $name)
Returns the string value of a data field from the database row supplied to the constructor.
Definition: SlotRecord.php:376
MediaWiki\Revision\SlotRecord\getSha1
getSha1()
Returns the content size.
Definition: SlotRecord.php:555
MediaWiki\Revision\SlotRecord\__construct
__construct( $row, $content, bool $derived=false)
The following fields are supported by the $row parameter:
Definition: SlotRecord.php:259
MediaWiki\Revision
Definition: ContributionsLookup.php:3
MediaWiki\Revision\SlotRecord\MAIN
const MAIN
Definition: SlotRecord.php:43
MediaWiki\Revision\SlotRecord\newFromSlotRecord
static newFromSlotRecord(SlotRecord $slot, array $overrides=[])
Constructs a new SlotRecord from an existing SlotRecord, overriding some fields.
Definition: SlotRecord.php:100
$blob
$blob
Definition: testCompression.php:70
MediaWiki\Revision\SlotRecord\setField
setField( $name, $value)
Definition: SlotRecord.php:617
MediaWiki\Revision\SlotRecord\getAddress
getAddress()
Returns the address of this slot's content.
Definition: SlotRecord.php:516
MediaWiki\Revision\SlotRecord\hasRevision
hasRevision()
Whether this slot has revision ID associated.
Definition: SlotRecord.php:497
MediaWiki\Revision\SlotRecord\hasAddress
hasAddress()
Whether this slot has an address.
Definition: SlotRecord.php:452
MediaWiki\Revision\SlotRecord\newDerived
static newDerived(string $role, Content $content)
Returns a SlotRecord for a derived slot.
Definition: SlotRecord.php:87
MediaWiki\Revision\SlotRecord\getOrigin
getOrigin()
Returns the revision ID of the revision that originated the slot's content.
Definition: SlotRecord.php:422
MediaWiki\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:184
MediaWiki\Revision\SlotRecord\isInherited
isInherited()
Whether this slot was inherited from an older revision.
Definition: SlotRecord.php:437
MediaWiki\Revision\SlotRecord\hasContentId
hasContentId()
Whether this slot has a content ID.
Definition: SlotRecord.php:486
MediaWiki\Revision\SlotRecord\hasSameContent
hasSameContent(SlotRecord $other)
Returns true if $other has the same content as this slot.
Definition: SlotRecord.php:652
MediaWiki\Revision\SlotRecord\getContentId
getContentId()
Returns the ID of the content meta data row associated with the slot.
Definition: SlotRecord.php:530
MediaWiki\Revision\SlotRecord\$derived
bool $derived
Definition: SlotRecord.php:60
Content
Base interface for content objects.
Definition: Content.php:35
MediaWiki\Revision\SlotRecord\getSize
getSize()
Returns the content size.
Definition: SlotRecord.php:539
MediaWiki\Revision\SlotRecord\isDerived
isDerived()
Definition: SlotRecord.php:683
MediaWiki\Revision\SlotRecord\base36Sha1
static base36Sha1( $blob)
Get the base 36 SHA-1 value for a string of text.
Definition: SlotRecord.php:629
Content\getModel
getModel()
Returns the ID of the content model used by this Content object.
MediaWiki\Revision\SlotRecord\getRole
getRole()
Returns the role of the slot.
Definition: SlotRecord.php:506
MediaWiki\Revision\SlotRecord\$content
Content callable $content
Definition: SlotRecord.php:55
MediaWiki\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:123
MediaWiki\Revision\SlotRecord\hasField
hasField( $name)
Definition: SlotRecord.php:397
MediaWiki\Revision\SlotRecord\getContent
getContent()
Returns the Content of the given slot.
Definition: SlotRecord.php:319
MediaWiki\Revision\SlotRecord\getModel
getModel()
Returns the content model.
Definition: SlotRecord.php:583
MediaWiki\Revision\SlotRecord
Value object representing a content slot associated with a page revision.
Definition: SlotRecord.php:40
MediaWiki\Revision\SlotRecord\getRevision
getRevision()
Returns the ID of the revision this slot is associated with.
Definition: SlotRecord.php:413