Files
Gnuboard7/tests/Unit/Seo/AbstractSitemapContributorTest.php
T
HeuJung 1a6fca0d6b feat(seo): 사이트맵 기여자 지연 스트리밍 + hreflang 계산 선형화
AbstractSitemapContributor 브리지 base 로 인터페이스를 바꾸지 않고
board/ecommerce/page 기여자를 getUrlsLazy 지연 스트리밍으로 전환해,
게시글이 수백만 건인 사이트에서도 URL 배열을 통째로 적재하지 않는다.
SitemapGenerator 는 capability 를 감지해 지연 경로를 쓰고, base 를
상속하지 않은 제3자 구현체는 기존 getUrls 경로로 그대로 동작한다.

hreflang alternate 집합을 base URL 당 한 번만 계산해 로케일별 <url>
에 재사용하도록 바꿔 계산 비용을 로케일 수의 제곱에서 선형으로 줄였다
(정상 구성 출력은 불변). 다국어 대체 링크 사용 여부 토글과 로케일 수
상한을 추가하고 설정/검증/관리자 UI/다국어(ko·en·ja) 표면을 동반했다.
2026-07-18 15:05:56 +09:00

130 lines
4.0 KiB
PHP

<?php
namespace Tests\Unit\Seo;
use App\Seo\AbstractSitemapContributor;
use App\Seo\Contracts\SitemapContributorInterface;
use Tests\TestCase;
/**
* AbstractSitemapContributor 단위 테스트
*
* getUrls()(배열)와 getUrlsLazy()(지연 제너레이터) 사이의 브리지를 검증합니다.
* 구현체가 둘 중 하나만 오버라이드해도 나머지 한쪽이 자동으로 제공되어야 합니다.
*/
class AbstractSitemapContributorTest extends TestCase
{
/**
* getUrls() 만 구현한 구현체는 getUrlsLazy() 가 그 배열을 지연 순회한다
*/
public function test_lazy_bridges_to_array_when_only_get_urls_is_implemented(): void
{
$contributor = new class extends AbstractSitemapContributor
{
public function getIdentifier(): string
{
return 'array-only';
}
public function getUrls(): array
{
return [
['loc' => 'https://example.test/a'],
['loc' => 'https://example.test/b'],
];
}
};
$lazy = $contributor->getUrlsLazy();
$this->assertInstanceOf(\Traversable::class, $lazy);
$this->assertSame(
['https://example.test/a', 'https://example.test/b'],
array_column(iterator_to_array($lazy, false), 'loc'),
);
}
/**
* getUrlsLazy() 만 구현한 구현체는 getUrls() 가 그 스트림을 배열로 실체화한다
*/
public function test_array_bridges_to_lazy_when_only_get_urls_lazy_is_implemented(): void
{
$contributor = new class extends AbstractSitemapContributor
{
public function getIdentifier(): string
{
return 'lazy-only';
}
public function getUrlsLazy(): iterable
{
yield ['loc' => 'https://example.test/1'];
yield ['loc' => 'https://example.test/2'];
yield ['loc' => 'https://example.test/3'];
}
};
$urls = $contributor->getUrls();
$this->assertIsArray($urls);
$this->assertSame(
['https://example.test/1', 'https://example.test/2', 'https://example.test/3'],
array_column($urls, 'loc'),
);
}
/**
* getUrlsLazy() 는 실제 지연 제너레이터여야 한다 (전량 실체화 금지)
*
* 소비를 시작하기 전에는 본문이 실행되지 않아야 대용량에서 메모리가 유계로 유지된다.
*/
public function test_get_urls_lazy_is_a_generator_that_defers_execution(): void
{
$contributor = new class extends AbstractSitemapContributor
{
public bool $executed = false;
public function getIdentifier(): string
{
return 'deferred';
}
public function getUrlsLazy(): iterable
{
$this->executed = true;
yield ['loc' => 'https://example.test/x'];
}
};
$lazy = $contributor->getUrlsLazy();
// 아직 순회하지 않았으므로 제너레이터 본문이 실행되지 않았다.
$this->assertFalse($contributor->executed, '순회 전에는 제너레이터 본문이 실행되지 않아야 합니다.');
iterator_to_array($lazy, false);
$this->assertTrue($contributor->executed, '순회하면 제너레이터 본문이 실행되어야 합니다.');
}
/**
* base 는 SitemapContributorInterface 를 구현한다 (기존 등록 경로 호환)
*/
public function test_is_a_sitemap_contributor(): void
{
$contributor = new class extends AbstractSitemapContributor
{
public function getIdentifier(): string
{
return 'iface';
}
public function getUrlsLazy(): iterable
{
yield from [];
}
};
$this->assertInstanceOf(SitemapContributorInterface::class, $contributor);
}
}