MediaWiki  master
SlotRecord.php
Go to the documentation of this file.
1 <?php
23 namespace MediaWiki\Revision;
24 
25 use Content;
27 use LogicException;
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' );
object $row
database result row, as a raw object.
Definition: SlotRecord.php:48
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
__construct( $row, $content)
The following fields are supported by the $row parameter:
Definition: SlotRecord.php:234
Exception throw when trying to access undefined fields on an incomplete RevisionRecord.
Value object representing a content slot associated with a page revision.
Definition: SlotRecord.php:39
getContent()
Returns the Content of the given slot.
Definition: SlotRecord.php:302
getModel()
Returns the content model.
Definition: SlotRecord.php:566
isInherited()
Whether this slot was inherited from an older revision.
Definition: SlotRecord.php:420
Created by PhpStorm.
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
getFormat()
Returns the blob serialization format as a MIME type.
Definition: SlotRecord.php:586
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
hasOrigin()
Whether this slot has an origin (revision ID that originated the slot&#39;s content.
Definition: SlotRecord.php:446
static newDerived(SlotRecord $slot, array $overrides=[])
Constructs a new SlotRecord from an existing SlotRecord, overriding some fields.
Definition: SlotRecord.php:80
getField( $name)
Returns the string value of a data field from the database row supplied to the constructor.
Definition: SlotRecord.php:329
getRevision()
Returns the ID of the revision this slot is associated with.
Definition: SlotRecord.php:396
Exception raised in response to an audience check when attempting to access suppressed information wi...
getStringField( $name)
Returns the string value of a data field from the database row supplied to the constructor.
Definition: SlotRecord.php:359
hasContentId()
Whether this slot has a content ID.
Definition: SlotRecord.php:469
__sleep()
Implemented to defy serialization.
Definition: SlotRecord.php:283
getOrigin()
Returns the revision ID of the revision that originated the slot&#39;s content.
Definition: SlotRecord.php:405
getRole()
Returns the role of the slot.
Definition: SlotRecord.php:489
static base36Sha1( $blob)
Get the base 36 SHA-1 value for a string of text.
Definition: SlotRecord.php:612
getSha1()
Returns the content size.
Definition: SlotRecord.php:538
hasRevision()
Whether this slot has revision ID associated.
Definition: SlotRecord.php:480
getContentId()
Returns the ID of the content meta data row associated with the slot.
Definition: SlotRecord.php:513
getSize()
Returns the content size.
Definition: SlotRecord.php:522
Content callable $content
Definition: SlotRecord.php:53
getAddress()
Returns the address of this slot&#39;s content.
Definition: SlotRecord.php:499
static newUnsaved( $role, Content $content)
Constructs a new Slot from a Content object for a new revision.
Definition: SlotRecord.php:129
hasSameContent(SlotRecord $other)
Returns true if $other has the same content as this slot.
Definition: SlotRecord.php:635
hasAddress()
Whether this slot has an address.
Definition: SlotRecord.php:435
getIntField( $name)
Returns the int value of a data field from the database row supplied to the constructor.
Definition: SlotRecord.php:372
getModel()
Returns the ID of the content model used by this Content object.
setField( $name, $value)
Definition: SlotRecord.php:600