Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 33 additions & 6 deletions src/Http/Controllers/CP/Collections/LocalizeEntryController.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@

namespace Statamic\Http\Controllers\CP\Collections;

use Illuminate\Cache\NoLock;
use Illuminate\Contracts\Cache\LockProvider;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Statamic\Facades\User;
use Statamic\Http\Controllers\CP\CpController;

Expand All @@ -14,13 +17,37 @@ public function __invoke(Request $request, $collection, $entry)

$request->validate(['site' => 'required']);

$localized = $entry->makeLocalization($site = $request->site);
$site = $request->site;

$localized->store(['user' => User::fromUser($request->user())]);
// The lock is keyed on the root entry, so concurrent requests from any
// localization in the origin chain wait for each other.
return $this->lock("localize-entry:{$entry->root()->id()}:{$site}", 10)->block(5, function () use ($request, $entry, $site) {
// Look through the whole origin chain, since the origin of a
// localization can be any existing localization.
$existing = $entry->in($site);

return [
'handle' => $site,
'url' => $localized->editUrl(),
];
if ($existing) {
return [
'handle' => $site,
'url' => $existing->editUrl(),
];
}

$localized = $entry->makeLocalization($site);

$localized->store(['user' => User::fromUser($request->user())]);

return [
'handle' => $site,
'url' => $localized->editUrl(),
];
});
}

private function lock(string $key, int $seconds)
{
return Cache::getStore() instanceof LockProvider
? Cache::lock($key, $seconds)
: new NoLock($key, $seconds);
}
}
116 changes: 116 additions & 0 deletions tests/Feature/Entries/LocalizeEntryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
namespace Tests\Feature\Entries;

use Facades\Tests\Factories\EntryFactory;
use Illuminate\Contracts\Cache\LockProvider;
use Illuminate\Contracts\Cache\Store;
use Illuminate\Support\Facades\Cache;
use PHPUnit\Framework\Attributes\Test;
use Statamic\Facades\Collection;
use Statamic\Facades\User;
Expand Down Expand Up @@ -51,6 +54,118 @@ public function it_localizes_an_entry()
$this->assertCount(0, $entry->in('fr')->revisions());
}

#[Test]
public function it_returns_the_existing_localization_instead_of_creating_another()
{
$entry = EntryFactory::collection(tap(Collection::make('blog')->revisionsEnabled(false))->save())->id('en-test')->slug('test')->create();
$existing = EntryFactory::collection('blog')->id('fr-test')->locale('fr')->origin('en-test')->slug('test')->create();

$this
->actingAs($this->user())
->localize($entry, ['site' => 'fr'])
->assertOk()
->assertJson(['handle' => 'fr', 'url' => $existing->editUrl()]);

$this->assertCount(1, \Statamic\Facades\Entry::query()->where('origin', 'en-test')->where('site', 'fr')->get());
}

#[Test]
public function it_returns_an_existing_localization_from_elsewhere_in_the_origin_chain()
{
$this->setSites([
'en' => ['url' => 'http://localhost/', 'locale' => 'en'],
'de' => ['url' => 'http://localhost/de/', 'locale' => 'de'],
'fr' => ['url' => 'http://localhost/fr/', 'locale' => 'fr'],
]);

$en = EntryFactory::collection(tap(Collection::make('blog')->revisionsEnabled(false))->save())->id('en-test')->slug('test')->create();
$de = EntryFactory::collection('blog')->id('de-test')->locale('de')->origin('en-test')->slug('test')->create();

$first = $this->actingAs($this->user())->localize($de, ['site' => 'fr'])->assertOk();
$second = $this->actingAs($this->user())->localize($en, ['site' => 'fr'])->assertOk();

$this->assertSame($first->json(), $second->json());
$this->assertCount(1, \Statamic\Facades\Entry::query()->where('collection', 'blog')->where('site', 'fr')->get());
}

#[Test]
public function it_localizes_an_entry_when_the_cache_store_does_not_support_locks()
{
Cache::extend('nolock', fn ($app) => Cache::repository(new class(Cache::store('file')->getStore()) implements Store
{
public function __construct(private Store $store)
{
}

public function get($key)
{
return $this->store->get($key);
}

public function many(array $keys)
{
return $this->store->many($keys);
}

public function put($key, $value, $seconds)
{
return $this->store->put($key, $value, $seconds);
}

public function putMany(array $values, $seconds)
{
return $this->store->putMany($values, $seconds);
}

public function increment($key, $value = 1)
{
return $this->store->increment($key, $value);
}

public function decrement($key, $value = 1)
{
return $this->store->decrement($key, $value);
}

public function forever($key, $value)
{
return $this->store->forever($key, $value);
}

public function touch($key, $seconds)
{
return $this->store->touch($key, $seconds);
}

public function forget($key)
{
return $this->store->forget($key);
}

public function flush()
{
return $this->store->flush();
}

public function getPrefix()
{
return $this->store->getPrefix();
}
}));

config(['cache.stores.nolock' => ['driver' => 'nolock'], 'cache.default' => 'nolock']);
$this->assertNotInstanceOf(LockProvider::class, Cache::getStore());

$entry = EntryFactory::collection(tap(Collection::make('blog')->revisionsEnabled(false))->save())->slug('test')->create();

$this
->actingAs($this->user())
->localize($entry, ['site' => 'fr'])
->assertOk();

$this->assertNotNull($entry->fresh()->in('fr'));
}

#[Test]
public function site_is_required()
{
Expand Down Expand Up @@ -255,6 +370,7 @@ private function user()
'access cp',
'access en site',
'access fr site',
'access de site',
'view blog entries',
'edit blog entries',
'view pages entries',
Expand Down
Loading