Files
isaacclad f45fdcd159
Deploy Ladill Link / deploy (push) Successful in 1m26s
Add Campaigns module for grouping short links.
Let accounts organise links by promo or launch, attach or detach links, and view combined click totals from the sidebar.
2026-07-16 20:43:11 +00:00

162 lines
4.6 KiB
PHP

<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Http\Request;
class ShortLink extends Model
{
protected $fillable = [
'user_id',
'campaign_id',
'source_app',
'source_kind',
'source_ref',
'slug',
'label',
'destination_url',
'manage_url',
'is_managed_here',
'is_active',
'clicks_total',
'unique_clicks_total',
'last_clicked_at',
'expires_at',
];
protected $casts = [
'is_managed_here' => 'boolean',
'is_active' => 'boolean',
'clicks_total' => 'integer',
'unique_clicks_total' => 'integer',
'last_clicked_at' => 'datetime',
'expires_at' => 'datetime',
];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function campaign(): BelongsTo
{
return $this->belongsTo(Campaign::class);
}
public function clicks(): HasMany
{
return $this->hasMany(LinkClick::class);
}
/**
* Shareable short URL for this slug — always derived at runtime from the
* owner's default domain (custom or platform), never stored.
*/
public function publicUrl(?User $owner = null): string
{
$owner ??= $this->user;
$base = $owner ? $owner->publicLinkBaseUrl() : self::publicBaseUrl();
return rtrim($base, '/').'/'.$this->slug;
}
public static function publicHost(): string
{
return strtolower((string) config('link.public_domain', 'ladl.link'));
}
public static function publicBaseUrl(): string
{
return 'https://'.self::publicHost();
}
/**
* Build the live redirect target: current destination_url plus any path
* suffix and query string from the inbound request (query keys on the
* short link override destination keys when both are set).
*/
public function resolveRedirectUrl(Request $request, ?string $path = null): string
{
$base = trim((string) $this->destination_url);
if ($base === '') {
return $this->publicUrl();
}
$suffix = $path !== null ? trim($path, '/') : '';
if ($suffix !== '') {
$parsed = parse_url($base);
$basePath = rtrim((string) ($parsed['path'] ?? ''), '/');
$rebuilt = ($parsed['scheme'] ?? 'https').'://'.($parsed['host'] ?? '');
if (! empty($parsed['port'])) {
$rebuilt .= ':'.$parsed['port'];
}
$rebuilt .= $basePath.'/'.$suffix;
if (! empty($parsed['query'])) {
$rebuilt .= '?'.$parsed['query'];
}
if (! empty($parsed['fragment'])) {
$rebuilt .= '#'.$parsed['fragment'];
}
$base = $rebuilt;
}
$requestQuery = $request->query();
if ($requestQuery === []) {
return $base;
}
$parts = parse_url($base);
$existing = [];
if (! empty($parts['query'])) {
parse_str((string) $parts['query'], $existing);
}
// Inbound short-link params win so campaigns can override destination defaults.
$merged = array_merge($existing, $requestQuery);
$query = http_build_query($merged);
$target = ($parts['scheme'] ?? 'https').'://'.($parts['host'] ?? '');
if (! empty($parts['port'])) {
$target .= ':'.$parts['port'];
}
$target .= $parts['path'] ?? '';
if ($query !== '') {
$target .= '?'.$query;
}
if (! empty($parts['fragment'])) {
$target .= '#'.$parts['fragment'];
}
return $target;
}
public function isExpired(): bool
{
return $this->expires_at !== null && $this->expires_at->isPast();
}
/** Simple URL redirects created in Link; QR ecosystem slugs resolve via the platform proxy. */
public function isDirectRedirect(): bool
{
return $this->is_managed_here && $this->source_kind === 'redirect';
}
public function sourceAppLabel(): string
{
return match ($this->source_app) {
'link' => 'Ladill Link',
'qrplus' => 'QR Plus',
'merchant' => 'Merchant',
'events' => 'Events',
'mini' => 'Mini',
'give' => 'Give',
'transfer' => 'Transfer',
'platform' => 'Ladill',
default => ucfirst((string) $this->source_app),
};
}
}