From 574e5a1fe85b4ede151d73b8ae40e66ac41117bb Mon Sep 17 00:00:00 2001 From: Morne Alberts Date: Sat, 30 Nov 2024 18:51:24 +0200 Subject: [PATCH] Rewrite item link with page sitelink (#24) --- phpstan-baseline.neon | 50 +-------- phpstan.neon | 4 + .../WikibaseFacetedSearchHooks.php | 100 +++++++++++++++++- src/Persistence/ItemPageLookup.php | 14 +++ src/Persistence/SiteLinkItemPageLookup.php | 32 ++++++ src/WikibaseFacetedSearchExtension.php | 12 +++ .../SiteLinkItemPageLookupTest.php | 70 ++++++++++++ 7 files changed, 232 insertions(+), 50 deletions(-) create mode 100644 src/Persistence/ItemPageLookup.php create mode 100644 src/Persistence/SiteLinkItemPageLookup.php create mode 100644 tests/Persistence/SiteLinkItemPageLookupTest.php diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 4fd5cee..24394c8 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1,55 +1,13 @@ parameters: ignoreErrors: - - message: '#^Call to method addHTML\(\) on an unknown class OutputPage\.$#' - identifier: class.notFound + message: '#^Method ProfessionalWiki\\WikibaseFacetedSearch\\EntryPoints\\WikibaseFacetedSearchHooks\:\:rewriteLinkForNonEntityResult\(\) never assigns null to &\$titleSnippet so it can be removed from the by\-ref type\.$#' + identifier: parameterByRef.unusedType count: 1 path: src/EntryPoints/WikibaseFacetedSearchHooks.php - - message: '#^Call to method addModuleStyles\(\) on an unknown class OutputPage\.$#' - identifier: class.notFound - count: 1 - path: src/EntryPoints/WikibaseFacetedSearchHooks.php - - - - message: '#^Call to method getNamespace\(\) on an unknown class Title\.$#' - identifier: class.notFound - count: 1 - path: src/EntryPoints/WikibaseFacetedSearchHooks.php - - - - message: '#^Call to static method element\(\) on an unknown class Html\.$#' - identifier: class.notFound - count: 1 - path: src/EntryPoints/WikibaseFacetedSearchHooks.php - - - - message: '#^Constant WB_NS_ITEM not found\.$#' - identifier: constant.notFound - count: 1 - path: src/EntryPoints/WikibaseFacetedSearchHooks.php - - - - message: '#^Parameter \$output of method ProfessionalWiki\\WikibaseFacetedSearch\\EntryPoints\\WikibaseFacetedSearchHooks\:\:onSpecialSearchResultsAppend\(\) has invalid type OutputPage\.$#' - identifier: class.notFound - count: 1 - path: src/EntryPoints/WikibaseFacetedSearchHooks.php - - - - message: '#^Parameter \$specialSearch of method ProfessionalWiki\\WikibaseFacetedSearch\\EntryPoints\\WikibaseFacetedSearchHooks\:\:onShowSearchHitTitle\(\) has invalid type SpecialSearch\.$#' - identifier: class.notFound - count: 1 - path: src/EntryPoints/WikibaseFacetedSearchHooks.php - - - - message: '#^Parameter \$specialSearch of method ProfessionalWiki\\WikibaseFacetedSearch\\EntryPoints\\WikibaseFacetedSearchHooks\:\:onSpecialSearchResultsAppend\(\) has invalid type SpecialSearch\.$#' - identifier: class.notFound - count: 1 - path: src/EntryPoints/WikibaseFacetedSearchHooks.php - - - - message: '#^Parameter \$title of method ProfessionalWiki\\WikibaseFacetedSearch\\EntryPoints\\WikibaseFacetedSearchHooks\:\:onShowSearchHitTitle\(\) has invalid type Title\.$#' - identifier: class.notFound + message: '#^Method ProfessionalWiki\\WikibaseFacetedSearch\\EntryPoints\\WikibaseFacetedSearchHooks\:\:rewriteLinkForNonEntityResult\(\) never assigns string to &\$titleSnippet so it can be removed from the by\-ref type\.$#' + identifier: parameterByRef.unusedType count: 1 path: src/EntryPoints/WikibaseFacetedSearchHooks.php diff --git a/phpstan.neon b/phpstan.neon index 18318a6..0c6e9f9 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -9,4 +9,8 @@ parameters: - ../../includes - ../../tests/phpunit - ../../vendor + - ../../extensions/CirrusSearch - ../../extensions/Wikibase + - ../../extensions/WikibaseCirrusSearch + bootstrapFiles: + - ../../includes/AutoLoader.php diff --git a/src/EntryPoints/WikibaseFacetedSearchHooks.php b/src/EntryPoints/WikibaseFacetedSearchHooks.php index 17c2b8c..dfdff54 100644 --- a/src/EntryPoints/WikibaseFacetedSearchHooks.php +++ b/src/EntryPoints/WikibaseFacetedSearchHooks.php @@ -5,17 +5,26 @@ namespace ProfessionalWiki\WikibaseFacetedSearch\EntryPoints; use HtmlArmor; +use IContextSource; +use Language; use OutputPage; +use ProfessionalWiki\WikibaseFacetedSearch\WikibaseFacetedSearchExtension; use SearchResult; use SpecialSearch; use Title; +use Wikibase\DataModel\Entity\ItemId; +use Wikibase\DataModel\Term\TermFallback; +use Wikibase\Lib\Store\LanguageFallbackLabelDescriptionLookup; +use Wikibase\Repo\Hooks\Formatters\EntityLinkFormatter; +use Wikibase\Repo\WikibaseRepo; +use Wikibase\Search\Elastic\EntityResult; class WikibaseFacetedSearchHooks { /** * @param string[] $terms - * @param array $query - * @param array $attributes + * @param string[] $query + * @param string[] $attributes */ public static function onShowSearchHitTitle( Title &$title, @@ -26,11 +35,94 @@ public static function onShowSearchHitTitle( array &$query, array &$attributes ): void { - if ( $title->getNamespace() !== WB_NS_ITEM ) { + $itemId = self::getItemId( $title ); + + if ( $itemId === null ) { + return; + } + + $pageTitle = self::getItemPage( $itemId ); + + if ( $pageTitle === null ) { return; } - // TODO: get item site link and replace $title + $title = $pageTitle; + + if ( !( $result instanceof EntityResult ) ) { + self::rewriteLinkForNonEntityResult( + self::newLabelDescriptionLookup( $specialSearch->getContext() ), + self::newLinkFormatter( $specialSearch->getLanguage() ), + $itemId, + $titleSnippet, + $attributes + ); + } + } + + private static function getItemId( Title $title ): ?ItemId { + $entityId = WikibaseRepo::getEntityIdLookup()->getEntityIdForTitle( $title ); + + if ( $entityId instanceof ItemId ) { + return $entityId; + } + + return null; + } + + private static function getItemPage( ItemId $itemId ): ?Title { + return WikibaseFacetedSearchExtension::getInstance()->getItemPageLookup()->getPageTitle( $itemId ); + } + + private static function newLinkFormatter( Language $language ): EntityLinkFormatter { + return WikibaseRepo::getEntityLinkFormatterFactory()->getDefaultLinkFormatter( $language ); + } + + private static function newLabelDescriptionLookup( IContextSource $context ): LanguageFallbackLabelDescriptionLookup { + return new LanguageFallbackLabelDescriptionLookup( + WikibaseRepo::getTermLookup(), + WikibaseRepo::getLanguageFallbackChainFactory()->newFromContext( $context ) + ); + } + + /** + * @param string[] $attributes + */ + private static function rewriteLinkForNonEntityResult( + LanguageFallbackLabelDescriptionLookup $labelDescriptionLookup, + EntityLinkFormatter $linkFormatter, + ItemId $itemId, + string|HtmlArmor|null &$titleSnippet, + array &$attributes + ): void { + $labelData = self::termFallbackToTermData( + $labelDescriptionLookup->getLabel( $itemId ) + ); + $descriptionData = self::termFallbackToTermData( + $labelDescriptionLookup->getDescription( $itemId ) + ); + + $titleSnippet = new HtmlArmor( $linkFormatter->getHtml( $itemId, $labelData ) ); + + $attributes['title'] = $linkFormatter->getTitleAttribute( + $itemId, + $labelData, + $descriptionData + ); + } + + /** + * @return string[]|null + */ + private static function termFallbackToTermData( ?TermFallback $term = null ): ?array { + if ( $term ) { + return [ + 'value' => $term->getText(), + 'language' => $term->getActualLanguageCode(), + ]; + } + + return null; } public static function onSpecialSearchResultsAppend( diff --git a/src/Persistence/ItemPageLookup.php b/src/Persistence/ItemPageLookup.php new file mode 100644 index 0000000..1cb21d6 --- /dev/null +++ b/src/Persistence/ItemPageLookup.php @@ -0,0 +1,14 @@ +siteLinksStore->getSiteLinksForItem( $itemId ), + fn( $siteLink ) => $siteLink->getSiteId() === $this->siteLinkSiteId + )[0] ?? null; + + if ( $siteLink === null ) { + return null; + } + + return Title::newFromText( $siteLink->getPageName() ); + } + +} diff --git a/src/WikibaseFacetedSearchExtension.php b/src/WikibaseFacetedSearchExtension.php index b282778..071986a 100644 --- a/src/WikibaseFacetedSearchExtension.php +++ b/src/WikibaseFacetedSearchExtension.php @@ -4,6 +4,10 @@ namespace ProfessionalWiki\WikibaseFacetedSearch; +use ProfessionalWiki\WikibaseFacetedSearch\Persistence\ItemPageLookup; +use ProfessionalWiki\WikibaseFacetedSearch\Persistence\SiteLinkItemPageLookup; +use Wikibase\Repo\WikibaseRepo; + class WikibaseFacetedSearchExtension { public static function getInstance(): self { @@ -13,4 +17,12 @@ public static function getInstance(): self { return $instance; } + public function getItemPageLookup(): ItemPageLookup { + return new SiteLinkItemPageLookup( + WikibaseRepo::getStore()->newSiteLinkStore(), + // TODO: https://github.com/ProfessionalWiki/WikibaseFacetedSearch/issues/12 + 'mardi' + ); + } + } diff --git a/tests/Persistence/SiteLinkItemPageLookupTest.php b/tests/Persistence/SiteLinkItemPageLookupTest.php new file mode 100644 index 0000000..489db6f --- /dev/null +++ b/tests/Persistence/SiteLinkItemPageLookupTest.php @@ -0,0 +1,70 @@ +siteLinkStore = new HashSiteLinkStore(); + $this->lookup = new SiteLinkItemPageLookup( + $this->siteLinkStore, + self::SITE_ID + ); + } + + public function testReturnsPageWhenSiteLinkExists(): void { + $this->createItemWithSiteLink( 'Q42', self::SITE_ID, 'Page for Q42' ); + + $this->assertItemPageHasTitle( 'Q42', 'Page for Q42' ); + } + + private function createItemWithSiteLink( string $itemId, string $siteId, string $pageName ): void { + $item = new Item( new ItemId( $itemId ) ); + $item->getSiteLinkList()->addNewSiteLink( $siteId, $pageName ); + $this->siteLinkStore->saveLinksOfItem( $item ); + } + + private function assertItemPageHasTitle( string $itemId, $pageTitle ): void { + $this->assertEquals( + Title::newFromText( $pageTitle ), + $this->lookup->getPageTitle( new ItemId( $itemId ) ) + ); + } + + public function testReturnsNullWhenNoSiteLinksExist(): void { + $this->assertNull( $this->lookup->getPageTitle( new ItemId( 'Q404' ) ) ); + } + + public function testReturnsNullWhenOnlyOtherSiteLinksExist(): void { + $this->createItemWithSiteLink( 'Q100', 'otherSiteId', 'Other page' ); + $this->createItemWithSiteLink( 'Q200', 'anotherSiteId', 'Another page' ); + + $this->assertNull( $this->lookup->getPageTitle( new ItemId( 'Q42' ) ) ); + } + + public function testReturnsPageWhenManySiteLinksExist(): void { + $this->createItemWithSiteLink( 'Q42', self::SITE_ID, 'Page for Q42' ); + $this->createItemWithSiteLink( 'Q100', 'otherSiteId', 'Other page' ); + $this->createItemWithSiteLink( 'Q200', 'anotherSiteId', 'Another page' ); + + $this->assertItemPageHasTitle( 'Q42', 'Page for Q42' ); + } + +}