advanced
Step 16 of 16
Building a CRUD Application
PHP Programming
Building a CRUD Application
A CRUD (Create, Read, Update, Delete) application is the foundation of most web applications. Whether you are building a blog, an inventory system, a project manager, or an e-commerce store, the core operations revolve around creating records, displaying them, editing them, and deleting them. In this final lesson, you will build a complete task management application that combines everything you have learned: routing, database access, form handling, validation, sessions, authentication, and security best practices into a production-quality PHP application.
Application Structure
# Project layout
task-manager/
├── composer.json
├── .env
├── public/
│ └── index.php # Entry point
├── src/
│ ├── App.php # Application bootstrap
│ ├── Router.php # Request routing
│ ├── Database.php # PDO wrapper
│ ├── Controllers/
│ │ ├── AuthController.php
│ │ └── TaskController.php
│ ├── Models/
│ │ └── Task.php
│ └── Middleware/
│ └── AuthMiddleware.php
├── templates/
│ ├── layout.php
│ ├── login.php
│ ├── tasks/
│ │ ├── index.php
│ │ ├── create.php
│ │ └── edit.php
└── storage/logs/
Task Model
<?php
// src/Models/Task.php
namespace App\Models;
use App\Database;
class Task {
public function __construct(
public readonly ?int $id,
public string $title,
public string $description,
public string $status,
public string $priority,
public int $user_id,
public ?string $due_date = null,
public ?string $created_at = null
) {}
public static function findAll(int $userId): array {
return Database::fetchAll(
"SELECT * FROM tasks WHERE user_id = ? ORDER BY
FIELD(priority, 'high', 'medium', 'low'),
created_at DESC",
[$userId]
);
}
public static function findById(int $id, int $userId): ?array {
return Database::fetchOne(
"SELECT * FROM tasks WHERE id = ? AND user_id = ?",
[$id, $userId]
);
}
public static function create(array $data): int {
Database::query(
"INSERT INTO tasks (title, description, status, priority, user_id, due_date)
VALUES (?, ?, ?, ?, ?, ?)",
[
$data['title'],
$data['description'] ?? '',
$data['status'] ?? 'pending',
$data['priority'] ?? 'medium',
$data['user_id'],
$data['due_date'] ?? null
]
);
return (int) Database::getConnection()->lastInsertId();
}
public static function update(int $id, array $data, int $userId): bool {
$stmt = Database::query(
"UPDATE tasks SET title = ?, description = ?, status = ?,
priority = ?, due_date = ? WHERE id = ? AND user_id = ?",
[
$data['title'],
$data['description'] ?? '',
$data['status'] ?? 'pending',
$data['priority'] ?? 'medium',
$data['due_date'] ?? null,
$id,
$userId
]
);
return $stmt->rowCount() > 0;
}
public static function delete(int $id, int $userId): bool {
$stmt = Database::query(
"DELETE FROM tasks WHERE id = ? AND user_id = ?",
[$id, $userId]
);
return $stmt->rowCount() > 0;
}
public static function validate(array $data): array {
$errors = [];
if (empty(trim($data['title'] ?? ''))) {
$errors['title'] = 'Title is required';
} elseif (strlen($data['title']) > 200) {
$errors['title'] = 'Title must be under 200 characters';
}
if (!in_array($data['priority'] ?? '', ['low', 'medium', 'high'])) {
$errors['priority'] = 'Invalid priority';
}
if (!in_array($data['status'] ?? '', ['pending', 'in_progress', 'completed'])) {
$errors['status'] = 'Invalid status';
}
return $errors;
}
}
?>
Task Controller
<?php
// src/Controllers/TaskController.php
namespace App\Controllers;
use App\Models\Task;
class TaskController {
private int $userId;
public function __construct() {
$this->userId = $_SESSION['user_id']
?? throw new \RuntimeException('Not authenticated');
}
public function index(): void {
$tasks = Task::findAll($this->userId);
$stats = [
'total' => count($tasks),
'completed' => count(array_filter($tasks, fn($t) => $t['status'] === 'completed')),
'pending' => count(array_filter($tasks, fn($t) => $t['status'] === 'pending')),
];
include __DIR__ . '/../../templates/tasks/index.php';
}
public function create(): void {
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$data = [
'title' => trim($_POST['title'] ?? ''),
'description' => trim($_POST['description'] ?? ''),
'priority' => $_POST['priority'] ?? 'medium',
'status' => 'pending',
'due_date' => $_POST['due_date'] ?: null,
'user_id' => $this->userId,
];
$errors = Task::validate($data);
if (empty($errors)) {
Task::create($data);
$_SESSION['flash'] = 'Task created successfully!';
header('Location: /tasks');
exit;
}
}
include __DIR__ . '/../../templates/tasks/create.php';
}
public function edit(string $id): void {
$task = Task::findById((int)$id, $this->userId);
if (!$task) {
http_response_code(404);
echo "Task not found";
return;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$data = [
'title' => trim($_POST['title'] ?? ''),
'description' => trim($_POST['description'] ?? ''),
'priority' => $_POST['priority'] ?? 'medium',
'status' => $_POST['status'] ?? 'pending',
'due_date' => $_POST['due_date'] ?: null,
];
$errors = Task::validate($data);
if (empty($errors)) {
Task::update((int)$id, $data, $this->userId);
$_SESSION['flash'] = 'Task updated!';
header('Location: /tasks');
exit;
}
}
include __DIR__ . '/../../templates/tasks/edit.php';
}
public function delete(string $id): void {
Task::delete((int)$id, $this->userId);
$_SESSION['flash'] = 'Task deleted.';
header('Location: /tasks');
exit;
}
}
?>
Pro tip: This CRUD application demonstrates the Model-View-Controller (MVC) pattern that all major PHP frameworks use. The model handles data and validation, the controller processes requests and coordinates between model and view, and the templates render HTML. Moving to a framework like Laravel will feel natural because you already understand these patterns — Laravel just adds more features, better tooling, and conventions on top of them.
Key Takeaways
- CRUD applications follow a predictable pattern: list, create form, store, edit form, update, delete.
- Separate your code into Models (data/validation), Controllers (request handling), and Templates (HTML output).
- Always validate input on the server side, even if you have client-side validation — it can be bypassed.
- Use session flash messages to show success/error notifications after redirect (Post-Redirect-Get pattern).
- This MVC pattern is the foundation of all PHP frameworks — understanding it makes learning Laravel or Symfony much easier.
arrow_back Security Best Practices
check_circle Lap Complete!