feat(link): cache service, create/update/delete actions, slug generation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
main
boban 2026-05-16 00:46:06 +02:00
parent 587aa09b40
commit 69e6318db5
5 changed files with 180 additions and 0 deletions

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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();
});