MediaWiki REL1_32
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
236 public function __construct( $row, $content ) {
237 Assert::parameterType( 'object', $row, '$row' );
238 Assert::parameterType( 'Content|callable', $content, '$content' );
239
240 Assert::parameter(
241 property_exists( $row, 'slot_revision_id' ),
242 '$row->slot_revision_id',
243 'must exist'
244 );
245 Assert::parameter(
246 property_exists( $row, 'slot_content_id' ),
247 '$row->slot_content_id',
248 'must exist'
249 );
250 Assert::parameter(
251 property_exists( $row, 'content_address' ),
252 '$row->content_address',
253 'must exist'
254 );
255 Assert::parameter(
256 property_exists( $row, 'model_name' ),
257 '$row->model_name',
258 'must exist'
259 );
260 Assert::parameter(
261 property_exists( $row, 'slot_origin' ),
262 '$row->slot_origin',
263 'must exist'
264 );
265 Assert::parameter(
266 !property_exists( $row, 'slot_inherited' ),
267 '$row->slot_inherited',
268 'must not exist'
269 );
270 Assert::parameter(
271 !property_exists( $row, 'slot_revision' ),
272 '$row->slot_revision',
273 'must not exist'
274 );
275
276 $this->row = $row;
277 $this->content = $content;
278 }
279
285 public function __sleep() {
286 throw new LogicException( __CLASS__ . ' is not serializable.' );
287 }
288
304 public function getContent() {
305 if ( $this->content instanceof Content ) {
306 return $this->content;
307 }
308
309 $obj = call_user_func( $this->content, $this );
310
311 Assert::postcondition(
312 $obj instanceof Content,
313 'Slot content callback should return a Content object'
314 );
315
316 $this->content = $obj;
317
318 return $this->content;
319 }
320
331 private function getField( $name ) {
332 if ( !isset( $this->row->$name ) ) {
333 // distinguish between unknown and uninitialized fields
334 if ( property_exists( $this->row, $name ) ) {
335 throw new IncompleteRevisionException( 'Uninitialized field: ' . $name );
336 } else {
337 throw new OutOfBoundsException( 'No such field: ' . $name );
338 }
339 }
340
341 $value = $this->row->$name;
342
343 // NOTE: allow callbacks, but don't trust plain string callables from the database!
344 if ( !is_string( $value ) && is_callable( $value ) ) {
345 $value = call_user_func( $value, $this );
346 $this->setField( $name, $value );
347 }
348
349 return $value;
350 }
351
361 private function getStringField( $name ) {
362 return strval( $this->getField( $name ) );
363 }
364
374 private function getIntField( $name ) {
375 return intval( $this->getField( $name ) );
376 }
377
382 private function hasField( $name ) {
383 if ( isset( $this->row->$name ) ) {
384 // if the field is a callback, resolve first, then re-check
385 if ( !is_string( $this->row->$name ) && is_callable( $this->row->$name ) ) {
386 $this->getField( $name );
387 }
388 }
389
390 return isset( $this->row->$name );
391 }
392
398 public function getRevision() {
399 return $this->getIntField( 'slot_revision_id' );
400 }
401
407 public function getOrigin() {
408 return $this->getIntField( 'slot_origin' );
409 }
410
422 public function isInherited() {
423 if ( $this->hasRevision() ) {
424 return $this->getRevision() !== $this->getOrigin();
425 } else {
426 return $this->hasAddress();
427 }
428 }
429
437 public function hasAddress() {
438 return $this->hasField( 'content_address' );
439 }
440
448 public function hasOrigin() {
449 return $this->hasField( 'slot_origin' );
450 }
451
471 public function hasContentId() {
472 return $this->hasField( 'slot_content_id' );
473 }
474
482 public function hasRevision() {
483 return $this->hasField( 'slot_revision_id' );
484 }
485
491 public function getRole() {
492 return $this->getStringField( 'role_name' );
493 }
494
501 public function getAddress() {
502 return $this->getStringField( 'content_address' );
503 }
504
515 public function getContentId() {
516 return $this->getIntField( 'slot_content_id' );
517 }
518
524 public function getSize() {
525 try {
526 $size = $this->getIntField( 'content_size' );
527 } catch ( IncompleteRevisionException $ex ) {
528 $size = $this->getContent()->getSize();
529 $this->setField( 'content_size', $size );
530 }
531
532 return $size;
533 }
534
540 public function getSha1() {
541 try {
542 $sha1 = $this->getStringField( 'content_sha1' );
543 } catch ( IncompleteRevisionException $ex ) {
544 $format = $this->hasField( 'format_name' )
545 ? $this->getStringField( 'format_name' )
546 : null;
547
548 $data = $this->getContent()->serialize( $format );
549 $sha1 = self::base36Sha1( $data );
550 $this->setField( 'content_sha1', $sha1 );
551 }
552
553 return $sha1;
554 }
555
563 public function getModel() {
564 try {
565 $model = $this->getStringField( 'model_name' );
566 } catch ( IncompleteRevisionException $ex ) {
567 $model = $this->getContent()->getModel();
568 $this->setField( 'model_name', $model );
569 }
570
571 return $model;
572 }
573
583 public function getFormat() {
584 // XXX: we currently do not plan to store the format for each slot!
585
586 if ( $this->hasField( 'format_name' ) ) {
587 return $this->getStringField( 'format_name' );
588 }
589
590 return null;
591 }
592
597 private function setField( $name, $value ) {
598 $this->row->$name = $value;
599 }
600
609 public static function base36Sha1( $blob ) {
610 return \Wikimedia\base_convert( sha1( $blob ), 16, 36, 31 );
611 }
612
632 public function hasSameContent( SlotRecord $other ) {
633 if ( $other === $this ) {
634 return true;
635 }
636
637 if ( $this->getModel() !== $other->getModel() ) {
638 return false;
639 }
640
641 if ( $this->hasAddress()
642 && $other->hasAddress()
643 && $this->getAddress() == $other->getAddress()
644 ) {
645 return true;
646 }
647
648 if ( $this->getSize() !== $other->getSize() ) {
649 return false;
650 }
651
652 if ( $this->getSha1() !== $other->getSha1() ) {
653 return false;
654 }
655
656 return true;
657 }
658
659}
660
665class_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)
SlotRecord constructor.
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.
The wiki should then use memcached to cache various data To use multiple just add more items to the array To increase the weight of a make its entry a array("192.168.0.1:11211", 2))