diff --git a/docs/developers/extensions/index.rst b/docs/developers/extensions/index.rst
index 77ac63ff6..2001e12aa 100644
--- a/docs/developers/extensions/index.rst
+++ b/docs/developers/extensions/index.rst
@@ -36,3 +36,4 @@ Some ways to extend the guides:
structure
templates
text-roles
+ interlinks
diff --git a/docs/developers/extensions/interlinks.rst b/docs/developers/extensions/interlinks.rst
new file mode 100644
index 000000000..ade00c68c
--- /dev/null
+++ b/docs/developers/extensions/interlinks.rst
@@ -0,0 +1,72 @@
+.. include:: /include.rst.txt
+
+.. _custom_interlink_resolvers:
+
+===================
+Interlink Resolvers
+===================
+
+Interlinks are external references resolved from inventory files.
+The format is inspired by Sphinx intersphinx inventories and lets guides resolve
+links like ``project-id:target`` against a configured inventory source.
+
+Implement a custom resolver
+===========================
+
+Create a class implementing
+:php:class:`phpDocumentor\Guides\ReferenceResolvers\Interlink\InventoryLinkResolver`.
+
+The key method is ``resolveInventoryLink()``. It receives the parsed cross-reference
+node and should return a :php:class:`phpDocumentor\Guides\ReferenceResolvers\Interlink\ResolvedInventoryLink`
+when the target can be resolved, otherwise ``null``.
+
+Register your resolver in DI
+============================
+
+Register your service with the tag ``phpdoc.guides.interlink_resolver``.
+The chained resolver collects all services with this tag.
+
+.. code-block:: php
+ :caption: your-extension/resources/config/your-extension.php
+
+ services()
+ ->defaults()
+ ->autowire()
+ ->autoconfigure()
+ ->set(MyInventoryResolver::class)
+ ->tag('phpdoc.guides.interlink_resolver');
+ };
+
+Your resolver is then considered together with the built-in default repository.
+
+Disable the default repository
+==============================
+
+If your extension should fully control interlink resolution, disable the built-in
+``DefaultInventoryRepository`` by setting the parameter
+``phpdoc.guides.interlink.default_repository.enabled`` to ``false``.
+
+.. code-block:: php
+ :caption: your-extension/resources/config/your-extension.php
+
+ parameters()
+ ->set('phpdoc.guides.interlink.default_repository.enabled', false);
+ };
+
+When disabled, the default repository reports no available inventories, so only
+custom tagged resolvers participate in interlink resolution.
diff --git a/docs/reference/restructuredtext/interlinks.rst b/docs/reference/restructuredtext/interlinks.rst
new file mode 100644
index 000000000..9716d2927
--- /dev/null
+++ b/docs/reference/restructuredtext/interlinks.rst
@@ -0,0 +1,60 @@
+.. include:: /include.rst.txt
+
+=========
+Interlinks
+=========
+
+Interlinks let you reference documentation in other projects.
+The feature is inspired by Sphinx intersphinx links: guides reads inventory data
+for configured external projects and resolves references by inventory id.
+
+Configure external inventories
+==============================
+
+Define one or more inventories in ``guides.xml``:
+
+.. code-block:: xml
+ :caption: guides.xml
+
+
+
+
+
+
+The ``id`` is the interlink domain used in references.
+
+Use interlinks in reStructuredText
+==================================
+
+Interlinks are used in reference-oriented text roles by prefixing the target with
+``:``.
+
+Examples with ``:ref:``
+-----------------------
+
+.. code-block::
+
+ :ref:`t3coreapi:assets`
+ :ref:`Working with assets `
+
+Examples with ``:doc:``
+-----------------------
+
+.. code-block::
+
+ :doc:`t3coreapi:ApiOverview/Assets/Index`
+ :doc:`Assets chapter `
+
+Resolution behavior
+===================
+
+- The resolver first selects a repository that reports the requested inventory id.
+- It then resolves the target in that inventory.
+- If no repository provides the inventory id, a warning is logged and the link is not resolved.
+
+See also
+========
+
+- :ref:`Text Roles `
+- :ref:`custom_interlink_resolvers`
+
diff --git a/docs/reference/restructuredtext/text-roles.rst b/docs/reference/restructuredtext/text-roles.rst
index 043b11aae..d54d236b2 100644
--- a/docs/reference/restructuredtext/text-roles.rst
+++ b/docs/reference/restructuredtext/text-roles.rst
@@ -6,6 +6,8 @@ Text Roles
Text roles can be used to style content inline. Some text roles have advanced processing such as reference resolving.
+For external project references using inventory ids, see :doc:`interlinks`.
+
You can also :ref:`add your own custom text roles `.
Currently the following text roles are implemented:
diff --git a/packages/guides/resources/config/guides.php b/packages/guides/resources/config/guides.php
index 02e9657a3..5196a65b6 100644
--- a/packages/guides/resources/config/guides.php
+++ b/packages/guides/resources/config/guides.php
@@ -30,6 +30,7 @@
use phpDocumentor\Guides\ReferenceResolvers\EmailReferenceResolver;
use phpDocumentor\Guides\ReferenceResolvers\ExternalReferenceResolver;
use phpDocumentor\Guides\ReferenceResolvers\ImageReferenceResolverPreRender;
+use phpDocumentor\Guides\ReferenceResolvers\Interlink\ChainedInventoryLinkResolver;
use phpDocumentor\Guides\ReferenceResolvers\Interlink\DefaultInventoryLoader;
use phpDocumentor\Guides\ReferenceResolvers\Interlink\DefaultInventoryRepository;
use phpDocumentor\Guides\ReferenceResolvers\Interlink\InventoryLoader;
@@ -74,7 +75,8 @@
return static function (ContainerConfigurator $container): void {
$container->parameters()
- ->set('phpdoc.guides.base_template_paths', [__DIR__ . '/../../../guides/resources/template/html']);
+ ->set('phpdoc.guides.base_template_paths', [__DIR__ . '/../../../guides/resources/template/html'])
+ ->set('phpdoc.guides.interlink.default_repository.enabled', true);
$container->services()
->defaults()
@@ -129,8 +131,13 @@
->set(DocumentNodeTraverser::class)
- ->set(InventoryRepository::class, DefaultInventoryRepository::class)
+ ->set(DefaultInventoryRepository::class)
->arg('$inventoryConfigs', param('phpdoc.guides.inventories'))
+ ->arg('$enabled', param('phpdoc.guides.interlink.default_repository.enabled'))
+ ->tag('phpdoc.guides.interlink_resolver')
+
+ ->set(InventoryRepository::class, ChainedInventoryLinkResolver::class)
+ ->arg('$repositories', tagged_iterator('phpdoc.guides.interlink_resolver'))
->set(InventoryLoader::class, DefaultInventoryLoader::class)
diff --git a/packages/guides/src/ReferenceResolvers/Interlink/ChainedInventoryLinkResolver.php b/packages/guides/src/ReferenceResolvers/Interlink/ChainedInventoryLinkResolver.php
new file mode 100644
index 000000000..2f9ab8909
--- /dev/null
+++ b/packages/guides/src/ReferenceResolvers/Interlink/ChainedInventoryLinkResolver.php
@@ -0,0 +1,107 @@
+ */
+ private array $cachedRepositories = [];
+
+ /** @param iterable $repositories */
+ public function __construct(
+ private readonly iterable $repositories,
+ ) {
+ }
+
+ public function getLink(CrossReferenceNode $node, RenderContext $renderContext, Messages $messages): InventoryLink|null
+ {
+ return $this->resolveInventoryLink($node, $renderContext, $messages)?->getLink();
+ }
+
+ public function hasInventory(string $key): bool
+ {
+ return $this->findInventoryRepository($key) !== null;
+ }
+
+ public function getInventory(CrossReferenceNode $node, RenderContext $renderContext, Messages $messages): Inventory|null
+ {
+ Deprecation::trigger(
+ 'phpDocumentor/guides',
+ 'https://github.com/phpDocumentor/guides/issues',
+ 'InventoryRepository::getInventory() is deprecated. Implement '
+ . 'InventoryLinkResolver::resolveInventoryLink() for one-call interlink resolution.',
+ );
+
+ return $this->findInventoryRepository($node->getInterlinkDomain())?->getInventory($node, $renderContext, $messages);
+ }
+
+ public function resolveInventoryLink(
+ CrossReferenceNode $node,
+ RenderContext $renderContext,
+ Messages $messages,
+ ): ResolvedInventoryLink|null {
+ $repository = $this->findInventoryRepository($node->getInterlinkDomain());
+ if ($repository === null) {
+ $messages->addWarning(
+ new Message(
+ sprintf('Inventory with key %s not found. ', $node->getInterlinkDomain()),
+ array_merge($renderContext->getLoggerInformation(), $node->getDebugInformation()),
+ ),
+ );
+
+ return null;
+ }
+
+ if ($repository instanceof InventoryLinkResolver) {
+ return $repository->resolveInventoryLink($node, $renderContext, $messages);
+ }
+
+ $inventory = $repository->getInventory($node, $renderContext, $messages);
+ $link = $repository->getLink($node, $renderContext, $messages);
+ if ($inventory === null || $link === null) {
+ return null;
+ }
+
+ return new ResolvedInventoryLink($inventory->getBaseUrl(), $link);
+ }
+
+ private function findInventoryRepository(string $key): InventoryRepository|null
+ {
+ if (array_key_exists($key, $this->cachedRepositories)) {
+ return $this->cachedRepositories[$key];
+ }
+
+ foreach ($this->repositories as $repository) {
+ if ($repository->hasInventory($key)) {
+ $this->cachedRepositories[$key] = $repository;
+
+ return $repository;
+ }
+ }
+
+ $this->cachedRepositories[$key] = null;
+
+ return null;
+ }
+}
diff --git a/packages/guides/src/ReferenceResolvers/Interlink/DefaultInventoryRepository.php b/packages/guides/src/ReferenceResolvers/Interlink/DefaultInventoryRepository.php
index a1192877a..7994ffde9 100644
--- a/packages/guides/src/ReferenceResolvers/Interlink/DefaultInventoryRepository.php
+++ b/packages/guides/src/ReferenceResolvers/Interlink/DefaultInventoryRepository.php
@@ -13,6 +13,7 @@
namespace phpDocumentor\Guides\ReferenceResolvers\Interlink;
+use Doctrine\Deprecations\Deprecation;
use phpDocumentor\Guides\Nodes\Inline\CrossReferenceNode;
use phpDocumentor\Guides\ReferenceResolvers\AnchorNormalizer;
use phpDocumentor\Guides\ReferenceResolvers\Message;
@@ -23,7 +24,7 @@
use function array_merge;
use function sprintf;
-final class DefaultInventoryRepository implements InventoryRepository
+final class DefaultInventoryRepository implements InventoryLinkResolver
{
/** @var array */
private array $inventories = [];
@@ -33,28 +34,58 @@ public function __construct(
private readonly AnchorNormalizer $anchorNormalizer,
private readonly InventoryLoader $inventoryLoader,
array $inventoryConfigs,
+ private readonly bool $enabled = true,
) {
foreach ($inventoryConfigs as $inventory) {
- $this->inventories[$this->anchorNormalizer->reduceAnchor($inventory['id'])] = new Inventory($inventory['url'], $anchorNormalizer);
+ $this->inventories[$this->anchorNormalizer->reduceAnchor($inventory['id'])]
+ = new Inventory($inventory['url'], $anchorNormalizer);
}
}
public function getLink(CrossReferenceNode $node, RenderContext $renderContext, Messages $messages): InventoryLink|null
{
- $inventory = $this->getInventory($node, $renderContext, $messages);
- $group = $inventory?->getGroup($node, $renderContext, $messages);
-
- return $group?->getLink($node, $renderContext, $messages);
+ return $this->resolveInventoryLink($node, $renderContext, $messages)?->getLink();
}
public function hasInventory(string $key): bool
{
+ if (!$this->enabled) {
+ return false;
+ }
+
$reducedKey = $this->anchorNormalizer->reduceAnchor($key);
return array_key_exists($reducedKey, $this->inventories);
}
public function getInventory(CrossReferenceNode $node, RenderContext $renderContext, Messages $messages): Inventory|null
+ {
+ Deprecation::trigger(
+ 'phpDocumentor/guides',
+ 'https://github.com/phpDocumentor/guides/issues',
+ 'InventoryRepository::getInventory() is deprecated. Implement '
+ . 'InventoryLinkResolver::resolveInventoryLink() for one-call interlink resolution.',
+ );
+
+ return $this->findInventory($node, $renderContext, $messages);
+ }
+
+ public function resolveInventoryLink(
+ CrossReferenceNode $node,
+ RenderContext $renderContext,
+ Messages $messages,
+ ): ResolvedInventoryLink|null {
+ $inventory = $this->findInventory($node, $renderContext, $messages);
+ $group = $inventory?->getGroup($node, $renderContext, $messages);
+ $link = $group?->getLink($node, $renderContext, $messages);
+ if ($inventory === null || $link === null) {
+ return null;
+ }
+
+ return new ResolvedInventoryLink($inventory->getBaseUrl(), $link);
+ }
+
+ private function findInventory(CrossReferenceNode $node, RenderContext $renderContext, Messages $messages): Inventory|null
{
$reducedKey = $this->anchorNormalizer->reduceAnchor($node->getInterlinkDomain());
if (!$this->hasInventory($reducedKey)) {
diff --git a/packages/guides/src/ReferenceResolvers/Interlink/InventoryLinkResolver.php b/packages/guides/src/ReferenceResolvers/Interlink/InventoryLinkResolver.php
new file mode 100644
index 000000000..019701c7d
--- /dev/null
+++ b/packages/guides/src/ReferenceResolvers/Interlink/InventoryLinkResolver.php
@@ -0,0 +1,27 @@
+baseUrl;
+ }
+
+ public function getLink(): InventoryLink
+ {
+ return $this->link;
+ }
+}
diff --git a/packages/guides/src/ReferenceResolvers/InterlinkReferenceResolver.php b/packages/guides/src/ReferenceResolvers/InterlinkReferenceResolver.php
index 3bef72104..7003001f7 100644
--- a/packages/guides/src/ReferenceResolvers/InterlinkReferenceResolver.php
+++ b/packages/guides/src/ReferenceResolvers/InterlinkReferenceResolver.php
@@ -17,6 +17,7 @@
use phpDocumentor\Guides\Nodes\Inline\CrossReferenceNode;
use phpDocumentor\Guides\Nodes\Inline\LinkInlineNode;
use phpDocumentor\Guides\Nodes\Inline\PlainTextInlineNode;
+use phpDocumentor\Guides\ReferenceResolvers\Interlink\InventoryLinkResolver;
use phpDocumentor\Guides\ReferenceResolvers\Interlink\InventoryRepository;
use phpDocumentor\Guides\RenderContext;
@@ -37,17 +38,29 @@ public function resolve(LinkInlineNode $node, RenderContext $renderContext, Mess
return false;
}
- $inventory = $this->inventoryRepository->getInventory($node, $renderContext, $messages);
- if ($inventory === null) {
- return false;
- }
+ if ($this->inventoryRepository instanceof InventoryLinkResolver) {
+ $resolvedInventoryLink = $this->inventoryRepository->resolveInventoryLink($node, $renderContext, $messages);
+ if ($resolvedInventoryLink === null) {
+ return false;
+ }
- $link = $this->inventoryRepository->getLink($node, $renderContext, $messages);
- if ($link === null) {
- return false;
+ $baseUrl = $resolvedInventoryLink->getBaseUrl();
+ $link = $resolvedInventoryLink->getLink();
+ } else {
+ $inventory = $this->inventoryRepository->getInventory($node, $renderContext, $messages);
+ if ($inventory === null) {
+ return false;
+ }
+
+ $link = $this->inventoryRepository->getLink($node, $renderContext, $messages);
+ if ($link === null) {
+ return false;
+ }
+
+ $baseUrl = $inventory->getBaseUrl();
}
- $node->setUrl($inventory->getBaseUrl() . $link->getPath());
+ $node->setUrl($baseUrl . $link->getPath());
if ($node instanceof CompoundNode) {
if (count($node->getChildren()) === 0) {
$node->addChildNode(new PlainTextInlineNode($link->getTitle()));
diff --git a/packages/guides/tests/unit/Interlink/ChainedInventoryLinkResolverTest.php b/packages/guides/tests/unit/Interlink/ChainedInventoryLinkResolverTest.php
new file mode 100644
index 000000000..1143c3dde
--- /dev/null
+++ b/packages/guides/tests/unit/Interlink/ChainedInventoryLinkResolverTest.php
@@ -0,0 +1,121 @@
+renderContext = $this->createMock(RenderContext::class);
+ $this->renderContext->method('getLoggerInformation')->willReturn([]);
+ }
+
+ public function testResolvesAndCachesRepositoryLookup(): void
+ {
+ $node = new ReferenceNode('modindex', [], 'some-key');
+ $messages = new Messages();
+ $link = new InventoryLink('project', '1.0', 'path.html', 'Some title');
+
+ $repository = $this->createMock(InventoryLinkResolver::class);
+ assert($repository instanceof InventoryLinkResolver);
+ $repository->expects(self::once())->method('hasInventory')->with('some-key')->willReturn(true);
+ $repository->expects(self::exactly(2))
+ ->method('resolveInventoryLink')
+ ->willReturn(new ResolvedInventoryLink('https://example.com/', $link));
+
+ $resolver = new ChainedInventoryLinkResolver([$repository]);
+
+ $resolved = $resolver->resolveInventoryLink($node, $this->renderContext, $messages);
+ self::assertNotNull($resolved);
+ self::assertEquals('https://example.com/', $resolved->getBaseUrl());
+
+ $resolved = $resolver->resolveInventoryLink($node, $this->renderContext, $messages);
+ self::assertNotNull($resolved);
+ self::assertEquals('path.html', $resolved->getLink()->getPath());
+ self::assertCount(0, $messages->getWarnings());
+ }
+
+ public function testAddsWarningWhenNoRepositoryMatchesDomain(): void
+ {
+ $node = new ReferenceNode('modindex', [], 'missing-domain');
+ $messages = new Messages();
+
+ $repository = $this->createMock(InventoryRepository::class);
+ assert($repository instanceof InventoryRepository);
+ $repository->expects(self::once())->method('hasInventory')->with('missing-domain')->willReturn(false);
+
+ $resolver = new ChainedInventoryLinkResolver([$repository]);
+ self::assertNull($resolver->resolveInventoryLink($node, $this->renderContext, $messages));
+ self::assertCount(1, $messages->getWarnings());
+ }
+
+ public function testFallsBackToLegacyRepositoryInterface(): void
+ {
+ $node = new ReferenceNode('modindex', [], 'legacy');
+ $messages = new Messages();
+
+ $anchorNormalizer = new NullAnchorNormalizer();
+ $inventory = new Inventory('https://legacy.example/', $anchorNormalizer);
+ $group = new InventoryGroup($anchorNormalizer);
+ $group->addLink('modindex', new InventoryLink('project', '1.0', 'legacy.html', 'Legacy'));
+ $inventory->addGroup('std:label', $group);
+
+ $repository = new class ($inventory) implements InventoryRepository {
+ public function __construct(private readonly Inventory $inventory)
+ {
+ }
+
+ public function getLink(CrossReferenceNode $node, RenderContext $renderContext, Messages $messages): InventoryLink|null
+ {
+ return $this->inventory->getGroup($node, $renderContext, $messages)?->getLink($node, $renderContext, $messages);
+ }
+
+ public function hasInventory(string $key): bool
+ {
+ return $key === 'legacy';
+ }
+
+ public function getInventory(CrossReferenceNode $node, RenderContext $renderContext, Messages $messages): Inventory|null
+ {
+ return $this->inventory;
+ }
+ };
+
+ $resolver = new ChainedInventoryLinkResolver([$repository]);
+ $resolved = $resolver->resolveInventoryLink($node, $this->renderContext, $messages);
+
+ self::assertNotNull($resolved);
+ self::assertEquals('https://legacy.example/', $resolved->getBaseUrl());
+ self::assertEquals('legacy.html', $resolved->getLink()->getPath());
+ }
+}
diff --git a/packages/guides/tests/unit/Interlink/InventoryLoaderTest.php b/packages/guides/tests/unit/Interlink/InventoryLoaderTest.php
index bc1120ee0..4a3194b27 100644
--- a/packages/guides/tests/unit/Interlink/InventoryLoaderTest.php
+++ b/packages/guides/tests/unit/Interlink/InventoryLoaderTest.php
@@ -22,6 +22,7 @@
use phpDocumentor\Guides\ReferenceResolvers\Interlink\DefaultInventoryRepository;
use phpDocumentor\Guides\ReferenceResolvers\Interlink\Inventory;
use phpDocumentor\Guides\ReferenceResolvers\Interlink\InventoryLink;
+use phpDocumentor\Guides\ReferenceResolvers\Interlink\InventoryLinkResolver;
use phpDocumentor\Guides\ReferenceResolvers\Interlink\JsonLoader;
use phpDocumentor\Guides\ReferenceResolvers\Messages;
use phpDocumentor\Guides\ReferenceResolvers\SluggerAnchorNormalizer;
@@ -73,10 +74,11 @@ public function loadObjectsJsonInv(string $filename): void
public function testInventoryLoaderLoadsInventory(): void
{
- $node = new DocReferenceNode('SomeDocument', [], 'somekey');
- $inventory = $this->inventoryRepository->getInventory($node, $this->renderContext, new Messages());
- self::assertTrue($inventory instanceof Inventory);
- self::assertGreaterThan(1, count($inventory->getGroups()));
+ $node = new ReferenceNode('modindex', [], 'somekey');
+ self::assertInstanceOf(InventoryLinkResolver::class, $this->inventoryRepository);
+ $resolved = $this->inventoryRepository->resolveInventoryLink($node, $this->renderContext, new Messages());
+ self::assertNotNull($resolved);
+ self::assertNotSame('', $resolved->getBaseUrl());
}
public function testInventoryIsLoadedExactlyOnce(): void
@@ -93,10 +95,11 @@ public function testInventoryIsLoadedExactlyOnce(): void
public function testInventoryLoaderAcceptsNull(): void
{
$this->loadObjectsJsonInv(__DIR__ . '/fixtures/null-in-objects.inv.json');
- $node = new DocReferenceNode('SomeDocument', [], 'somekey');
- $inventory = $this->inventoryRepository->getInventory($node, $this->renderContext, new Messages());
- self::assertTrue($inventory instanceof Inventory);
- self::assertGreaterThan(1, count($inventory->getGroups()));
+ $node = new ReferenceNode('modindex', [], 'somekey');
+ self::assertInstanceOf(InventoryLinkResolver::class, $this->inventoryRepository);
+ $resolved = $this->inventoryRepository->resolveInventoryLink($node, $this->renderContext, new Messages());
+ self::assertNotNull($resolved);
+ self::assertNotSame('', $resolved->getBaseUrl());
}
#[DataProvider('rawAnchorProvider')]
@@ -179,4 +182,17 @@ public static function notFoundInventoryProvider(): Generator
new DocReferenceNode('Page1-Subpage1', [], 'somekey'),
];
}
+
+ public function testDisabledRepositoryNeverClaimsInventory(): void
+ {
+ $disabledRepository = new DefaultInventoryRepository(
+ new SluggerAnchorNormalizer(),
+ $this->inventoryLoader,
+ [['id' => 'somekey', 'url' => 'https://example.com/']],
+ false,
+ );
+
+ self::assertFalse($disabledRepository->hasInventory('somekey'));
+ self::assertFalse($disabledRepository->hasInventory('some-key'));
+ }
}
diff --git a/packages/guides/tests/unit/ReferenceResolvers/InterlinkReferenceResolverTest.php b/packages/guides/tests/unit/ReferenceResolvers/InterlinkReferenceResolverTest.php
index fe256b252..5f2316691 100644
--- a/packages/guides/tests/unit/ReferenceResolvers/InterlinkReferenceResolverTest.php
+++ b/packages/guides/tests/unit/ReferenceResolvers/InterlinkReferenceResolverTest.php
@@ -13,10 +13,13 @@
namespace phpDocumentor\Guides\ReferenceResolvers;
+use phpDocumentor\Guides\Nodes\Inline\CrossReferenceNode;
use phpDocumentor\Guides\Nodes\Inline\DocReferenceNode;
use phpDocumentor\Guides\ReferenceResolvers\Interlink\Inventory;
use phpDocumentor\Guides\ReferenceResolvers\Interlink\InventoryLink;
+use phpDocumentor\Guides\ReferenceResolvers\Interlink\InventoryLinkResolver;
use phpDocumentor\Guides\ReferenceResolvers\Interlink\InventoryRepository;
+use phpDocumentor\Guides\ReferenceResolvers\Interlink\ResolvedInventoryLink;
use phpDocumentor\Guides\RenderContext;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\MockObject\MockObject;
@@ -25,16 +28,12 @@
final class InterlinkReferenceResolverTest extends TestCase
{
private RenderContext&MockObject $renderContext;
- private MockObject&InventoryRepository $inventoryRepository;
- private InterlinkReferenceResolver $subject;
private AnchorNormalizer $anchorNormalizer;
protected function setUp(): void
{
$this->renderContext = $this->createMock(RenderContext::class);
- $this->inventoryRepository = $this->createMock(InventoryRepository::class);
$this->anchorNormalizer = new NullAnchorNormalizer();
- $this->subject = new InterlinkReferenceResolver($this->inventoryRepository);
}
#[DataProvider('pathProvider')]
@@ -43,10 +42,50 @@ public function testDocumentReducer(string $expected, string $input, string $pat
$input = new DocReferenceNode($input, [], 'interlink-target');
$inventoryLink = new InventoryLink('project', '1.0', $path, '');
$inventory = new Inventory('base-url/', $this->anchorNormalizer);
- $this->inventoryRepository->expects(self::once())->method('getInventory')->willReturn($inventory);
- $this->inventoryRepository->expects(self::once())->method('getLink')->willReturn($inventoryLink);
+
+ $inventoryRepository = new class ($inventory, $inventoryLink) implements InventoryRepository {
+ public function __construct(private readonly Inventory $inventory, private readonly InventoryLink $inventoryLink)
+ {
+ }
+
+ public function getLink(CrossReferenceNode $node, RenderContext $renderContext, Messages $messages): InventoryLink|null
+ {
+ return $this->inventoryLink;
+ }
+
+ public function hasInventory(string $key): bool
+ {
+ return true;
+ }
+
+ public function getInventory(CrossReferenceNode $node, RenderContext $renderContext, Messages $messages): Inventory|null
+ {
+ return $this->inventory;
+ }
+ };
+
+ $subject = new InterlinkReferenceResolver($inventoryRepository);
$messages = new Messages();
- self::assertTrue($this->subject->resolve($input, $this->renderContext, $messages));
+ self::assertTrue($subject->resolve($input, $this->renderContext, $messages));
+ self::assertEmpty($messages->getWarnings());
+ self::assertEquals($expected, $input->getUrl());
+ }
+
+ #[DataProvider('pathProvider')]
+ public function testDocumentReducerUsesOneCallResolver(string $expected, string $input, string $path): void
+ {
+ $input = new DocReferenceNode($input, [], 'interlink-target');
+ $inventoryLink = new InventoryLink('project', '1.0', $path, '');
+
+ $inventoryRepository = $this->createMock(InventoryLinkResolver::class);
+ $inventoryRepository->expects(self::once())
+ ->method('resolveInventoryLink')
+ ->willReturn(new ResolvedInventoryLink('base-url/', $inventoryLink));
+
+ $subject = new InterlinkReferenceResolver($inventoryRepository);
+ $messages = new Messages();
+
+ self::assertTrue($subject->resolve($input, $this->renderContext, $messages));
self::assertEmpty($messages->getWarnings());
self::assertEquals($expected, $input->getUrl());
}