Receiving a user's registration submission and inserting row into database #2 I asked this question before and mickmackusa and Your Common Sense gave me some good answers. I went over them and made as much changes as I could because some of the code didn't work properly for me. So again i want to know if this is a good way to have my registration code for people to sign up.
//check if form is submitted
if ( $_SERVER['REQUEST_METHOD'] != 'POST' || ! isset($_POST['Register'])) {

    // looks like a hack, send to index.php
    header('Location: ../../index.php');
    die();
}

require '../../config/connect.php';
$con = new mysqli(...$dbCredentials);

$first_name = $_POST['first_name'] ?? '';
$last_name = $_POST['last_name'] ?? '';
$username = $_POST['username'] ?? '';
$email = $_POST['email'] ?? '';
$pw = $_POST['pw'] ?? '';
$pw2 = $_POST['pw2'] ?? '';

$errors = [];

if (!trim($first_name)) {

    $errors[] = &quot;Fill in first name to sign up&quot;;
}
if (!ctype_alnum($first_name)) {
    $errors[] = &quot;Invalid first name, it only may contain letters or digits&quot;;
}

if (!trim($last_name)) {

    $errors[] = &quot;Fill in last name to sign up&quot;;
}
if (!ctype_alnum($last_name)) {
    $errors[] = &quot;Invalid last name, only letters and/or digits.&quot;;
}

if (!trim($username)) {

    $errors[] = &quot;Fill in last name to sign up&quot;;
}
if (!ctype_alnum($username)) {
    $errors[] = &quot;Invalid last name, only letters and/or digits.&quot;;
}

if (!trim($pw)) {
    $errors[] = &quot;Fill in password to sign up&quot;;
}
if (!trim($pw2)) {
    $errors[] = &quot;Confirm password to sign up&quot;;
}

if (!trim($email)) {
    $errors[] = &quot;Fill in email to sign up&quot;;
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
    $errors[] = &quot;Invalid email&quot;;
}

if ($pw !== $pw2) {

    echo &quot;Your passwords do not match&quot;;
}

if (!$errors) {

    $check_email = $con-&gt;prepare(&quot;SELECT username FROM users WHERE username=?&quot;);
    $check_email-&gt;bind_param(&quot;s&quot;, $username);
    $check_email-&gt;execute();
    $row = $check_email-&gt;get_result()-&gt;fetch_assoc();

    if ($row) {

        echo &quot;That username is already in use&quot;;
        die();
    }
}

if (!$errors) {

    $check_username = $con-&gt;prepare(&quot;SELECT email FROM users WHERE email=?&quot;);
    $check_username-&gt;bind_param(&quot;s&quot;, $email);
    $check_username-&gt;execute();
    $row = $check_username-&gt;get_result()-&gt;fetch_assoc();
    
    if ($row) {
        echo &quot;That email is already in use&quot;;
        die();
    }
}

if (!$errors) {

    $pw = password_hash($_POST['pw'], PASSWORD_BCRYPT, array('cost' =&gt; 14));

    $stmt = $con-&gt;prepare(&quot;INSERT INTO users (first_name, last_name, username, email, pw)
        VALUES (?, ?, ?, ?, ?)&quot;);
    $stmt-&gt;bind_param(&quot;sssss&quot;, $first_name, $last_name, $username, $email, $pw);
    $stmt-&gt;execute();
    
    $_SESSION[&quot;id&quot;] = $_POST['username'];
    header(&quot;Location: ../../index.php&quot;);
    exit();

} else {
    // The foreach construct provides an easy way to iterate over arrays. 
    foreach ($errors as $error) {
        $errors[] = 'An error occurred.';
    }
    
}
While it is sensible to some people to precisely specify the cause of the submission failure, I prefer to return compound messages rather than a length error message and a quality error message.  I would probably save on screen space (versus piling upto a dozen separate email messages on the user), and merge some responses like:

First name is a required field and may only contain letters and/or digits.

Also, you are repeating yourself a few times with these error messages and you can easily implement a looped battery of checks -- and it is simpler to do that within the $_POST array.  Not only does this avoid some code bloat, you are assured to have consistent error message and reduce your chances of copy-pasting typos.  (Maybe you didn't realize that you wrote last name in the username errors  -- this is the kind of thing that really perplexes users!)
Note: ctype_alnum() will return false on a zero-length string so the !trim() can be omitted.
$alnums = ['first_name', 'last_name', 'username'];  // whitelist of required alphanumeric fields
foreach ($alnums as $field) {
    if (!isset($_POST[$field]) || !ctype_alnum($_POST[$field])) { 
        $errors[] = &quot;$field is a required field and may only contain letters and/or digits.&quot;;
    }
}

if (!isset($_POST['email']) || !filter_var($_POST['email'], FILTER_VALIDATE_EMAIL)) {
    $errors[] = &quot;Email is a required field and must be valid.&quot;;
}

if (!isset($_POST['pw'], $_POST['pw2']) || $pw !== $pw2) {
    $errors[] = &quot;Password and Password 2 are required fields and must be identical.&quot;;
}

Ultimately, all of these server-side checks are to protect you and your database -- NOT to help your users.  For the best possible user experience (UX), you need to duplicate all of these validations with javascript onsubmit of the form.  This way you inform the user as quickly as possible AND avoid making a fruitless trip to your server.
You can check for unique usernames and email addresses in a single trip to the database.  Because the earlier validation assures no leading or trailing whitespace, the posted data will match the respective result set values (iow, no trimming is needed).
if (!$errors) {
    $stmt = $con-&gt;prepare(&quot;SELECT username, email FROM users WHERE username=? OR email=?&quot;);
    $stmt-&gt;bind_param(&quot;ss&quot;, $_POST['username'], $_POST['email']);
    $stmt-&gt;execute();
    foreach ($stmt-&gt;get_result() as $row) {
        foreach (['username', 'email'] as $field) {
            if ($row[$field] == $_POST[$field]) {
                $errors[] = &quot;Sorry, the submitted $field is already in use.&quot;;
            }
        }
    }
}

Once you have fully validated all incoming data as valid, THEN you can happily insert the new user into your database and save the SESSION data.  By the way, I don't recommend changing the naming convention from username to id.
If any of the checkpoints are failed then present your $errors.  As already pointed out, you are not actually displaying your error messages.  In simplest terms, you could create &lt;div&gt; tags as you loop.
if (!$errors) {
    $pw = password_hash($_POST['pw'], PASSWORD_BCRYPT, ['cost' =&gt; 14]);
    $stmt = $con-&gt;prepare(&quot;INSERT INTO users (first_name, last_name, username, email, pw) VALUES (?, ?, ?, ?, ?)&quot;);
    $stmt-&gt;bind_param(
        &quot;sssss&quot;,
        $_POST['first_name'],
        $_POST['last_name'],
        $_POST['username'],
        $_POST['email'],
        $_POST['pw']
    );
    $stmt-&gt;execute();
    
    $_SESSION[&quot;username&quot;] = $_POST['username'];  // I always expect an id to be an integer
    header(&quot;Location: ../../index.php&quot;);
    exit();
}

foreach ($errors as $error) {
    echo &quot;&lt;div class=\&quot;error\&quot;&gt;$error&lt;/div&gt;&quot;;
}

None of the above snippets have been tested; I make no guarantees that they will work &quot;out of the box&quot;.