PROJECT 2 — CONTACT FORM APPLICATION
Starter files for PHP the TPRM Way, Chapter 14


WHAT IS IN THIS FOLDER

  contact.php            The page. It shows the form and processes the
                         submission, the way Section 10.4 does. Every rule is a
                         TODO.
  contact_functions.php  Validation, record building, and JSON persistence.
                         Every body is a stub that returns a placeholder.
  submissions.json       One stored submission. It is the fixture your loading
                         code reads on the first run, and it is also the record
                         shape your own save has to produce.
  README.txt             This file.

These are starter files, not a solution. Start the server in this folder with

  php -S localhost:8000

and open http://localhost:8000/contact.php. You get the form, and submitting it
tells you that nothing is validated yet.

Running php contact.php from the terminal also works, and it is worth doing after
every edit. There is no request, so the page behaves as a first visit. It is the
fastest way to find a parse error without leaving the terminal, and it is the
reason every request key in this starter is read with a fallback.


WHAT TO BUILD

A contact form for the browser. It validates a request, preserves useful field
values after errors, renders feedback safely, and writes accepted submissions to
a local data file.

If you completed the Chapter 10 mini-project, start from that file. That was one
field, two rules, one page. This is an application: four fields with different
rules, a stored record that outlives the request, and a failure path for when
storage does not work. The three new requirements are not optional, because they
are the ones that make it real.

One stored record looks like this:

  {
    "id": "6f1c9a2b7d4e5081",
    "created_at": "2026-01-06T09:15:02+00:00",
    "name": "Maya",
    "email": "maya@example.com",
    "subject": "Question about Chapter 14",
    "message": "How large should a starter project be?"
  }


BUILD MILESTONES

  1. Create a semantic HTML form with name, email, subject, and message fields.
  2. Read every POST key with an intentional fallback and trim only where your
     rules permit it.
  3. Validate required values, email format, and sensible length limits into an
     $errors array.
  4. Redisplay submitted values with HTML escaping when validation fails.
  5. On success, create a structured record with a generated id and a server
     timestamp: bin2hex(random_bytes(8)) gives a unique id, and date("c") gives
     an ISO-8601 timestamp.
  6. Append the record to the locally stored JSON collection using explicit JSON
     error handling.
  7. Show a success state without echoing unescaped request values.
  8. Test HTML-looking text, missing fields, oversized input, and malformed
     persistence data.

Neither the id nor the timestamp comes from the request, and that is deliberate.
A client that can choose its own record id can overwrite somebody else's record,
and a client that can choose its own timestamp can file a message in the past.
Both are server facts.


ACCEPTANCE TESTS

  Normal               Valid form creates one stored record and a success
                       response.
  Validation           Invalid email and missing fields produce specific errors.
  HTML safety          Markup-looking input is shown as text when redisplayed.
  Persistence failure  A write or read problem produces controlled feedback
                       rather than a false success.

For HTML safety, submit a name of <b>Maya</b> and look at the page source of the
redisplayed form. The value attribute has to read value="&lt;b&gt;Maya&lt;/b&gt;".
An attribute is an output context exactly as page text is, and it is the one
people forget, because escaping usually gets added where a value is displayed
rather than where it is put back into the form after a failure.

For Persistence failure, make the data file unwritable, or point the path at a
folder that does not exist, and confirm that the visitor is told the message was
not stored. A success page over a failed write is the worst outcome available.


A POST IS NOT ENCRYPTION

Treat every request value as untrusted, validate it for its meaning, and escape
it only where it enters an HTML output context. Choosing POST over GET describes
what the request is for. It protects nothing.


STRETCH GOALS

  - Use Post/Redirect/Get after a successful submission, so a refresh does not
    resubmit. It needs header() before any output.
  - Add a CSRF-token exercise after consulting current PHP security guidance.
  - Send mail only in a controlled environment, and only after the storage
    workflow is correct.

A JSON file is a learning store. It is not built for concurrent writers, and the
moment two submissions can arrive at the same moment you want a database, which
is Project 4.
