Files
ladill-link/app/Services/Link/LinkManagerService.php
isaaccladandCursor f1fe749934
Deploy Ladill Link / deploy (push) Successful in 29s
Aggregate ladl.link slugs from all platform apps in the link list.
Sync QR and storefront short codes from sibling apps via a platform catalog API so My Links shows every ladl.link URL in one place.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-27 13:26:16 +00:00

115 lines
3.5 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Services\Link;
use App\Models\LinkClick;
use App\Models\LinkWallet;
use App\Models\ShortLink;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use RuntimeException;
class LinkManagerService
{
public function __construct(
private LinkBillingService $billing,
private LinkClickRecorder $clickRecorder,
) {}
public function create(User $user, array $data): ShortLink
{
if (! $this->billing->canCreate($user)) {
throw new RuntimeException('Insufficient wallet balance. Top up to create links.');
}
return DB::transaction(function () use ($user, $data) {
$slug = $this->resolveSlug($data['custom_slug'] ?? null);
$wallet = $user->getOrCreateLinkWallet();
$link = ShortLink::create([
'user_id' => $user->id,
'source_app' => 'link',
'source_kind' => 'redirect',
'is_managed_here' => true,
'slug' => $slug,
'label' => $data['label'] ?? null,
'destination_url' => $this->normalizeUrl($data['destination_url']),
'expires_at' => $data['expires_at'] ?? null,
]);
$this->billing->debitForLinkCreation($wallet, $link);
return $link;
});
}
public function update(ShortLink $link, array $data): ShortLink
{
$link->fill([
'label' => $data['label'] ?? $link->label,
'destination_url' => isset($data['destination_url'])
? $this->normalizeUrl($data['destination_url'])
: $link->destination_url,
'is_active' => $data['is_active'] ?? $link->is_active,
'expires_at' => array_key_exists('expires_at', $data) ? $data['expires_at'] : $link->expires_at,
])->save();
return $link->fresh();
}
public function resolve(Request $request, string $slug): ?ShortLink
{
$link = ShortLink::query()
->where('slug', $slug)
->where('is_active', true)
->first();
if ($link === null || $link->isExpired()) {
return null;
}
$this->clickRecorder->record($link, $request);
return $link;
}
private function resolveSlug(?string $custom): string
{
if ($custom !== null && $custom !== '') {
$custom = strtolower(trim($custom));
if (! preg_match('/^[a-z0-9][a-z0-9-]{1,18}[a-z0-9]$/', $custom)) {
throw new RuntimeException('Custom slug must be 320 characters: lowercase letters, numbers, and hyphens.');
}
if (ShortLink::where('slug', $custom)->exists()) {
throw new RuntimeException('That slug is already taken.');
}
return $custom;
}
do {
$slug = Str::lower(Str::random((int) config('link.slug_length', 8)));
} while (ShortLink::where('slug', $slug)->exists());
return $slug;
}
private function normalizeUrl(string $url): string
{
$url = trim($url);
if ($url === '') {
throw new RuntimeException('Destination URL is required.');
}
if (! preg_match('#^https?://#i', $url)) {
$url = 'https://'.$url;
}
if (! filter_var($url, FILTER_VALIDATE_URL)) {
throw new RuntimeException('Enter a valid URL.');
}
return $url;
}
}