marque/parley

Polymorphic threaded discussion for Marque — torrent comments and forum-lite from one data model

Maintainers

Package info

github.com/letterofmarque/parley

Issues

pkg:composer/marque/parley

Transparency log

Statistics

Installs: 4

Dependents: 1

Suggesters: 1

Stars: 0

v2.0.0 2026-08-20 09:29 UTC

This package is auto-updated.

Last update: 2026-08-20 09:41:39 UTC


README

Polymorphic threaded discussion for the Marque tracker platform. Torrent comments and a lightweight forum, from one data model.

  threads
    ├─ threadable set, no title    →  comments on a torrent, or anything else
    ├─ threadable null, has title  →  a forum thread
    └─ pinned + locked, no title   →  an announcement

Why

Comments and a forum look like two features. They are one: a title-less, subject-attached thread and a titled, categorised thread are the same row shape with different columns set. Building that as a single polymorphic model means a comment box and a forum share every line of posting, replying, editing and moderation logic — nothing is implemented twice, and anything that later wants discussion (requests, uploads, user profiles) gets it by attaching to the same threads table.

Parley does not implement its own text formatting. Post bodies go through marque/squidink — parley stores source text and the name of the parser that wrote it, and renders through squidink's pipeline. A post and a torrent description behave identically, and the site owner's choice of input syntax applies everywhere.

Installation

composer require marque/parley

Publish the config if you want to change anything:

php artisan vendor:publish --tag=parley-config

Requires marque/trove (user model, roles) and marque/ise (Blade components) — both are composer required automatically as dependencies.

Attaching discussion to a model you own

Add the HasThreads trait to any Eloquent model:

use Marque\Parley\Concerns\HasThreads;

class Torrent extends Model
{
    use HasThreads;
}
$torrent->comments($userId);   // the comment thread, created on first use
$torrent->commentCount();
$torrent->hasComments();
$torrent->latestComment();

The thread is created lazily — reading it never creates a row, so a torrent nobody has commented on yet holds no thread. Only posting does.

Attaching to a model you don't own

If the model belongs to a package that can't take a dependency on parley — the situation guise is in with trove's Torrent, since trove is Marque's one mandatory package — use ThreadServiceInterface::threadFor() instead of the trait. It resolves the same thread by morph class and key, without requiring HasThreads on the model at all:

use Marque\Parley\Contracts\ThreadServiceInterface;

$thread = app(ThreadServiceInterface::class)->threadFor($torrent, auth()->user());

See docs/integration.md in the monorepo for the full reasoning — it covers this pattern generally, for any optional package attaching to a model it doesn't own.

The comment thread component

One Livewire component serves both presentations — a torrent's comments and a forum thread's replies are the same posting, replying, editing and deleting mechanics on the same models, so there is no second implementation to keep in sync.

{{-- Comments: pass the subject, the thread resolves or is created lazily --}}
<livewire:parley-comment-thread :subject="$torrent" />

{{-- Forum: pass the thread directly, it always already exists --}}
<livewire:parley-comment-thread :thread="$thread" />

Handles the full post tree — arbitrary-depth nested replies, submit, reply, edit, delete, all policy-gated — with an empty state and permission-aware controls. Soft-deleted posts render as [deleted] rather than disappearing, so a reply chain survives its parent's removal.

Indentation caps at config('parley.nesting.indent_depth'); nesting itself has no limit in storage. Changing the cap is a display decision, not a migration.

The forum

Behind a config toggle — comments and the forum share one set of tables, so switching the forum off is a route/UI choice, not an install choice:

'forum' => [
    'enabled' => env('PARLEY_FORUM', true),
],

When off, the forum's routes are not registered at all — a comments-only deployment 404s the forum rather than merely lacking a link to it.

Four pages, each a full Livewire component:

Component Route name Purpose
CategoryIndex parley.forum.categories.index Every category, thread count
ThreadIndex parley.forum.categories.show Threads in a category, pinned first
ThreadShow parley.forum.threads.show One thread — title, moderation, posts
ThreadCreate parley.forum.threads.create New thread: title + first post

Categories are created directly against the Category model — there's no admin UI in the package; manage them the way you manage any other reference data in your app.

Moderation

Pin, lock and soft-delete, keyed off marque/trove's roles:

'moderation' => [
    'role' => 'moderator',   // the minimum trove role that can moderate
],

Authors can always edit and delete their own posts and threads. Pin and lock are moderator-only regardless of ownership. Locking a thread is enforced at the service layer (ThreadLockedException), not just hidden in the UI, so it holds against any caller — a REST endpoint, a queued job, a script — not only the component that greys out its submit button.

Rate limiting

Not included. Flood control for user-generated content is a deployment-specific decision — see the package's issue tracker if you're deciding this for a public-facing install.

What's out of scope

Private messages, reputation, badges, signatures, polls, rich moderation queues, and full-text search beyond a basic LIKE. The moment any of these is genuinely wanted, a dedicated forum platform (Discourse, etc.) is the better answer — parley is not trying to compete with one.

Testing

composer test

License

MIT