diff --git a/src/SiteInterface.php b/src/SiteInterface.php index 7b2edba..01403bc 100644 --- a/src/SiteInterface.php +++ b/src/SiteInterface.php @@ -19,11 +19,6 @@ interface SiteInterface { - public static function init( - ContentRoot $fileSystemRoot, - HttpRoot $domain - ): self|false; - public function domain(): HttpRoot; public function contentRoot(): ContentRoot; diff --git a/src/SiteWithRequest.php b/src/SiteWithRequest.php new file mode 100644 index 0000000..d76150e --- /dev/null +++ b/src/SiteWithRequest.php @@ -0,0 +1,211 @@ + + */ + private array $public_metas = []; + + /** + * @var array + */ + private array $publicContents = []; + + public static function init( + ContentRoot $fileSystemRoot, + RequestInterface $request + ): self|false { + return new self($fileSystemRoot, $request); + } + + final private function __construct( + private readonly ContentRoot $fileSystemRoot, + private readonly RequestInterface $request + ) { + } + + public function contentRoot(): ContentRoot + { + return $this->fileSystemRoot; + } + + public function publicRoot(): PublicRoot + { + if (isset($this->fileSystemPublicRoot) === false) { + $this->fileSystemPublicRoot = PublicRoot::inRoot( + $this->contentRoot() + ); + } + return $this->fileSystemPublicRoot; + } + + public function request(): RequestInterface + { + return $this->request; + } + + public function domain(): HttpRoot + { + if (isset($this->domain) === false) { + $this->domain = HttpRoot::fromRequest($this->request()); + } + return $this->domain; + } + + public function hasPublicMeta(Path|false $at = false): bool + { + if ($at === false) { + return $this->publicMeta($at)->toBool(); + } + return $this->publicMeta($at)->toBool(); + } + + public function publicMeta(Path|false $at = false): PublicMeta + { + $at = $this->path($at); + + $key = $at->toString(); + + if (array_key_exists($key, $this->public_metas)) { + return $this->public_metas[$key]; + } + + $meta = PublicMeta::inRoot($this->contentRoot(), $at); + $this->public_metas[$key] = $meta; // TODO: Make a custom collection + + return $meta; + } + + // TODO: Recurring question will be whether "at" separator + // is URI (known) or file system (unknown) + public function hasPublicContent(Path|false $at = false): bool + { + $at = $this->path($at); + + return $this->publicContent($at)->toBool(); + } + + public function publicContent(Path|false $at = false): PublicContent + { + $at = $this->path($at); + + $key = $at->toString(); + + if (array_key_exists($key, $this->publicContents)) { + return $this->publicContents[$key]; + } + + $content = PublicContent::inRoot($this->contentRoot(), $at); + $this->publicContents[$key] = $content; // TODO: convert to custom collection + + return $content; + } + + public function publicFile( + Filename $filename, + Path|false $at = false + ): PublicFile { + $at = $this->path($at); + + return PublicFile::inRoot($this->contentRoot(), $filename, $at); + } + + /** + * @return string[] + */ + public function titles(Path|false $at = false): array + { + $at = $this->path($at); + + return array_values( + $this->linkStack($at) + ); + } + + /** + * @return array + */ + public function breadcrumbs( + Path|false $at = false, + int $offset = 0, + int|false $length = false + ): array { + $at = $this->path($at); + + $sorted = array_reverse( + $this->linkStack($at) + ); + + if ($length === false) { + $length = null; + } + + return array_slice($sorted, $offset, $length); + } + + /** + * @return array + */ + public function linkStack(Path|false $at = false): array + { + $at = $this->path($at); + + $parts = $at->parts(); + + $stack = []; + while (count($parts) > 0) { + $key = '/' . implode('/', $parts) . '/'; + $this->updateStack($key, $stack); + + array_pop($parts); + } + + $this->updateStack('/', $stack); + + return array_filter($stack); + } + + /** + * @param array $stack + */ + private function updateStack(string $key, array &$stack): void + { + $uriPath = Path::fromString($key); + $stack[$key] = $this->publicMeta(at: $uriPath)->title(); + } + + private function path(Path|false $at = false): Path + { + if ($at === false) { + return Path::fromString( + $this->request()->getUri()->getPath() + ); + } + return $at; + } +} diff --git a/tests/SiteWithRequestTest.php b/tests/SiteWithRequestTest.php new file mode 100644 index 0000000..7440713 --- /dev/null +++ b/tests/SiteWithRequestTest.php @@ -0,0 +1,193 @@ +hasPublicContent(); + + $this->assertTrue($result); + + $expected = <<publicContent()->toString(); + + $this->assertSame( + $expected, + $result + ); + } + + /** + * @test + */ + public function has_titles(): void + { + $expected = [ + 'L1 page', + 'Root test content' + ]; + + $result = parent::siteWithRequest('/l1-page')->titles(); + + $this->assertSame( + $expected, + $result + ); + } + + /** + * @test + */ + public function has_expected_link_stack(): void + { + $expected = [ + '/l1-page/l2-page/' => 'L2 page', + '/l1-page/' => 'L1 page', + '/' => 'Root test content' + ]; + + $result = parent::siteWithRequest('/l1-page/l2-page/') + ->linkStack(); + + $this->assertSame( + $expected, + $result + ); + } + + /** + * @test + */ + public function can_build_breadcrumbs_l2(): void + { + $expected = [ + '/' => 'Root test content', + '/l1-page/' => 'L1 page', + '/l1-page/l2-page/' => 'L2 page' + ]; + + $result = parent::siteWithRequest('/l1-page/l2-page') + ->breadcrumbs(); + + $this->assertSame( + $expected, + $result + ); + } + + /** + * @test + */ + public function can_build_breadcrumbs_l4(): void + { + $expected = [ + '/' => 'Root test content', + '/l1-page/' => 'L1 page', + '/l1-page/l2-page/' => 'L2 page', + '/l1-page/l2-page/l3-page/' => 'L3 page', + '/l1-page/l2-page/l3-page/l4-page/' => 'L4 page' + ]; + + $result = parent::siteWithRequest('/l1-page/l2-page/l3-page/l4-page') + ->breadcrumbs(); + + $this->assertSame( + $expected, + $result + ); + } + + /** + * @test + */ + public function can_build_breadcrumbs_can_exclude_levels(): void + { + $expected = [ + // '/' => 'Root test content', + '/l1-page/' => 'L1 page', + '/l1-page/l2-page/' => 'L2 page', + '/l1-page/l2-page/l3-page/' => 'L3 page', + '/l1-page/l2-page/l3-page/l4-page/' => 'L4 page' + ]; + + $result = parent::siteWithRequest('/l1-page/l2-page/l3-page/l4-page') + ->breadcrumbs(offset: 1); + + $this->assertSame( + $expected, + $result + ); + + $expected = [ + // '/' => 'Root test content', + // '/l1-page/' => 'L1 page', + '/l1-page/l2-page/' => 'L2 page', + '/l1-page/l2-page/l3-page/' => 'L3 page', + // '/l1-page/l2-page/l3-page/l4-page/' => 'L4 page' + ]; + + $result = parent::siteWithRequest('/l1-page/l2-page/l3-page/l4-page') + ->breadcrumbs(offset: 2, length: 2); + + $this->assertSame( + $expected, + $result + ); + } + + /** + * @test + */ + public function has_public_meta(): void + { + $result = parent::siteWithRequest()->hasPublicMeta(); + + $this->assertTrue($result); + + $result = parent::siteWithRequest()->publicMeta()->title(); + + $expected = "Root test content"; + + $this->assertEquals( + $expected, + $result + ); + + $result = parent::siteWithRequest()->hasPublicMeta( + Path::fromString('nonexistent') + ); + + $this->assertFalse($result); + } + + /** + * @test + */ + public function has_expected_domain(): void + { + $expected = $this->domain()->toString(); + + $result = parent::siteWithRequest()->domain()->toString(); + + $this->assertSame( + $expected, + $result + ); + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php index 9259fcc..f03ecf8 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -6,6 +6,7 @@ use PHPUnit\Framework\TestCase as BaseTestCase; use Eightfold\Amos\Site; +use Eightfold\Amos\SiteWithRequest; use SplFileInfo; @@ -47,9 +48,17 @@ protected function site(string $path = '/'): Site ); } + protected function siteWithRequest(string $path = '/'): SiteWithRequest + { + return SiteWithRequest::init( + $this->root(), + $this->request($path) + ); + } + protected function request(string $path = '/'): ServerRequest { - return new ServerRequest('get', $this->domain() . $path); + return new ServerRequest('get', $this->domain()->toString() . $path); } protected function domain(): HttpRoot