Skip to content

PHP Tutorial Database CRUD

Create Records

Lesson 271 of 369 2 min read By Deepak Paswan Updated Oct 5, 2026

Create has two parts: show a form, then process what the visitor sends.

The repository method

index.php
PHP
<?php
final class ProductRepository
{
    public function __construct(private PDO $pdo) {}

    public function create(string $name, float $price, int $stock, string $category): int
    {
        $statement = $this->pdo->prepare(
            "INSERT INTO products (name, price, stock, category) VALUES (:name, :price, :stock, :category)"
        );
        $statement->execute([
            "name" => $name,
            "price" => $price,
            "stock" => $stock,
            "category" => $category,
        ]);

        return (int) $this->pdo->lastInsertId();
    }
}

$repo = new ProductRepository(require __DIR__ . "/db.php");

$id = $repo->create("Stapler", 120, 15, "office");

echo "Created product $id";
Output
OUTPUT
Created product 6

The method returns the new id, so the page can redirect to the new product.

Validation as a function

index.php
PHP
<?php
function validateProduct(array $input): array
{
    $errors = [];

    $name = trim((string) ($input["name"] ?? ""));
    $price = filter_var($input["price"] ?? "", FILTER_VALIDATE_FLOAT);
    $stock = filter_var($input["stock"] ?? "", FILTER_VALIDATE_INT);
    $category = trim((string) ($input["category"] ?? ""));

    if ($name === "" || mb_strlen($name) > 150) {
        $errors["name"] = "Enter a name (up to 150 characters).";
    }

    if ($price === false || $price < 0) {
        $errors["price"] = "Enter a price of 0 or more.";
    }

    if ($stock === false || $stock < 0) {
        $errors["stock"] = "Enter a whole number of 0 or more.";
    }

    if ($category === "" || mb_strlen($category) > 50) {
        $errors["category"] = "Enter a category (up to 50 characters).";
    }

    return $errors;
}

print_r(validateProduct(["name" => "", "price" => "abc", "stock" => "5", "category" => "office"]));
Output
OUTPUT
Array
(
    [name] => Enter a name (up to 150 characters).
    [price] => Enter a price of 0 or more.
)

The page: product-new.php

product-new.php
PHP
<?php
declare(strict_types=1);

session_start();

require __DIR__ . "/ProductRepository.php";
require __DIR__ . "/functions.php";   // h() and validateProduct()

$pdo = require __DIR__ . "/db.php";
$repo = new ProductRepository($pdo);

$old = ["name" => "", "price" => "", "stock" => "0", "category" => "general"];
$errors = [];

if ($_SERVER["REQUEST_METHOD"] === "POST") {
    // 1. CSRF check (see the CSRF lesson)
    if (!hash_equals($_SESSION["csrf_token"] ?? "", $_POST["csrf_token"] ?? "")) {
        http_response_code(403);
        exit("Invalid form token.");
    }

    $old = array_map(fn($v) => is_string($v) ? trim($v) : "", array_intersect_key($_POST, $old));
    $errors = validateProduct($old);

    if ($errors === []) {
        $id = $repo->create($old["name"], (float) $old["price"], (int) $old["stock"], $old["category"]);

        $_SESSION["flash"] = "Product created.";
        header("Location: product.php?id=" . $id);
        exit;
    }
}

$_SESSION["csrf_token"] ??= bin2hex(random_bytes(32));
?>
<!DOCTYPE html>
<html lang="en">
<head><meta charset="utf-8"><title>Add product</title></head>
<body>
  <h1>Add product</h1>

  <form method="post" novalidate>
    <input type="hidden" name="csrf_token" value="<?= h($_SESSION["csrf_token"]) ?>">

    <p>
      <label for="name">Name</label><br>
      <input id="name" name="name" value="<?= h($old["name"]) ?>" required>
      <?php if (isset($errors["name"])): ?><br><span role="alert"><?= h($errors["name"]) ?></span><?php endif; ?>
    </p>
    <p>
      <label for="price">Price</label><br>
      <input id="price" name="price" inputmode="decimal" value="<?= h($old["price"]) ?>" required>
      <?php if (isset($errors["price"])): ?><br><span role="alert"><?= h($errors["price"]) ?></span><?php endif; ?>
    </p>
    <p>
      <label for="stock">Stock</label><br>
      <input id="stock" name="stock" inputmode="numeric" value="<?= h($old["stock"]) ?>">
      <?php if (isset($errors["stock"])): ?><br><span role="alert"><?= h($errors["stock"]) ?></span><?php endif; ?>
    </p>
    <p>
      <label for="category">Category</label><br>
      <input id="category" name="category" value="<?= h($old["category"]) ?>">
      <?php if (isset($errors["category"])): ?><br><span role="alert"><?= h($errors["category"]) ?></span><?php endif; ?>
    </p>
    <button type="submit">Save product</button>
  </form>
</body>
</html>

What the page does

  1. GET: shows an empty form (with a fresh CSRF token).
  2. POST: checks the token, reads only the four fields we expect (array_intersect_key), trims them and validates.
  3. If there are errors: shows the form again with the old values and messages.
  4. If valid: inserts, stores a flash message and redirects to the new product’s page.

Things to notice

  • The form posts to itself (no action), so errors can be shown right away.
  • Only the expected fields are used. A visitor who adds is_admin=1 changes nothing.
  • The price is read as text and converted only after validation.
  • h() is a short helper for htmlspecialchars (see Header and Footer Components and Output Escaping).

Exercise: add a description

Add a description TEXT column (nullable) to the table, and a textarea to the form. Which three places need a change?

Show the answer
  1. The table: ALTER TABLE products ADD COLUMN description TEXT NULL;.
  2. The repository: add description to the INSERT statement and the parameters.
  3. The page: add the field to $old, add a <textarea> with h($old["description"]) between the tags, and (optionally) a length check in validateProduct().