Make Content JsonCodecable

By default this uses the existing ContentHandler::serializeContent() and
::unserializeContent() methods.  But in cases where existing PHP
serialization preserved fields that ::serializeContent() did not,
provide an additional ContentHandler::serializeContentToJsonArray()
and ContentHandler::deserializeContentFromJsonArray() methods which
can be used.  Use these in WikitextContentHandler to preserve the
PST flags.

Added test cases and a ContentSerializationTestTrait to make it
easy to ensure forward- and backward-compatibility in accord with

https://www.mediawiki.org/wiki/Manual:Parser_cache/Serialization_compatibility

The new JsonCodecable codec will be used to improve PageEditStashContent
serialization, which no longer has to PHP-serialize its Content object.
New test case added demonstrating compatibility.

Bug: T264389
Bug: T161647
Change-Id: I544625136088164561b9169a63aed7450cce82f5
(cherry picked from commit 21576d6c1893079777a1a51d0f81c4941c58e376)
This commit is contained in:
C. Scott Ananian 2025-07-01 15:06:59 -04:00
parent 71fea78777
commit df9756b9a1
21 changed files with 441 additions and 2 deletions

View file

@ -1142,6 +1142,7 @@ $wgAutoloadLocalClasses = [
'MediaWiki\\Content\\Content' => __DIR__ . '/includes/content/Content.php',
'MediaWiki\\Content\\ContentHandler' => __DIR__ . '/includes/content/ContentHandler.php',
'MediaWiki\\Content\\ContentHandlerFactory' => __DIR__ . '/includes/content/ContentHandlerFactory.php',
'MediaWiki\\Content\\ContentJsonCodec' => __DIR__ . '/includes/content/ContentJsonCodec.php',
'MediaWiki\\Content\\ContentModelChange' => __DIR__ . '/includes/content/ContentModelChange.php',
'MediaWiki\\Content\\CssContent' => __DIR__ . '/includes/content/CssContent.php',
'MediaWiki\\Content\\CssContentHandler' => __DIR__ . '/includes/content/CssContentHandler.php',

View file

@ -62,6 +62,7 @@ use MediaWiki\Config\Config;
use MediaWiki\Config\ConfigFactory;
use MediaWiki\Config\ConfigRepository;
use MediaWiki\Config\GlobalVarConfig;
use MediaWiki\Content\ContentJsonCodec;
use MediaWiki\Content\IContentHandlerFactory;
use MediaWiki\Content\Renderer\ContentRenderer;
use MediaWiki\Content\Transform\ContentTransformer;
@ -966,6 +967,13 @@ class MediaWikiServices extends ServiceContainer {
return $this->getService( 'ContentHandlerFactory' );
}
/**
* @since 1.45
*/
public function getContentJsonCodec(): ContentJsonCodec {
return $this->getService( 'ContentJsonCodec' );
}
/**
* @since 1.32
*/

View file

@ -83,6 +83,7 @@ use MediaWiki\Config\ConfigFactory;
use MediaWiki\Config\ConfigRepository;
use MediaWiki\Config\ServiceOptions;
use MediaWiki\Content\ContentHandlerFactory;
use MediaWiki\Content\ContentJsonCodec;
use MediaWiki\Content\IContentHandlerFactory;
use MediaWiki\Content\Renderer\ContentRenderer;
use MediaWiki\Content\Transform\ContentTransformer;
@ -630,6 +631,12 @@ return [
);
},
'ContentJsonCodec' => static function ( MediaWikiServices $services ): ContentJsonCodec {
return new ContentJsonCodec(
$services->getContentHandlerFactory(),
);
},
'ContentLanguage' => static function ( MediaWikiServices $services ): Language {
return $services->getLanguageFactory()->getLanguage(
$services->getMainConfig()->get( MainConfigNames::LanguageCode ) );

View file

@ -34,6 +34,10 @@ use MediaWiki\MediaWikiServices;
use MediaWiki\Parser\MagicWord;
use MediaWiki\Title\Title;
use MWException;
use Psr\Container\ContainerInterface;
use Wikimedia\JsonCodec\JsonClassCodec;
use Wikimedia\JsonCodec\JsonCodecable;
use Wikimedia\JsonCodec\JsonCodecInterface;
/**
* Base implementation for content objects.
@ -42,7 +46,7 @@ use MWException;
*
* @ingroup Content
*/
abstract class AbstractContent implements Content {
abstract class AbstractContent implements Content, JsonCodecable {
/**
* Name of the content model this Content object represents.
* Use with CONTENT_MODEL_XXX constants
@ -419,6 +423,12 @@ abstract class AbstractContent implements Content {
return $result;
}
public static function jsonClassCodec(
JsonCodecInterface $codec,
ContainerInterface $serviceContainer
): JsonClassCodec {
return $serviceContainer->get( 'ContentJsonCodec' );
}
}
/** @deprecated class alias since 1.43 */

View file

@ -32,6 +32,7 @@ use Action;
use DifferenceEngine;
use DifferenceEngineSlotDiffRenderer;
use InvalidArgumentException;
use JsonException;
use LogicException;
use MediaWiki\CommentStore\CommentStore;
use MediaWiki\Content\Renderer\ContentParseParams;
@ -354,6 +355,25 @@ abstract class ContentHandler {
*/
abstract public function serializeContent( Content $content, $format = null );
/**
* Serializes a Content object of the type supported by this
* ContentHandler to an array which is JsonCodecable.
*
* @since 1.45
*
* @param Content $content The Content object to serialize
*
* @return array An array of JsonCodecable content
*/
public function serializeContentToJsonArray( Content $content ): array {
$format = $this->getJsonFormat();
$blob = $this->serializeContent( $content, $format );
return [
'format' => $format,
'blob' => $blob,
];
}
/**
* Applies transformations on export (returns the blob unchanged by default).
* Subclasses may override this to perform transformations such as conversion
@ -381,9 +401,30 @@ abstract class ContentHandler {
*
* @return Content The Content object created by deserializing $blob
* @throws MWContentSerializationException
* @see ContentJsonCodec
*/
abstract public function unserializeContent( $blob, $format = null );
/**
* Deserializes a Content object of the type supported by this
* ContentHandler from a JsonCodecable array.
*
* @since 1.45
*
* @param array $json Serialized form of the content
*
* @return Content The Content object created by deserializing $blob
* @throws JsonException
* @see ContentJsonCodec
*/
public function deserializeContentFromJsonArray( array $json ): Content {
try {
return $this->unserializeContent( $json['blob'], $json['format'] );
} catch ( MWContentSerializationException $e ) {
throw new JsonException( $e->getMessage() );
}
}
/**
* Apply import transformation (by default, returns $blob unchanged).
* This gives subclasses an opportunity to transform data blobs on import.
@ -491,6 +532,16 @@ abstract class ContentHandler {
return $this->mSupportedFormats[0];
}
/**
* Allow ContentHandler to chose a non-default format for JSON
* serialization.
*
* In most cases will return the same as `::getDefaultFormat()`.
*/
public function getJsonFormat(): string {
return $this->getDefaultFormat();
}
/**
* Returns true if $format is a serialization format supported by this
* ContentHandler, and false otherwise.

View file

@ -0,0 +1,69 @@
<?php
declare( strict_types = 1 );
/**
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
*/
namespace MediaWiki\Content;
use Wikimedia\JsonCodec\JsonClassCodec;
/**
* ContentJsonCodec handles serialization of Content objects to/from
* JSON using methods of the appropriate ContentHandler.
*
* @template-implements JsonClassCodec<Content>
* @internal
*/
class ContentJsonCodec implements JsonClassCodec {
public function __construct(
private IContentHandlerFactory $contentHandlerFactory
) {
}
/** @inheritDoc */
public function toJsonArray( $content ): array {
'@phan-var Content $content'; /** @var Content $content */
// To serialize content we need a handler.
$model = $content->getModel();
$handler = $this->contentHandlerFactory->getContentHandler(
$model
);
return [ 'model' => $model ] +
$handler->serializeContentToJsonArray( $content );
}
/** @inheritDoc */
public function newFromJsonArray( string $className, array $json ) {
// To deserialize content we need a handler.
$model = $json['model'];
$handler = $this->contentHandlerFactory->getContentHandler(
$model
);
$content = $handler->deserializeContentFromJsonArray( $json );
// phan's support for generics is broken :(
// @phan-suppress-next-line PhanTypeMismatchReturn
return $content;
}
/** @inheritDoc */
public function jsonClassHintFor( string $className, string $keyName ) {
return null;
}
}

View file

@ -424,6 +424,20 @@ class WikitextContentHandler extends TextContentHandler {
$parserOutput->setOutputFlag( ParserOutputFlags::USER_SIGNATURE );
}
}
public function serializeContentToJsonArray( Content $content ): array {
'@phan-var WikitextContent $content'; /** @var WikitextContent $content */
return parent::serializeContentToJsonArray( $content ) + [
'pstFlags' => $content->getPreSaveTransformFlags(),
];
}
public function deserializeContentFromJsonArray( array $json ): WikitextContent {
$content = parent::deserializeContentFromJsonArray( $json );
'@phan-var WikitextContent $content'; /** @var WikitextContent $content */
$content->setPreSaveTransformFlags( $json['pstFlags'] ?? [] );
return $content;
}
}
/** @deprecated class alias since 1.43 */

View file

@ -120,6 +120,7 @@ $wgAutoloadClasses += [
'LoggedServiceOptions' => "$testDir/phpunit/includes/config/LoggedServiceOptions.php",
# tests/phpunit/includes/content
'MediaWiki\\Tests\\Content\\CssContentTest' => "$testDir/phpunit/includes/content/CssContentTest.php",
'DummyContentHandlerForTesting' =>
"$testDir/phpunit/mocks/content/DummyContentHandlerForTesting.php",
'DummyContentForTesting' => "$testDir/phpunit/mocks/content/DummyContentForTesting.php",
@ -127,10 +128,13 @@ $wgAutoloadClasses += [
'DummyNonTextContent' => "$testDir/phpunit/mocks/content/DummyNonTextContent.php",
'DummySerializeErrorContentHandler' =>
"$testDir/phpunit/mocks/content/DummySerializeErrorContentHandler.php",
'MediaWiki\\Tests\\Content\\JavaScriptContentTest' => "$testDir/phpunit/includes/content/JavaScriptContentTest.php",
'TextContentTest' => "$testDir/phpunit/includes/content/TextContentTest.php",
'MediaWiki\\Tests\\Content\\TextContentTest' => "$testDir/phpunit/includes/content/TextContentTest.php",
'TextContentHandlerIntegrationTest' => "$testDir/phpunit/includes/content/TextContentHandlerIntegrationTest.php",
'WikitextContentTest' => "$testDir/phpunit/includes/content/WikitextContentTest.php",
'MediaWiki\\Tests\\Content\\WikitextContentTest' => "$testDir/phpunit/includes/content/WikitextContentTest.php",
'JavaScriptContentHandlerTest' => "$testDir/phpunit/includes/content/JavaScriptContentHandlerTest.php",
'MediaWiki\\Tests\\Content\\ContentSerializationTestTrait' => "$testDir/phpunit/includes/content/ContentSerializationTestTrait.php",
# tests/phpunit/includes/db
'DatabaseTestHelper' => "$testDir/phpunit/includes/db/DatabaseTestHelper.php",

View file

@ -0,0 +1 @@
{"model":"css","format":"text/css","blob":"/* hello */","_type_":"MediaWiki\\Content\\CssContent","_complex_":true}

View file

@ -0,0 +1 @@
{"model":"css","format":"text/css","blob":"/* #REDIRECT */@import url(//example.org/w/index.php?title=MediaWiki:MonoBook.css&action=raw&ctype=text/css);","_type_":"MediaWiki\\Content\\CssContent","_complex_":true}

View file

@ -0,0 +1 @@
{"model":"javascript","format":"text/javascript","blob":"/* hello */","_type_":"MediaWiki\\Content\\JavaScriptContent","_complex_":true}

View file

@ -0,0 +1 @@
{"model":"javascript","format":"text/javascript","blob":"/* #REDIRECT */mw.loader.load(\"//example.org/w/index.php?title=MediaWiki:MonoBook.js&action=raw&ctype=text/javascript\");","_type_":"MediaWiki\\Content\\JavaScriptContent","_complex_":true}

View file

@ -0,0 +1 @@
{"model":"wikitext","format":"text/x-wiki","blob":"hello","pstFlags":[],"_type_":"MediaWiki\\Content\\WikitextContent","_complex_":true}

View file

@ -0,0 +1 @@
{"model":"wikitext","format":"text/x-wiki","blob":"with PST flags","pstFlags":["show-toc","vary-revision"],"_type_":"MediaWiki\\Content\\WikitextContent","_complex_":true}

View file

@ -0,0 +1 @@
{"pstContent":{"model":"wikitext","format":"text/x-wiki","blob":"hello","_type_":"MediaWiki\\Content\\WikitextContent","_complex_":true},"output":{"Text":null,"LanguageLinks":[],"Categories":[],"Indicators":[],"TitleText":"","Links":[],"LinksSpecial":[],"Templates":[],"TemplateIds":[],"Images":[],"FileSearchOptions":[],"ExternalLinks":[],"InterwikiLinks":[],"ExistenceLinks":[],"NewSection":false,"HideNewSection":false,"NoGallery":false,"HeadItems":[],"Modules":[],"ModuleStyles":[],"JsConfigVars":[],"Warnings":[],"WarningMsgs":[],"Sections":[],"Properties":[],"Timestamp":null,"EnableOOUI":false,"IndexPolicy":"","ExtensionData":{"test1":"test2"},"LimitReportData":[],"LimitReportJSData":[],"CacheMessage":"","TimeProfile":[],"ParseStartTime":[],"PreventClickjacking":false,"ExtraScriptSrcs":[],"ExtraDefaultSrcs":[],"ExtraStyleSrcs":[],"Flags":[],"SpeculativeRevId":null,"SpeculativePageIdUsed":null,"RevisionTimestampUsed":null,"RevisionUsedSha1Base36":null,"WrapperDivClasses":[],"ParseUsedOptions":[],"CacheExpiry":null,"CacheTime":"20250701173710","CacheRevisionId":null,"_type_":"MediaWiki\\Parser\\ParserOutput","_complex_":true},"timestamp":"20250701173710","edits":42,"_type_":"MediaWiki\\Storage\\PageEditStashContents","_complex_":true}

View file

@ -0,0 +1,28 @@
<?php
declare( strict_types = 1 );
namespace MediaWiki\Tests\Content;
use MediaWiki\MediaWikiServices;
use Wikimedia\Tests\SerializationTestTrait;
trait ContentSerializationTestTrait {
use SerializationTestTrait;
public static function getSerializedDataPath(): string {
return __DIR__ . '/../../data/Content';
}
public static function getSupportedSerializationFormats(): array {
$jsonCodec = MediaWikiServices::getInstance()->getJsonCodec();
return [ [
'ext' => 'json',
'serializer' => static function ( $obj ) use ( $jsonCodec ) {
return $jsonCodec->serialize( $obj );
},
'deserializer' => static function ( $data ) use ( $jsonCodec ) {
return $jsonCodec->deserialize( $data );
},
] ];
}
}

View file

@ -1,4 +1,7 @@
<?php
declare( strict_types = 1 );
namespace MediaWiki\Tests\Content;
use MediaWiki\Content\Content;
use MediaWiki\Content\CssContent;
@ -12,6 +15,7 @@ use MediaWiki\MainConfigNames;
* @covers \MediaWiki\Content\CssContent
*/
class CssContentTest extends TextContentTest {
use ContentSerializationTestTrait;
protected function setUp(): void {
parent::setUp();
@ -115,4 +119,36 @@ class CssContentTest extends TextContentTest {
public function testEquals( Content $a, ?Content $b = null, $equal = false ) {
$this->assertEquals( $equal, $a->equals( $b ) );
}
public static function getClassToTest(): string {
return CssContent::class;
}
public static function getTestInstancesAndAssertions(): array {
$redirects = self::provideGetRedirectTarget();
[ $redirectTitle, $redirectBlob ] = $redirects[0];
return [
'basic' => [
'instance' => new CssContent( '/* hello */' ),
'assertions' => static function ( $testCase, $obj ) {
$testCase->assertInstanceof( CssContent::class, $obj );
$testCase->assertSame( '/* hello */', $obj->getText() );
$testCase->assertNull( $obj->getRedirectTarget() );
},
],
'redirect' => [
'instance' => new CssContent( $redirectBlob ),
'assertions' => static function ( $testCase, $obj ) use ( $redirectTitle, $redirectBlob ) {
$testCase->overrideConfigValues( [
MainConfigNames::Server => '//example.org',
MainConfigNames::ScriptPath => '/w',
MainConfigNames::Script => '/w/index.php',
] );
$testCase->assertInstanceof( CssContent::class, $obj );
$testCase->assertSame( $redirectBlob, $obj->getText() );
$testCase->assertSame( $redirectTitle, $obj->getRedirectTarget()->getPrefixedText() );
},
],
];
}
}

View file

@ -1,5 +1,9 @@
<?php
declare( strict_types = 1 );
namespace MediaWiki\Tests\Content;
use JavaScriptContentHandlerTest;
use MediaWiki\Content\CssContent;
use MediaWiki\Content\JavaScriptContent;
use MediaWiki\MainConfigNames;
@ -13,6 +17,7 @@ use MediaWiki\Title\Title;
* @covers \MediaWiki\Content\JavaScriptContent
*/
class JavaScriptContentTest extends TextContentTest {
use ContentSerializationTestTrait;
public function newContent( $text ) {
return new JavaScriptContent( $text );
@ -213,4 +218,37 @@ class JavaScriptContentTest extends TextContentTest {
'/* #REDIRECT */mw.loader.load("//example.org/w/index.php?title=MediaWiki:MonoBook.js\u0026action=raw\u0026ctype=text/javascript");'
];
}
public static function getClassToTest(): string {
return JavaScriptContent::class;
}
public static function getTestInstancesAndAssertions(): array {
$redirects = JavaScriptContentHandlerTest::provideMakeRedirectContent();
[ $redirectTitle, $redirectBlob ] = $redirects['MediaWiki namespace page'];
return [
'basic' => [
'instance' => new JavaScriptContent( '/* hello */' ),
'assertions' => static function ( $testCase, $obj ) {
$testCase->assertInstanceof( JavaScriptContent::class, $obj );
$testCase->assertSame( '/* hello */', $obj->getText() );
$testCase->assertNull( $obj->getRedirectTarget() );
},
],
'redirect' => [
'instance' => new JavaScriptContent( $redirectBlob ),
'assertions' => static function ( $testCase, $obj ) use ( $redirectTitle, $redirectBlob ) {
$testCase->overrideConfigValues( [
MainConfigNames::Server => '//example.org',
MainConfigNames::ScriptPath => '/w',
MainConfigNames::Script => '/w/index.php',
MainConfigNames::ResourceBasePath => '/w',
] );
$testCase->assertInstanceof( JavaScriptContent::class, $obj );
$testCase->assertSame( $redirectBlob, $obj->getText() );
$testCase->assertSame( $redirectTitle, $obj->getRedirectTarget()->getPrefixedText() );
},
],
];
}
}

View file

@ -1,4 +1,7 @@
<?php
declare( strict_types = 1 );
namespace MediaWiki\Tests\Content;
use MediaWiki\Content\Content;
use MediaWiki\Content\JavaScriptContent;
@ -8,6 +11,7 @@ use MediaWiki\Context\RequestContext;
use MediaWiki\MainConfigNames;
use MediaWiki\Title\Title;
use MediaWiki\User\User;
use MediaWikiLangTestCase;
/**
* Needs database to do link updates.
@ -290,3 +294,4 @@ class TextContentTest extends MediaWikiLangTestCase {
}
}
class_alias( TextContentTest::class, 'TextContentTest' );

View file

@ -1,11 +1,14 @@
<?php
namespace MediaWiki\Tests\Content;
use MediaWiki\Content\JavaScriptContent;
use MediaWiki\Content\TextContent;
use MediaWiki\Content\WikitextContent;
use MediaWiki\Deferred\LinksUpdate\LinksDeletionUpdate;
use MediaWiki\Parser\Parser;
use MediaWiki\Parser\ParserOptions;
use MediaWiki\Parser\ParserOutputFlags;
use MediaWiki\Title\Title;
/**
@ -16,6 +19,8 @@ use MediaWiki\Title\Title;
* @covers \MediaWiki\Content\WikitextContent
*/
class WikitextContentTest extends TextContentTest {
use ContentSerializationTestTrait;
public const SECTIONS = "Intro
== stuff ==
@ -348,4 +353,38 @@ just a test"
// @todo more...?
];
}
public static function getClassToTest(): string {
return WikitextContent::class;
}
public static function getTestInstancesAndAssertions(): array {
// Note that WikitextContent::{get,set}PreSaveTransformFlags()
// is preserved over JSON serialization.
$pstFlags = [
ParserOutputFlags::SHOW_TOC,
ParserOutputFlags::VARY_REVISION,
];
$withPstFlags = new WikitextContent( 'with PST flags' );
$withPstFlags->setPreSaveTransformFlags( $pstFlags );
return [
'basic' => [
'instance' => new WikitextContent( 'hello' ),
'assertions' => static function ( $testCase, $obj ) {
$testCase->assertInstanceof( WikitextContent::class, $obj );
$testCase->assertSame( 'hello', $obj->getText() );
$testCase->assertArrayEquals( [], $obj->getPreSaveTransformFlags() );
},
],
'withPstFlags' => [
'instance' => $withPstFlags,
'assertions' => static function ( $testCase, $obj ) use ( $pstFlags ) {
$testCase->assertInstanceof( WikitextContent::class, $obj );
$testCase->assertSame( 'with PST flags', $obj->getText() );
$testCase->assertArrayEquals( $pstFlags, $obj->getPreSaveTransformFlags() );
},
],
];
}
}

View file

@ -0,0 +1,122 @@
<?php
namespace MediaWiki\Tests\Content;
use MediaWiki\Logger\ConsoleLogger;
use MediaWiki\Maintenance\Maintenance;
use Wikimedia\Tests\SerializationTestUtils;
define( 'MW_AUTOLOAD_TEST_CLASSES', true );
define( 'MW_PHPUNIT_TEST', true );
require_once __DIR__ . '/../../../../maintenance/Maintenance.php';
// phpcs:disable MediaWiki.Files.ClassMatchesFilename.WrongCase
class ValidateContentTestData extends Maintenance {
public function __construct() {
parent::__construct();
$this->addArg(
'path',
'Path of serialization files.',
false
);
$this->addOption( 'create', 'Create missing serialization' );
$this->addOption( 'update', 'Update mismatching serialization files' );
$this->addOption( 'version', 'Specify version for which to check serialization. '
. 'Also determines which files may be created or updated if '
. 'the respective options are set.'
. 'Unserialization is always checked against all versions. ', false, true );
}
public function execute() {
$tests = [
WikitextContentTest::class,
JavaScriptContentTest::class,
CssContentTest::class,
];
foreach ( $tests as $testClass ) {
$objClass = $testClass::getClassToTest();
$this->validateSerialization(
$objClass,
$testClass::getSerializedDataPath(),
$testClass::getSupportedSerializationFormats(),
array_map( static function ( $testCase ) {
return $testCase['instance'];
}, $testClass::getTestInstancesAndAssertions() )
);
}
}
/**
* Ensures that objects will serialize into the form expected for the given version.
* If the respective options are set in the constructor, this will create missing files or
* update mismatching files.
*
* @param string $className
* @param string $defaultDirectory
* @param array $supportedFormats
* @param array $testInstances
*/
public function validateSerialization(
string $className,
string $defaultDirectory,
array $supportedFormats,
array $testInstances
) {
$ok = true;
foreach ( $supportedFormats as $serializationFormat ) {
$serializationUtils = new SerializationTestUtils(
$this->getArg( 1 ) ?: $defaultDirectory,
$testInstances,
$serializationFormat['ext'],
$serializationFormat['serializer'],
$serializationFormat['deserializer']
);
$serializationUtils->setLogger( new ConsoleLogger( 'validator' ) );
foreach ( $serializationUtils->getSerializedInstances() as $testCaseName => $currentSerialized ) {
$expected = $serializationUtils
->getStoredSerializedInstance( $className, $testCaseName, $this->getOption( 'version' ) );
$ok = $this->validateSerializationData( $currentSerialized, $expected ) && $ok;
}
}
if ( !$ok ) {
$this->output( "\n\n" );
$this->fatalError( "Serialization data mismatch! "
. "If this was expected, rerun the script with the --update option "
. "to update the expected serialization. WARNING: make sure "
. "a forward compatible version of the code is live before deploying a "
. "serialization change!\n"
);
}
}
private function validateSerializationData( string $data, \stdClass $fileInfo ): bool {
if ( !$fileInfo->data ) {
if ( $this->hasOption( 'create' ) ) {
$this->output( 'Creating file: ' . $fileInfo->path . "\n" );
file_put_contents( $fileInfo->path, $data );
} else {
$this->fatalError( "File not found: {$fileInfo->path}. "
. "Rerun the script with the --create option set to create it."
);
}
} else {
if ( $data !== $fileInfo->data ) {
if ( $this->hasOption( 'update' ) ) {
$this->output( 'Data mismatch, updating file: ' . $fileInfo->currentVersionPath . "\n" );
file_put_contents( $fileInfo->currentVersionPath, $data );
} else {
$this->output( 'Serialization MISMATCH: ' . $fileInfo->path . "\n" );
return false;
}
} else {
$this->output( "Serialization OK: " . $fileInfo->path . "\n" );
}
}
return true;
}
}
return ValidateContentTestData::class;