A PHP comment system with replies needs three things working together: a comment row that points to its parent, prepared SQL statements for every user-supplied value, and context-appropriate output encoding when text is rendered. The pattern below uses PDO, a nullable parent_id, POST/redirect/GET submission, and an in-memory tree for displaying threaded comments.
Choose the reply relationship first
Store each comment in one table. A top-level comment has parent_id = NULL; a reply stores the ID of the comment it answers. Every row also needs to identify the page or article so a reply cannot accidentally appear in another discussion.
| Column | Purpose | Typical value |
|---|---|---|
id |
Unique comment identifier | Integer or database-specific ID |
page_id |
Article, post, or page owning the thread | The current page’s ID |
parent_id |
Immediate parent comment | NULL for a root comment |
author_id or display name |
Author identity | Your authenticated user ID or approved name |
body |
Comment text | Stored as plain text |
created_at |
Creation time | A database timestamp |
This model supports one-level replies or deeper nesting. Whether you allow unlimited depth, impose a maximum, moderate comments, or paginate large threads is an application decision rather than a PHP requirement.
Example table and indexes
The following is a starting point; adapt types and foreign-key syntax to your database engine.
Do these 3 things before closing this tab:
1Fix the driver behind crashes, sound loss and screen glitches2Repair Windows errors before they cause bigger problems3Scan for outdated or missing drivers - takes under a minute#1 Best Overall
CREATE TABLE comments (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
page_id BIGINT UNSIGNED NOT NULL,
parent_id BIGINT UNSIGNED NULL,
author_id BIGINT UNSIGNED NULL,
body TEXT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
INDEX comments_page_parent (page_id, parent_id),
INDEX comments_parent (parent_id)
);
An index beginning with page_id helps retrieve one page’s discussion and then group its replies. Add foreign keys, delete behavior, uniqueness rules, and moderation columns only after deciding how your application handles deleted users, removed comments, and whole reply subtrees.
Build the POST handler with PDO
Use a POST request to create a comment, bind values through a prepared statement, then redirect to the page. PHP’s PDO documentation states that calling PDO::prepare() and PDOStatement::execute() helps prevent SQL injection by avoiding manual quoting and escaping of parameters.
Rank #2
-
Read and validate expected fields
Obtain the page ID, body, and optional parent ID from the request. Validate that IDs are integers in the range your application accepts. Trim the body and reject an empty value or a value beyond your chosen length.
-
Verify the parent belongs to this thread
If a parent ID was submitted, query it by both its ID and the current
page_id. Reject it when no matching row exists. This prevents attaching a reply to a comment from another article.Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy. -
Insert bound values
$sql = 'INSERT INTO comments (page_id, parent_id, author_id, body) VALUES (:page_id, :parent_id, :author_id, :body)'; $stmt = $pdo->prepare($sql); $stmt->execute([ ':page_id' => $pageId, ':parent_id' => $parentId, // NULL for a top-level comment ':author_id' => $authorId, ':body' => $body ]);Placeholders represent complete data literals. They cannot stand in for table names, column names, keywords, or arbitrary SQL fragments, so any dynamic identifier must come from a fixed allow-list rather than request text.
-
Redirect after success
Send a redirect to the page after the insert and terminate the request. The POST/redirect/GET pattern prevents a browser refresh from submitting the same form again.
Rank #4
Do not confuse filtering, validation, escaping, and binding
- Validation asks whether a value has the required type, range, relationship, or format. For example, a parent ID must be an integer and must belong to the current page.
- PDO binding keeps SQL data separate from SQL syntax. Do not replace it with string concatenation or manual quote escaping.
- HTML escaping protects the HTML text context when a stored comment is printed.
- Input filtering is not automatic safety. PHP’s
filter_input()usesFILTER_DEFAULT, an alias ofFILTER_UNSAFE_RAW, unless you explicitly select a filter; retrieving a value does not validate it by itself.
Render comments as a tree
Fetch comments for the current page, index them by ID, and attach each row to its parent’s children. The code below renders arbitrary depth. Add a depth limit if your product requires one.
$stmt = $pdo->prepare(
'SELECT id, parent_id, author_id, body, created_at
FROM comments
WHERE page_id = :page_id
ORDER BY created_at ASC, id ASC'
);
$stmt->execute([':page_id' => $pageId]);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
$byId = [];
$roots = [];
foreach ($rows as $row) {
$row['children'] = [];
$byId[$row['id']] = $row;
}
foreach ($byId as $id => $row) {
if ($row['parent_id'] === null) {
$roots[] = $id;
} elseif (isset($byId[$row['parent_id']])) {
$byId[$row['parent_id']]['children'][] = $id;
}
}
function e(string $value): string {
return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
function renderComments(array $ids, array $byId, int $depth = 0): void {
echo '<ul class="comments">';
foreach ($ids as $id) {
$comment = $byId[$id];
echo '<li class="comment">';
echo '<p>' . nl2br(e($comment['body'])) . '</p>';
echo '<small>' . e($comment['created_at']) . '</small>';
if ($comment['children']) {
renderComments($comment['children'], $byId, $depth + 1);
}
echo '</li>';
}
echo '</ul>';
}
renderComments($roots, $byId);
The helper uses UTF-8, substitutes invalid byte sequences, and escapes quotes as well as angle brackets and ampersands. Set the document and database connection to the encoding your application actually uses. HTML escaping is for HTML text; it does not make a value safe for SQL, a URL, JavaScript, or another output context.
Quick wins for a faster PC:
Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →Repair Windows errors before they cause bigger problemsFix Now →Accept replies safely in the form
Include the parent ID as a hidden field only when the user is replying. Treat it as untrusted input anyway: parse it, verify that it exists on the same page, and apply your rules for locked, deleted, or moderated parents.
<form method="post" action="/comments">
<input type="hidden" name="page_id" value="<?= e((string) $pageId) ?>">
<input type="hidden" name="parent_id" value="<?= e((string) $parentId) ?>">
<textarea name="body" required maxlength="5000"></textarea>
<button type="submit">Post comment</button>
</form>
Protect the endpoint with your normal authentication and authorization checks. If your application uses cookies for login, add CSRF protection; prepared statements and HTML escaping do not address cross-site request forgery.
Decide how much nesting to expose
One-level replies
Allow replies only to root comments and display a flat list of responses beneath each root. This is easiest to moderate and paginate.
Bounded nesting
Store the same parent_id relationship but reject a reply whose ancestor depth exceeds your chosen limit. The limit is a product rule, not a PHP default.
Free tools Windows power users keep installed
One-click scans. No signup required.
Deeper trees
Allow replies to replies and render recursively, as in the example. For large discussions, consider pagination or loading children separately so one request does not build an unbounded tree in memory.
Quick Recap
Common failure modes
- Replies appear at the top level: the insert is writing
NULLinstead of the verified parent ID, or the renderer is ignoringparent_id. - A reply joins the wrong article: the parent lookup checks only
id; require bothidandpage_id. - User text becomes markup: the body is echoed without
htmlspecialchars(). Escape at output, using the actual document encoding. - SQL errors or injection risk: values are concatenated into SQL, or placeholders are being used for identifiers. Bind values and allow-list any dynamic SQL structure.
- Duplicate comments after refresh: the form response renders directly after POST. Redirect after a successful transaction.
- Unexpected accepted input: code assumes
filter_input()validates by default. Select explicit validation rules and enforce relationships in server-side code.
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

