MediaWiki REL1_34
SlotRecord.php
Go to the documentation of this file.
1<?php
23namespace MediaWiki\Revision;
24
25use Content;
26use InvalidArgumentException;
27use LogicException;
28use OutOfBoundsException;
29use Wikimedia\Assert\Assert;
30
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
668class_alias( SlotRecord::class, 'MediaWiki\Storage\SlotRecord' );
Exception throw when trying to access undefined fields on an incomplete RevisionRecord.
Value object representing a content slot associated with a page revision.
getContent()
Returns the Content of the given slot.
hasSameContent(SlotRecord $other)
Returns true if $other has the same content as this slot.
getRole()
Returns the role of the slot.
static newInherited(SlotRecord $slot)
Constructs a new SlotRecord for a new revision, inheriting the content of the given SlotRecord of a p...
hasAddress()
Whether this slot has an address.
getSha1()
Returns the content size.
getSize()
Returns the content size.
__sleep()
Implemented to defy serialization.
getAddress()
Returns the address of this slot's content.
__construct( $row, $content)
The following fields are supported by the $row parameter:
hasOrigin()
Whether this slot has an origin (revision ID that originated the slot's content.
hasRevision()
Whether this slot has revision ID associated.
getModel()
Returns the content model.
static base36Sha1( $blob)
Get the base 36 SHA-1 value for a string of text.
isInherited()
Whether this slot was inherited from an older revision.
getOrigin()
Returns the revision ID of the revision that originated the slot's content.
static newSaved( $revisionId, $contentId, $contentAddress, SlotRecord $protoSlot)
Constructs a complete SlotRecord for a newly saved revision, based on the incomplete proto-slot.
hasContentId()
Whether this slot has a content ID.
static newWithSuppressedContent(SlotRecord $slot)
Returns a new SlotRecord just like the given $slot, except that calling getContent() will fail with a...
getContentId()
Returns the ID of the content meta data row associated with the slot.
getRevision()
Returns the ID of the revision this slot is associated with.
object $row
database result row, as a raw object.
getStringField( $name)
Returns the string value of a data field from the database row supplied to the constructor.
getField( $name)
Returns the string value of a data field from the database row supplied to the constructor.
static newUnsaved( $role, Content $content)
Constructs a new Slot from a Content object for a new revision.
getFormat()
Returns the blob serialization format as a MIME type.
static newDerived(SlotRecord $slot, array $overrides=[])
Constructs a new SlotRecord from an existing SlotRecord, overriding some fields.
getIntField( $name)
Returns the int value of a data field from the database row supplied to the constructor.
Exception raised in response to an audience check when attempting to access suppressed information wi...
Base interface for content objects.
Definition Content.php:34
getModel()
Returns the ID of the content model used by this Content object.