Skip to main content

Course Progress

Loading...

CRUD Operations in PHP Applications

Duration: 100 minutes
Module 3: PHP Integration

Learning Objectives

  • Build complete CRUD functionality with PHP and MySQL
  • Implement data validation and sanitization
  • Create user interfaces for data management
  • Handle file uploads and storage
  • Implement search and filtering
  • Add pagination to result sets
  • Build relationships between entities
  • Apply MVC patterns to PHP applications

Understanding CRUD Operations

CRUD (Create, Read, Update, Delete) operations form the foundation of data-driven applications. Let's build a complete system! 🚀

💡
CRUD Best Practices
  • Always use prepared statements to prevent SQL injection
  • Validate data on both client and server side
  • Implement proper error handling and user feedback
  • Use transactions for related operations
  • Consider soft deletes for important data
  • Log all CRUD operations for audit trails

Complete CRUD Implementation

graph TB USER[User Interface] --> CONTROLLER[Controller] CONTROLLER --> MODEL[Model/Database] MODEL --> DB[(MySQL)] CONTROLLER --> VALIDATE[Validation] VALIDATE --> SANITIZE[Sanitization] SANITIZE --> MODEL MODEL --> RESULT[Result] RESULT --> VIEW[View/Template] VIEW --> USER style USER fill:#dcfce7 style CONTROLLER fill:#dbeafe style MODEL fill:#fef3c7 style DB fill:#e9d5ff

Complete Product Management System

<?php
// ========================================
// DATABASE CONNECTION CLASS
// ========================================

class Database {
    private static $instance = null;
    private $pdo;
    
    private function __construct() {
        $config = [
            'host' => 'localhost',
            'dbname' => 'shop',
            'username' => 'root',
            'password' => '',
            'charset' => 'utf8mb4'
        ];
        
        try {
            $dsn = "mysql:host={$config['host']};dbname={$config['dbname']};charset={$config['charset']}";
            
            $this->pdo = new PDO($dsn, $config['username'], $config['password'], [
                PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
                PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
                PDO::ATTR_EMULATE_PREPARES => false
            ]);
        } catch (PDOException $e) {
            die("Connection failed: " . $e->getMessage());
        }
    }
    
    public static function getInstance() {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }
    
    public function getConnection() {
        return $this->pdo;
    }
}

// ========================================
// PRODUCT MODEL CLASS
// ========================================

class Product {
    private $db;
    private $table = 'products';
    
    public function __construct() {
        $this->db = Database::getInstance()->getConnection();
    }
    
    /**
     * CREATE - Add new product
     */
    public function create($data) {
        try {
            // Validate required fields
            $required = ['name', 'price', 'category_id'];
            foreach ($required as $field) {
                if (empty($data[$field])) {
                    throw new Exception("Field '$field' is required");
                }
            }
            
            // Sanitize data
            $data = $this->sanitizeData($data);
            
            // Prepare SQL
            $sql = "INSERT INTO {$this->table} 
                    (name, description, price, category_id, stock_quantity, sku, created_at) 
                    VALUES 
                    (:name, :description, :price, :category_id, :stock_quantity, :sku, NOW())";
            
            $stmt = $this->db->prepare($sql);
            
            // Bind parameters
            $stmt->bindParam(':name', $data['name']);
            $stmt->bindParam(':description', $data['description']);
            $stmt->bindParam(':price', $data['price']);
            $stmt->bindParam(':category_id', $data['category_id']);
            $stmt->bindParam(':stock_quantity', $data['stock_quantity']);
            $stmt->bindParam(':sku', $data['sku']);
            
            // Execute
            if ($stmt->execute()) {
                return [
                    'success' => true,
                    'id' => $this->db->lastInsertId(),
                    'message' => 'Product created successfully'
                ];
            }
            
            return ['success' => false, 'message' => 'Failed to create product'];
            
        } catch (Exception $e) {
            return ['success' => false, 'message' => $e->getMessage()];
        }
    }
    
