diff --git a/src/Content/Version.php b/src/Content/Version.php index 5d6613d0b2..5e4dbffa78 100644 --- a/src/Content/Version.php +++ b/src/Content/Version.php @@ -2,7 +2,9 @@ namespace Kirby\Content; +use Kirby\Cms\Language; use Kirby\Cms\ModelWithContent; +use Kirby\Exception\InvalidArgumentException; use Kirby\Exception\NotFoundException; /** @@ -33,39 +35,50 @@ public function content(string $language = 'default'): Content { return new Content( parent: $this->model, - data: $this->model->storage()->read($this->id, $language), + data: $this->model->storage()->read($this->id, $this->language($language)), ); } /** * Creates a new version for the given language + * + * @param array $fields Content fields */ public function create(array $fields, string $language = 'default'): void { - $this->model->storage()->create($this->id, $language, $fields); + $this->model->storage()->create($this->id, $this->language($language), $fields); } /** * Deletes a version by language or for any language + * + * @param string|null $language If null, all available languages will be deleted */ public function delete(string|null $language = null): void { - // delete all languages - if ($language === null) { - foreach ($this->model->kirby()->languages() as $language) { - $this->model->storage()->delete($this->id, $language->code()); - } + // delete a single language + if ($this->model->kirby()->multilang() === false) { + $this->deleteLanguage('default'); } - // delete the default language in single-language mode - if ($this->model->kirby()->multilang() === false) { - $this->model->storage()->delete($this->id, 'default'); + // delete a specific language + if ($language !== null) { + $this->deleteLanguage($language); return; } - // delete a single language - $this->model->storage()->delete($this->id, $language); + // delete all languages + foreach ($this->model->kirby()->languages() as $language) { + $this->deleteLanguage($language); + } + } + /** + * Deletes a version by a specific language + */ + public function deleteLanguage(string $language = 'default'): void + { + $this->model->storage()->delete($this->id, $this->language($language)); } /** @@ -76,10 +89,17 @@ public function delete(string|null $language = null): void */ public function ensure( string $language = 'default' - ): void { + ): bool { if ($this->exists($language) !== true) { - throw new NotFoundException('Version "' . $this->id . ' (' . $language . ')" does not already exist'); + $message = match($this->model->kirby()->multilang()) { + true => 'Version "' . $this->id . ' (' . $language . ')" does not already exist', + false => 'Version "' . $this->id . '" does not already exist', + }; + + throw new NotFoundException($message); } + + return true; } /** @@ -87,7 +107,7 @@ public function ensure( */ public function exists(string $language = 'default'): bool { - return $this->model->storage()->exists($this->id, $language); + return $this->model->storage()->exists($this->id, $this->language($language)); } /** @@ -98,6 +118,27 @@ public function id(): VersionId return $this->id; } + /** + * Converts a "user-facing" language code to a `Language` object + * to be used in storage methods + */ + protected function language( + string|null $languageCode = null, + ): Language { + // single language + if ($this->model->kirby()->multilang() === false) { + return Language::single(); + } + + // look up the actual language object if possible + if ($language = $this->model->kirby()->language($languageCode)) { + return $language; + } + + // validate the language code + throw new InvalidArgumentException('Invalid language: ' . $languageCode); + } + /** * Returns the parent model */ @@ -109,14 +150,12 @@ public function model(): ModelWithContent /** * Returns the modification timestamp of a version * if it exists - * - * @param string $lang Code `'default'` in a single-lang installation */ public function modified( string $language = 'default' ): int|null { $this->ensure($language); - return $this->model->storage()->modified($this->id, $language); + return $this->model->storage()->modified($this->id, $this->language($language)); } /** @@ -125,61 +164,74 @@ public function modified( public function move(string $fromLanguage, VersionId $toVersionId, string $toLanguage): void { $this->ensure($fromLanguage); - $this->model->storage()->move($this->id, $fromLanguage, $toVersionId, $toLanguage); + $this->model->storage()->move( + fromVersionId: $this->id, + fromLanguage: $this->language($fromLanguage), + toVersionId: $toVersionId, + toLanguage: $this->language($toLanguage) + ); } /** * Returns the stored content fields * - * @param string $lang Code `'default'` in a single-lang installation * @return array */ public function read(string $language = 'default'): array { $this->ensure($language); - return $this->model->storage()->read($this->id, $language); + return $this->model->storage()->read($this->id, $this->language($language)); } /** * Updates the modification timestamp of an existing version * - * @param string $lang Code `'default'` in a single-lang installation + * @param string|null $language If null, all available languages will be touched * * @throws \Kirby\Exception\NotFoundException If the version does not exist */ public function touch(string|null $language = null): void { - // touch all languages - if ($language === null) { - foreach ($this->model->kirby()->languages() as $language) { - $this->touch($language->code()); - } + // touch a single language + if ($this->model->kirby()->multilang() === false) { + $this->touchLanguage('default'); + return; } - // make sure the version exists - $this->ensure($language); - - // touch the default language in single-language mode - if ($this->model->kirby()->multilang() === false) { - $this->model->storage()->touch($this->id, 'default'); + // touch a specific language + if ($language !== null) { + $this->touchLanguage($language); return; } - // touch a single language - $this->model->storage()->touch($this->id, $language); + // touch all languages + foreach ($this->model->kirby()->languages() as $language) { + $this->touchLanguage($language); + } + } + + /** + * Updates the modification timestamp of a specific language + * + * @throws \Kirby\Exception\NotFoundException If the version does not exist + */ + public function touchLanguage(string $language = 'default'): void + { + // make sure the version exists + $this->ensure($language); + $this->model->storage()->touch($this->id, $this->language($language)); } /** * Updates the content fields of an existing version * * @param array $fields Content fields - * @param string $lang Code `'default'` in a single-lang installation * * @throws \Kirby\Exception\NotFoundException If the version does not exist */ public function update(array $fields, string $language = 'default'): void { $this->ensure($language); - $this->model->storage()->update($this->id, $language, $fields); + $this->model->storage()->update($this->id, $this->language($language), $fields); } } diff --git a/tests/Content/VersionTest.php b/tests/Content/VersionTest.php index fc476a15ff..ed399886d8 100644 --- a/tests/Content/VersionTest.php +++ b/tests/Content/VersionTest.php @@ -4,6 +4,7 @@ use Kirby\Cms\App; use Kirby\Cms\Page; +use Kirby\Exception\NotFoundException; use Kirby\Filesystem\Dir; use Kirby\TestCase; @@ -14,18 +15,63 @@ class VersionTest extends TestCase { public const TMP = KIRBY_TMP_DIR . '/Content.Version'; + protected $app; protected $model; public function setUp(): void { Dir::make(static::TMP); + } - $this->model = new Page([ - 'kirby' => new App(), - 'root' => static::TMP, - 'slug' => 'a-page', - 'template' => 'article' + public function setUpMultiLanguage(): void + { + $this->app = new App([ + 'languages' => [ + [ + 'code' => 'en', + 'default' => true + ], + [ + 'code' => 'de' + ] + ], + 'roots' => [ + 'index' => static::TMP + ], + 'site' => [ + 'children' => [ + [ + 'slug' => 'a-page', + 'template' => 'article', + ] + ] + ] ]); + + $this->model = $this->app->page('a-page'); + + Dir::make($this->model->root()); + } + + public function setUpSingleLanguage(): void + { + $this->app = new App([ + 'roots' => [ + 'index' => static::TMP + ], + 'site' => [ + 'children' => [ + [ + 'slug' => 'a-page', + 'template' => 'article' + ] + ] + ] + ]); + + $this->model = $this->app->page('a-page'); + + Dir::make($this->model->root()); } public function tearDown(): void @@ -34,11 +80,77 @@ public function tearDown(): void Dir::remove(static::TMP); } + /** + * @covers ::content + */ + public function testContentMultiLanguage(): void + { + $this->setUpMultiLanguage(); + + $version = new Version( + model: $this->model, + id: VersionId::published() + ); + + $version->create([ + 'title' => 'Test' + ], 'en'); + + $version->create([ + 'title' => 'Töst' + ], 'de'); + + $this->assertSame('Test', $version->content('en')->get('title')->value()); + $this->assertSame('Töst', $version->content('de')->get('title')->value()); + } + + /** + * @covers ::content + */ + public function testContentSingleLanguage(): void + { + $this->setUpSingleLanguage(); + + $version = new Version( + model: $this->model, + id: VersionId::published() + ); + + $version->create([ + 'title' => 'Test' + ]); + + $this->assertSame('Test', $version->content()->get('title')->value()); + } + + /** + * @covers ::create + */ + public function testCreateMultiLanguage(): void + { + $this->setUpMultiLanguage(); + + $version = new Version( + model: $this->model, + id: VersionId::published() + ); + + $this->assertFalse($version->exists('de')); + + $version->create([ + 'title' => 'Test' + ], 'de'); + + $this->assertTrue($version->exists('de')); + } + /** * @covers ::create */ - public function testCreate(): void + public function testCreateSingleLanguage(): void { + $this->setUpSingleLanguage(); + $version = new Version( model: $this->model, id: VersionId::published() @@ -54,10 +166,13 @@ public function testCreate(): void } /** - * @covers ::create + * @covers ::delete + * @covers ::deleteLanguage */ - public function testCreateLanguage(): void + public function testDeleteMultiLanguage(): void { + $this->setUpMultiLanguage(); + $version = new Version( model: $this->model, id: VersionId::published() @@ -70,13 +185,20 @@ public function testCreateLanguage(): void ], 'de'); $this->assertTrue($version->exists('de')); + + $version->delete('de'); + + $this->assertFalse($version->exists('de')); } /** * @covers ::delete + * @covers ::deleteLanguage */ - public function testDelete(): void + public function testDeleteSingleLanguage(): void { + $this->setUpSingleLanguage(); + $version = new Version( model: $this->model, id: VersionId::published() @@ -95,11 +217,100 @@ public function testDelete(): void $this->assertFalse($version->exists()); } + /** + * @covers ::ensure + */ + public function testEnsureMultiLanguage(): void + { + $this->setUpMultiLanguage(); + + $version = new Version( + model: $this->model, + id: VersionId::published() + ); + + $version->create([], 'de'); + $this->assertTrue($version->ensure('de')); + } + + /** + * @covers ::ensure + */ + public function testEnsureSingleLanguage(): void + { + $this->setUpSingleLanguage(); + + $version = new Version( + model: $this->model, + id: VersionId::published() + ); + + $version->create([]); + $this->assertTrue($version->ensure()); + } + + /** + * @covers ::ensure + */ + public function testEnsureWhenMissingMultiLanguage(): void + { + $this->setUpMultiLanguage(); + + $version = new Version( + model: $this->model, + id: VersionId::published() + ); + + $this->expectException(NotFoundException::class); + $this->expectExceptionMessage('Version "published (de)" does not already exist'); + + $version->ensure('de'); + } + + /** + * @covers ::ensure + */ + public function testEnsureWhenMissingSingleLanguage(): void + { + $this->setUpSingleLanguage(); + + $version = new Version( + model: $this->model, + id: VersionId::published() + ); + + $this->expectException(NotFoundException::class); + $this->expectExceptionMessage('Version "published" does not already exist'); + + $version->ensure(); + } + /** * @covers ::exists */ - public function testExists(): void + public function testExistsMultiLanguage(): void { + $this->setUpMultiLanguage(); + + $version = new Version( + model: $this->model, + id: VersionId::published() + ); + + $this->assertFalse($version->exists('de')); + + $version->create([], 'de'); + + $this->assertTrue($version->exists('de')); + } + + /** + * @covers ::exists + */ + public function testExistsSingleLanguage(): void + { + $this->setUpSingleLanguage(); + $version = new Version( model: $this->model, id: VersionId::published() @@ -117,6 +328,8 @@ public function testExists(): void */ public function testId(): void { + $this->setUpSingleLanguage(); + $version = new Version( model: $this->model, id: $id = VersionId::published() @@ -130,6 +343,8 @@ public function testId(): void */ public function testModel(): void { + $this->setUpSingleLanguage(); + $version = new Version( model: $this->model, id: VersionId::published() @@ -138,11 +353,71 @@ public function testModel(): void $this->assertSame($this->model, $version->model()); } + /** + * @covers ::modified + */ + public function testModifiedMultiLanguage(): void + { + $this->setUpMultiLanguage(); + + $version = new Version( + model: $this->model, + id: VersionId::published() + ); + + touch($this->model->root() . '/article.de.txt', $modified = 123456); + + $this->assertSame($modified, $version->modified('de')); + } + + /** + * @covers ::modified + */ + public function testModifiedSingleLanguage(): void + { + $this->setUpSingleLanguage(); + + $version = new Version( + model: $this->model, + id: VersionId::published() + ); + + touch($this->model->root() . '/article.txt', $modified = 123456); + + $this->assertSame($modified, $version->modified()); + } + + /** + * @covers ::read + */ + public function testReadMultiLanguage(): void + { + $this->setUpMultiLanguage(); + + $version = new Version( + model: $this->model, + id: VersionId::published() + ); + + $version->create($contentEN = [ + 'title' => 'Test' + ], 'en'); + + $version->create($contentDE = [ + 'title' => 'Töst' + ], 'de'); + + $this->assertSame($contentEN, $version->read('en')); + $this->assertSame($contentDE, $version->read('de')); + } + /** * @covers ::read */ - public function testRead(): void + public function testReadSingleLanguage(): void { + $this->setUpSingleLanguage(); + $version = new Version( model: $this->model, id: VersionId::published() @@ -155,11 +430,88 @@ public function testRead(): void $this->assertSame($content, $version->read()); } + /** + * @covers ::touch + * @covers ::touchLanguage + */ + public function testTouchMultiLanguage(): void + { + $this->setUpMultiLanguage(); + + $version = new Version( + model: $this->model, + id: VersionId::published() + ); + + touch($root = $this->model->root() . '/article.de.txt', 123456); + $this->assertSame(123456, filemtime($root)); + + $minTime = time(); + + $version->touch('de'); + + clearstatcache(); + + $this->assertGreaterThanOrEqual($minTime, filemtime($root)); + } + + /** + * @covers ::touch + * @covers ::touchLanguage + */ + public function testTouchSingleLanguage(): void + { + $this->setUpSingleLanguage(); + + $version = new Version( + model: $this->model, + id: VersionId::published() + ); + + touch($root = $this->model->root() . '/article.txt', 123456); + $this->assertSame(123456, filemtime($root)); + + $minTime = time(); + + $version->touch(); + + clearstatcache(); + + $this->assertGreaterThanOrEqual($minTime, filemtime($root)); + } + + /** + * @covers ::update + */ + public function testUpdateMultiLanguage(): void + { + $this->setUpMultiLanguage(); + + $version = new Version( + model: $this->model, + id: VersionId::published() + ); + + $version->create([ + 'title' => 'Test' + ], 'de'); + + $this->assertSame('Test', $version->read('de')['title']); + + $version->update([ + 'title' => 'Updated Title' + ], 'de'); + + $this->assertSame('Updated Title', $version->read('de')['title']); + } + /** * @covers ::update */ - public function testUpdate(): void + public function testUpdateSingleLanguage(): void { + $this->setUpSingleLanguage(); + $version = new Version( model: $this->model, id: VersionId::published() @@ -171,7 +523,7 @@ public function testUpdate(): void $this->assertSame('Test', $version->read()['title']); - $version->create([ + $version->update([ 'title' => 'Updated Title' ]);