diff --git a/docs/reference/extending.md b/docs/reference/extending.md index 32f691071..38db007c6 100644 --- a/docs/reference/extending.md +++ b/docs/reference/extending.md @@ -74,7 +74,10 @@ $registrar->addNeo4jValueBuilder( ColorType::NAME, static fn ( $value ) => $valu ### Page Property Providers Page Property Providers contribute key/value metadata to the Page node in the graph (queryable via Cypher; -Neo4j is currently the only graph backend). Implement `PagePropertyProvider`: +Neo4j is currently the only graph backend). They run when a revision is stored for a page carrying Subject +data (including undeletions), and again for such pages when the graph is rebuilt with the +`RebuildGraphDatabases` maintenance script. Pages without Subject data are not stored in the graph, so +providers are never invoked for them. Implement `PagePropertyProvider`: ```php class StaticPagePropertyProvider implements PagePropertyProvider { @@ -87,14 +90,14 @@ class StaticPagePropertyProvider implements PagePropertyProvider { ``` Register with `NeoWikiRegistrar::addPagePropertyProvider()`. The context exposes the page id, title, -creation and modification times, categories, and last editor, so providers can derive Page Properties -from the page content without re-fetching or re-parsing it. +creation and modification times, categories, and last editor, as well as the page content and its parse +products, so providers can derive Page Properties from the page content without re-fetching or re-parsing it. -To derive Page Properties from the content, prefer the parse products: `categories`, and +To derive Page Properties from the content, prefer the parse products: `categories` and `parserProperties` — the MediaWiki page properties recorded during parsing (e.g. those a parser hook sets via `ParserOutput::setPageProperty`). These are template-expansion-safe and robust. (Note that `parserProperties` are an input from MediaWiki's parse; they are not the NeoWiki Page Properties this -provider returns.) The raw main slot `content` and its `contentModel` are also exposed, but scraping +provider returns.) The raw main slot `content` and its `contentModel` are the fallback: scraping raw wikitext is fragile — reach for them mainly when handling a custom, non-wikitext content model that the parse products do not cover. Example: [`src/StaticPagePropertyProvider.php`](https://github.com/ProfessionalWiki/NeoWiki/blob/master/tests/RedHerb/src/StaticPagePropertyProvider.php). diff --git a/src/PagePropertiesBuilder.php b/src/PagePropertiesBuilder.php index b24f4dd26..373fea643 100644 --- a/src/PagePropertiesBuilder.php +++ b/src/PagePropertiesBuilder.php @@ -61,7 +61,7 @@ private function buildContext( RevisionRecord $revision, ?UserIdentity $user ): private function parse( Content $content, RevisionRecord $revision ): ParserOutput { return $this->contentHandlerFactory->getContentHandler( $content->getModel() ) - ->getParserOutput( $content, new ContentParseParams( $revision->getPage() ) ); + ->getParserOutput( $content, new ContentParseParams( $revision->getPage(), $revision->getId() ) ); } private function getCreationTime( RevisionRecord $revision ): string { diff --git a/tests/phpunit/PagePropertiesBuilderTest.php b/tests/phpunit/PagePropertiesBuilderTest.php index f1e18e332..41a07811f 100644 --- a/tests/phpunit/PagePropertiesBuilderTest.php +++ b/tests/phpunit/PagePropertiesBuilderTest.php @@ -4,6 +4,7 @@ namespace ProfessionalWiki\NeoWiki\Tests; +use MediaWiki\Revision\RevisionRecord; use ProfessionalWiki\NeoWiki\Domain\Page\PagePropertyProviderContext; use ProfessionalWiki\NeoWiki\Domain\Page\PagePropertyProviderRegistry; use ProfessionalWiki\NeoWiki\PagePropertiesBuilder; @@ -39,9 +40,46 @@ public function testProviderReceivesCategoriesFromParsedContent(): void { $this->assertSame( [ 'Cats' ], $context->categories ); } + public function testParseIsBoundToTheProvidedRevision(): void { + $revision = $this->editPage( 'PagePropertiesBuilderTestPage', '{{DEFAULTSORT:Rev{{REVISIONID}}}}' )->getNewRevision(); + $this->editPage( 'PagePropertiesBuilderTestPage', 'Newer revision without a defaultsort' ); + + $this->assertSame( + 'Rev' . $revision->getId(), + $this->getContextForRevision( $revision )->parserProperties['defaultsort'] + ); + } + + public function testProviderReceivesContentModelOfNonWikitextContent(): void { + $revision = $this->editPage( 'MediaWiki:PagePropertiesBuilderTest.json', '{ "answer": 42 }' )->getNewRevision(); + + $this->assertSame( CONTENT_MODEL_JSON, $this->getContextForRevision( $revision )->contentModel ); + } + + public function testContextHasEmptySentinelsWhenContentIsUnavailable(): void { + $revision = $this->editPage( 'PagePropertiesBuilderTestPage', 'Hidden [[Category:Cats]] {{DEFAULTSORT:Zebra}}' )->getNewRevision(); + $this->editPage( 'PagePropertiesBuilderTestPage', 'Newer public revision' ); + $this->revisionDelete( $revision ); + + $context = $this->getContextForRevision( $this->getSuppressedRevision( $revision->getId() ) ); + + $this->assertSame( '', $context->content ); + $this->assertSame( '', $context->contentModel ); + $this->assertSame( [], $context->parserProperties ); + $this->assertSame( [], $context->categories ); + } + + private function getSuppressedRevision( int $revisionId ): RevisionRecord { + return $this->getServiceContainer()->getRevisionStore()->getRevisionById( $revisionId ); + } + private function getContextForNewPageWithContent( string $wikitext ): PagePropertyProviderContext { - $revision = $this->editPage( 'PagePropertiesBuilderTestPage', $wikitext )->getNewRevision(); + return $this->getContextForRevision( + $this->editPage( 'PagePropertiesBuilderTestPage', $wikitext )->getNewRevision() + ); + } + private function getContextForRevision( RevisionRecord $revision ): PagePropertyProviderContext { $spy = new SpyPagePropertyProvider(); $this->newPagePropertiesBuilder( $spy )->getPagePropertiesFor( $revision, null );