    /**
     * READ - Get products with pagination and filtering
     */
    public function read($params = []) {
        try {
            // Base query
            $sql = "SELECT p.*, c.name as category_name 
                    FROM {$this->table} p 
                    LEFT JOIN categories c ON p.category_id = c.id 
                    WHERE 1=1";
            
            $bindings = [];
            
            // Add search filter
            if (!empty($params['search'])) {
                $sql .= " AND (p.name LIKE :search OR p.description LIKE :search)";
                $bindings[':search'] = '%' . $params['search'] . '%';
            }
            
            // Add category filter
            if (!empty($params['category'])) {
                $sql .= " AND p.category_id = :category";
                $bindings[':category'] = $params['category'];
            }
            
            // Add price range filter
            if (!empty($params['min_price'])) {
                $sql .= " AND p.price >= :min_price";
                $bindings[':min_price'] = $params['min_price'];
            }
            
            if (!empty($params['max_price'])) {
                $sql .= " AND p.price <= :max_price";
                $bindings[':max_price'] = $params['max_price'];
            }
            
            // Count total records
            $countSql = "SELECT COUNT(*) as total FROM ({$sql}) as count_table";
            $countStmt = $this->db->prepare($countSql);
            $countStmt->execute($bindings);
            $total = $countStmt->fetch()['total'];
            
            // Add sorting
            $sortColumn = $params['sort'] ?? 'name';
            $sortOrder = $params['order'] ?? 'ASC';
            $allowedColumns = ['name', 'price', 'stock_quantity', 'created_at'];
            
            if (in_array($sortColumn, $allowedColumns)) {
                $sql .= " ORDER BY p.{$sortColumn} {$sortOrder}";
            }
            
            // Add pagination
            $page = max(1, $params['page'] ?? 1);
            $perPage = $params['per_page'] ?? 10;
            $offset = ($page - 1) * $perPage;
            
            $sql .= " LIMIT :limit OFFSET :offset";
            
            // Prepare and execute
            $stmt = $this->db->prepare($sql);
            foreach ($bindings as $key => $value) {
                $stmt->bindValue($key, $value);
            }
            $stmt->bindValue(':limit', $perPage, PDO::PARAM_INT);
            $stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
            
            $stmt->execute();
            
            return [
                'success' => true,
                'data' => $stmt->fetchAll(),
                'pagination' => [
                    'total' => $total,
                    'per_page' => $perPage,
                    'current_page' => $page,
                    'total_pages' => ceil($total / $perPage)
                ]
            ];
            
        } catch (Exception $e) {
            return ['success' => false, 'message' => $e->getMessage()];
        }
    }
    
    /**
     * READ - Get single product
     */
    public function findById($id) {
        try {
            $sql = "SELECT p.*, c.name as category_name 
                    FROM {$this->table} p 
                    LEFT JOIN categories c ON p.category_id = c.id 
                    WHERE p.id = :id";
            
            $stmt = $this->db->prepare($sql);
            $stmt->execute([':id' => $id]);
            
            $product = $stmt->fetch();
            
            if ($product) {
                return ['success' => true, 'data' => $product];
            }
            
            return ['success' => false, 'message' => 'Product not found'];
            
        } catch (Exception $e) {
            return ['success' => false, 'message' => $e->getMessage()];
        }
    }
    
    /**
     * UPDATE - Update product
     */
    public function update($id, $data) {
        try {
            // Check if product exists
            $existing = $this->findById($id);
            if (!$existing['success']) {
                return ['success' => false, 'message' => 'Product not found'];
            }
            
            // Sanitize data
            $data = $this->sanitizeData($data);
            
            // Build update query dynamically
            $fields = [];
            $bindings = [':id' => $id];
            
            foreach (['name', 'description', 'price', 'category_id', 'stock_quantity', 'sku'] as $field) {
                if (isset($data[$field])) {
                    $fields[] = "{$field} = :{$field}";
                    $bindings[":{$field}"] = $data[$field];
                }
            }
            
            if (empty($fields)) {
                return ['success' => false, 'message' => 'No fields to update'];
            }
            
            // Add updated_at
            $fields[] = "updated_at = NOW()";
            
            $sql = "UPDATE {$this->table} SET " . implode(', ', $fields) . " WHERE id = :id";
            
            $stmt = $this->db->prepare($sql);
            
            if ($stmt->execute($bindings)) {
                return [
                    'success' => true,
                    'message' => 'Product updated successfully',
                    'affected_rows' => $stmt->rowCount()
                ];
            }
            
            return ['success' => false, 'message' => 'Failed to update product'];
            
        } catch (Exception $e) {
            return ['success' => false, 'message' => $e->getMessage()];
        }
    }
    
    /**
     * DELETE - Delete product (with soft delete option)
     */
    public function delete($id, $soft = true) {
        try {
            if ($soft) {
                // Soft delete - mark as deleted
                $sql = "UPDATE {$this->table} 
                        SET deleted_at = NOW() 
                        WHERE id = :id AND deleted_at IS NULL";
            } else {
                // Hard delete - remove from database
                $sql = "DELETE FROM {$this->table} WHERE id = :id";
            }
            
            $stmt = $this->db->prepare($sql);
            $stmt->execute([':id' => $id]);
            
            if ($stmt->rowCount() > 0) {
                return [
                    'success' => true,
                    'message' => 'Product deleted successfully'
                ];
            }
            
            return ['success' => false, 'message' => 'Product not found or already deleted'];
            
        } catch (Exception $e) {
            return ['success' => false, 'message' => $e->getMessage()];
        }
    }
    
