feat(link): cache service, create/update/delete actions, slug generation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>main
parent
587aa09b40
commit
69e6318db5
|
|
@ -0,0 +1,53 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Link\Actions;
|
||||||
|
|
||||||
|
use App\Domains\Link\Models\Link;
|
||||||
|
use App\Domains\Link\Services\LinkCacheService;
|
||||||
|
use App\Domains\Workspace\Models\Workspace;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class CreateLink
|
||||||
|
{
|
||||||
|
public function __construct(private LinkCacheService $cache) {}
|
||||||
|
|
||||||
|
public function handle(Workspace $workspace, User $creator, array $data): Link
|
||||||
|
{
|
||||||
|
$slug = $data['slug'] ?? $this->generateSlug($workspace->id, $data['domain_id'] ?? null);
|
||||||
|
|
||||||
|
$link = Link::create([
|
||||||
|
'workspace_id' => $workspace->id,
|
||||||
|
'domain_id' => $data['domain_id'] ?? null,
|
||||||
|
'slug' => $slug,
|
||||||
|
'target_url' => $data['target_url'],
|
||||||
|
'title' => $data['title'] ?? null,
|
||||||
|
'description' => $data['description'] ?? null,
|
||||||
|
'status' => 'active',
|
||||||
|
'expires_at' => $data['expires_at'] ?? null,
|
||||||
|
'click_limit' => $data['click_limit'] ?? null,
|
||||||
|
'rules' => $data['rules'] ?? null,
|
||||||
|
'pixel_config' => $data['pixel_config'] ?? null,
|
||||||
|
'tags' => $data['tags'] ?? null,
|
||||||
|
'created_by' => $creator->id,
|
||||||
|
'updated_by' => $creator->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->cache->warmFromLink($link);
|
||||||
|
|
||||||
|
return $link;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function generateSlug(int $workspaceId, ?int $domainId, int $length = 6): string
|
||||||
|
{
|
||||||
|
do {
|
||||||
|
$slug = Str::random($length);
|
||||||
|
$exists = Link::where('slug', $slug)
|
||||||
|
->where('workspace_id', $workspaceId)
|
||||||
|
->where('domain_id', $domainId)
|
||||||
|
->exists();
|
||||||
|
} while ($exists);
|
||||||
|
|
||||||
|
return $slug;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Link\Actions;
|
||||||
|
|
||||||
|
use App\Domains\Link\Models\Link;
|
||||||
|
use App\Domains\Link\Services\LinkCacheService;
|
||||||
|
|
||||||
|
class DeleteLink
|
||||||
|
{
|
||||||
|
public function __construct(private LinkCacheService $cache) {}
|
||||||
|
|
||||||
|
public function handle(Link $link): void
|
||||||
|
{
|
||||||
|
$this->cache->forget($link->slug, $link->domain_id);
|
||||||
|
$link->delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Link\Actions;
|
||||||
|
|
||||||
|
use App\Domains\Link\Models\Link;
|
||||||
|
use App\Domains\Link\Services\LinkCacheService;
|
||||||
|
use App\Models\User;
|
||||||
|
|
||||||
|
class UpdateLink
|
||||||
|
{
|
||||||
|
public function __construct(private LinkCacheService $cache) {}
|
||||||
|
|
||||||
|
public function handle(Link $link, User $updater, array $data): Link
|
||||||
|
{
|
||||||
|
$link->update([
|
||||||
|
'target_url' => $data['target_url'] ?? $link->target_url,
|
||||||
|
'title' => $data['title'] ?? $link->title,
|
||||||
|
'description' => $data['description'] ?? $link->description,
|
||||||
|
'status' => $data['status'] ?? $link->status,
|
||||||
|
'expires_at' => $data['expires_at'] ?? $link->expires_at,
|
||||||
|
'click_limit' => $data['click_limit'] ?? $link->click_limit,
|
||||||
|
'rules' => $data['rules'] ?? $link->rules,
|
||||||
|
'pixel_config' => $data['pixel_config'] ?? $link->pixel_config,
|
||||||
|
'tags' => $data['tags'] ?? $link->tags,
|
||||||
|
'updated_by' => $updater->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->cache->forget($link->slug, $link->domain_id);
|
||||||
|
$this->cache->warmFromLink($link->fresh());
|
||||||
|
|
||||||
|
return $link->fresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Link\Services;
|
||||||
|
|
||||||
|
use App\Domains\Link\Models\Link;
|
||||||
|
use Illuminate\Support\Facades\Redis;
|
||||||
|
|
||||||
|
class LinkCacheService
|
||||||
|
{
|
||||||
|
private string $prefix = 'link:';
|
||||||
|
|
||||||
|
public function key(string $slug, ?int $domainId = null): string
|
||||||
|
{
|
||||||
|
return $domainId
|
||||||
|
? "{$this->prefix}{$domainId}:{$slug}"
|
||||||
|
: "{$this->prefix}{$slug}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public function put(string $slug, array $data, ?int $domainId = null): void
|
||||||
|
{
|
||||||
|
$key = $this->key($slug, $domainId);
|
||||||
|
Redis::hMSet($key, $data);
|
||||||
|
Redis::expire($key, 86400);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get(string $slug, ?int $domainId = null): ?array
|
||||||
|
{
|
||||||
|
$key = $this->key($slug, $domainId);
|
||||||
|
$data = Redis::hGetAll($key);
|
||||||
|
return empty($data) ? null : $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function forget(string $slug, ?int $domainId = null): void
|
||||||
|
{
|
||||||
|
Redis::del($this->key($slug, $domainId));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function warmFromLink(Link $link): void
|
||||||
|
{
|
||||||
|
$this->put($link->slug, [
|
||||||
|
'target' => $link->target_url,
|
||||||
|
'status' => $link->status,
|
||||||
|
'rules_json' => $link->rules ? json_encode($link->rules) : '',
|
||||||
|
'expires_at' => $link->expires_at?->timestamp ?? '',
|
||||||
|
'workspace_id' => (string) $link->workspace_id,
|
||||||
|
'pixel_config' => $link->pixel_config ? json_encode($link->pixel_config) : '',
|
||||||
|
'link_id' => (string) $link->id,
|
||||||
|
], $link->domain_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Domains\Link\Services\LinkCacheService;
|
||||||
|
use Illuminate\Support\Facades\Redis;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
uses(TestCase::class);
|
||||||
|
|
||||||
|
it('stores link data in redis hash', function () {
|
||||||
|
$service = new LinkCacheService;
|
||||||
|
|
||||||
|
$service->put('testslug', [
|
||||||
|
'target' => 'https://example.com',
|
||||||
|
'status' => 'active',
|
||||||
|
'workspace_id' => 1,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$data = $service->get('testslug');
|
||||||
|
|
||||||
|
expect($data['target'])->toBe('https://example.com')
|
||||||
|
->and($data['status'])->toBe('active');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null for unknown slug', function () {
|
||||||
|
$service = new LinkCacheService;
|
||||||
|
expect($service->get('nonexistent-' . uniqid()))->toBeNull();
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue