'array', 'top_categories_json' => 'array', 'followed_creator_ids_json' => 'array', 'tag_weights_json' => 'array', 'category_weights_json' => 'array', 'disliked_tag_ids_json' => 'array', ]; // ── Relations ───────────────────────────────────────────────────────────── public function user(): BelongsTo { return $this->belongsTo(User::class); } // ── Helpers ─────────────────────────────────────────────────────────────── /** * Hydrate a DTO from this model's JSON columns. */ public function toDTO(): UserRecoProfileDTO { return new UserRecoProfileDTO( topTagSlugs: (array) ($this->top_tags_json ?? []), topCategorySlugs: (array) ($this->top_categories_json ?? []), strongCreatorIds: array_map('intval', (array) ($this->followed_creator_ids_json ?? [])), tagWeights: array_map('floatval', (array) ($this->tag_weights_json ?? [])), categoryWeights: array_map('floatval', (array) ($this->category_weights_json ?? [])), dislikedTagSlugs: (array) ($this->disliked_tag_ids_json ?? []), ); } /** * True when the stored profile is still within the configured TTL. */ public function isFresh(): bool { if ($this->updated_at === null) { return false; } $ttl = (int) config('recommendations.ttl.user_reco_profile', 6 * 3600); return $this->updated_at->addSeconds($ttl)->isFuture(); } }