    /**
     * Bulk operations
     */
    public function bulkDelete($ids) {
        try {
            $placeholders = str_repeat('?,', count($ids) - 1) . '?';
            $sql = "UPDATE {$this->table} 
                    SET deleted_at = NOW() 
                    WHERE id IN ($placeholders)";
            
            $stmt = $this->db->prepare($sql);
            $stmt->execute($ids);
            
            return [
                'success' => true,
                'message' => $stmt->rowCount() . ' products deleted'
            ];
            
        } catch (Exception $e) {
            return ['success' => false, 'message' => $e->getMessage()];
        }
    }
    
    /**
     * Data sanitization
     */
    private function sanitizeData($data) {
        $sanitized = [];
        
        foreach ($data as $key => $value) {
            switch ($key) {
                case 'name':
                case 'description':
                case 'sku':
                    $sanitized[$key] = htmlspecialchars(trim($value), ENT_QUOTES, 'UTF-8');
                    break;
                    
                case 'price':
                    $sanitized[$key] = filter_var($value, FILTER_VALIDATE_FLOAT);
                    break;
                    
                case 'category_id':
                case 'stock_quantity':
                    $sanitized[$key] = filter_var($value, FILTER_VALIDATE_INT);
                    break;
                    
                default:
                    $sanitized[$key] = $value;
            }
        }
        
        return $sanitized;
    }
}

User Interface for CRUD Operations

HTML Forms and UI Components

<?php
// ========================================
// PRODUCT CONTROLLER
// ========================================

class ProductController {
    private $model;
    
    public function __construct() {
        $this->model = new Product();
    }
    
    /**
     * Display products list
     */
    public function index() {
        // Get parameters from request
        $params = [
            'search' => $_GET['search'] ?? '',
            'category' => $_GET['category'] ?? '',
            'page' => $_GET['page'] ?? 1,
            'sort' => $_GET['sort'] ?? 'name',
            'order' => $_GET['order'] ?? 'ASC'
        ];
        
        $result = $this->model->read($params);
        
        if ($result['success']) {
            $products = $result['data'];
            $pagination = $result['pagination'];
            include 'views/products/index.php';
        } else {
            $error = $result['message'];
            include 'views/error.php';
        }
    }
    
    /**
     * Show create form
     */
    public function create() {
        include 'views/products/create.php';
    }
    
    /**
     * Store new product
     */
    public function store() {
        if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
            header('Location: /products');
            exit;
        }
        
        // CSRF token validation
        if (!$this->validateCSRFToken($_POST['csrf_token'] ?? '')) {
            $_SESSION['error'] = 'Invalid request';
            header('Location: /products/create');
            exit;
        }
        
        $result = $this->model->create($_POST);
        
        if ($result['success']) {
            $_SESSION['success'] = $result['message'];
            header('Location: /products');
        } else {
            $_SESSION['error'] = $result['message'];
            $_SESSION['old_input'] = $_POST;
            header('Location: /products/create');
        }
        exit;
    }
    
    /**
     * Show edit form
     */
    public function edit($id) {
        $result = $this->model->findById($id);
        
        if ($result['success']) {
            $product = $result['data'];
            include 'views/products/edit.php';
        } else {
            $_SESSION['error'] = 'Product not found';
            header('Location: /products');
            exit;
        }
    }
    
    /**
     * Update product
     */
    public function update($id) {
        if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
            header('Location: /products');
            exit;
        }
        
        // CSRF validation
        if (!$this->validateCSRFToken($_POST['csrf_token'] ?? '')) {
            $_SESSION['error'] = 'Invalid request';
            header("Location: /products/{$id}/edit");
            exit;
        }
        
        $result = $this->model->update($id, $_POST);
        
        if ($result['success']) {
            $_SESSION['success'] = $result['message'];
            header('Location: /products');
        } else {
            $_SESSION['error'] = $result['message'];
            $_SESSION['old_input'] = $_POST;
            header("Location: /products/{$id}/edit");
        }
        exit;
    }
    
    /**
     * Delete product
     */
    public function destroy($id) {
        if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
            header('Location: /products');
            exit;
        }
        
        // CSRF validation
        if (!$this->validateCSRFToken($_POST['csrf_token'] ?? '')) {
            $_SESSION['error'] = 'Invalid request';
            header('Location: /products');
            exit;
        }
        
        $result = $this->model->delete($id);
        
        if ($result['success']) {
            $_SESSION['success'] = $result['message'];
        } else {
            $_SESSION['error'] = $result['message'];
        }
        
        header('Location: /products');
        exit;
    }
    
    /**
     * Generate CSRF token
     */
    public function generateCSRFToken() {
        if (empty($_SESSION['csrf_token'])) {
            $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
        }
        return $_SESSION['csrf_token'];
    }
    
    /**
     * Validate CSRF token
     */
    private function validateCSRFToken($token) {
        return isset($_SESSION['csrf_token']) && 
               hash_equals($_SESSION['csrf_token'], $token);
    }
}

// ========================================
// VIEW: products/index.php
// ========================================
?>

<!DOCTYPE html>
<html>
<head>
    <title>Product Management</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
    <div class="container mt-5">
        <h1>Products</h1>
        
        <!-- Success/Error Messages -->
        <?php if (isset($_SESSION['success'])): ?>
            <div class="alert alert-success alert-dismissible fade show">
                <?= htmlspecialchars($_SESSION['success']) ?>
                <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
            </div>
            <?php unset($_SESSION['success']); ?>
        <?php endif; ?>
        
        <!-- Search and Filter Form -->
        <form method="GET" action="/products" class="row g-3 mb-4">
            <div class="col-md-4">
                <input type="text" name="search" class="form-control" 
                       placeholder="Search products..." 
                       value="<?= htmlspecialchars($params['search']) ?>">
            </div>
            <div class="col-md-3">
                <select name="category" class="form-control">
                    <option value="">All Categories</option>
                    <!-- Categories would be loaded dynamically -->
                </select>
            </div>
            <div class="col-md-2">
                <button type="submit" class="btn btn-primary">Search</button>
            </div>
            <div class="col-md-3 text-end">
                <a href="/products/create" class="btn btn-success">Add New Product</a>
            </div>
        </form>
        
        <!-- Products Table -->
        <table class="table table-striped">
            <thead>
                <tr>
                    <th><a href="?sort=idℴ=<?= $params['order'] === 'ASC' ? 'DESC' : 'ASC' ?>">ID</a></th>
                    <th><a href="?sort=nameℴ=<?= $params['order'] === 'ASC' ? 'DESC' : 'ASC' ?>">Name</a></th>
                    <th>Category</th>
                    <th><a href="?sort=priceℴ=<?= $params['order'] === 'ASC' ? 'DESC' : 'ASC' ?>">Price</a></th>
                    <th>Stock</th>
                    <th>Actions</th>
                </tr>
            </thead>
            <tbody>
                <?php foreach ($products as $product): ?>
                <tr>
                    <td><?= $product['id'] ?></td>
                    <td><?= htmlspecialchars($product['name']) ?></td>
                    <td><?= htmlspecialchars($product['category_name']) ?></td>
                    <td>$<?= number_format($product['price'], 2) ?></td>
                    <td>
                        <span class="badge <?= $product['stock_quantity'] > 10 ? 'bg-success' : 'bg-warning' ?>">
                            <?= $product['stock_quantity'] ?>
                        </span>
                    </td>
                    <td>
                        <a href="/products/<?= $product['id'] ?>" class="btn btn-sm btn-info">View</a>
                        <a href="/products/<?= $product['id'] ?>/edit" class="btn btn-sm btn-warning">Edit</a>
                        <form method="POST" action="/products/<?= $product['id'] ?>/delete" 
                              style="display:inline;" onsubmit="return confirm('Are you sure?')">
                            <input type="hidden" name="csrf_token" value="<?= $controller->generateCSRFToken() ?>">
                            <button type="submit" class="btn btn-sm btn-danger">Delete</button>
                        </form>
                    </td>
                </tr>
                <?php endforeach; ?>
            </tbody>
        </table>
        
        <!-- Pagination -->
        <?php if ($pagination['total_pages'] > 1): ?>
        <nav>
            <ul class="pagination">
                <?php for ($i = 1; $i <= $pagination['total_pages']; $i++): ?>
                    <li class="page-item <?= $i === $pagination['current_page'] ? 'active' : '' ?>">
                        <a class="page-link" href="?page=<?= $i ?>&search=<?= urlencode($params['search']) ?>">
                            <?= $i ?>
                        </a>
                    </li>
                <?php endfor; ?>
            </ul>
        </nav>
        <?php endif; ?>
    </div>
</body>
</html>

Practice Exercise

💻
CRUD Implementation Challenges
Build these CRUD features:
  1. Create a complete User Management System
  2. Implement Category CRUD with hierarchy
  3. Build an Order Management System
  4. Create a Blog Post system with comments
  5. Implement file upload for products
  6. Add bulk import/export functionality
  7. Create an audit log for all CRUD operations
  8. Build REST API endpoints for CRUD