Skip to main content
This guide walks through implementing full CRUD (Create, Read, Update, Delete) functionality using the Songs feature as a real-world example.

Architecture Overview

MINI’s CRUD operations follow the MVC pattern:
  • Model (application/model/model.php) - Database queries
  • Controller (application/controller/songs.php) - Business logic and flow control
  • View (application/view/songs/) - HTML presentation

Read Operations

Get All Records

application/model/model.php
public function getAllSongs()
{
    $sql = "SELECT id, artist, track, link FROM song";
    $query = $this->db->prepare($sql);
    $query->execute();
    
    return $query->fetchAll();
}
The getAllSongs() method returns an array of objects. Each object’s properties correspond to database columns.

Get Single Record

application/model/model.php
public function getSong($song_id)
{
    $sql = "SELECT id, artist, track, link FROM song WHERE id = :song_id LIMIT 1";
    $query = $this->db->prepare($sql);
    $parameters = array(':song_id' => $song_id);
    
    $query->execute($parameters);
    
    return $query->fetch();
}

Create Operations

Add New Record

1

Create the form view

application/view/songs/index.php
<form action="<?php echo URL; ?>songs/addsong" method="POST">
    <label>Artist</label>
    <input type="text" name="artist" value="" required />
    
    <label>Track</label>
    <input type="text" name="track" value="" required />
    
    <label>Link</label>
    <input type="text" name="link" value="" />
    
    <input type="submit" name="submit_add_song" value="Submit" />
</form>
2

Handle form submission in controller

application/controller/songs.php
public function addSong()
{
    // Check if form was submitted
    if (isset($_POST["submit_add_song"])) {
        // Call model method
        $this->model->addSong(
            $_POST["artist"],
            $_POST["track"],
            $_POST["link"]
        );
    }
    
    // Redirect back to index
    header('location: ' . URL . 'songs/index');
}
3

Execute INSERT query in model

application/model/model.php
public function addSong($artist, $track, $link)
{
    $sql = "INSERT INTO song (artist, track, link) VALUES (:artist, :track, :link)";
    $query = $this->db->prepare($sql);
    $parameters = array(
        ':artist' => $artist,
        ':track' => $track,
        ':link' => $link
    );
    
    $query->execute($parameters);
}
The controller action (e.g., addSong()) is not a regular page - it’s an ACTION endpoint. It processes POST data and redirects. Never render views directly from action methods.

Update Operations

Modify Existing Record

1

Create edit form with pre-filled data

application/view/songs/edit.php
<form action="<?php echo URL; ?>songs/updatesong" method="POST">
    <label>Artist</label>
    <input type="text" name="artist" 
           value="<?php echo htmlspecialchars($song->artist, ENT_QUOTES, 'UTF-8'); ?>" 
           required />
    
    <label>Track</label>
    <input type="text" name="track" 
           value="<?php echo htmlspecialchars($song->track, ENT_QUOTES, 'UTF-8'); ?>" 
           required />
    
    <label>Link</label>
    <input type="text" name="link" 
           value="<?php echo htmlspecialchars($song->link, ENT_QUOTES, 'UTF-8'); ?>" />
    
    <!-- Hidden field to track which record to update -->
    <input type="hidden" name="song_id" 
           value="<?php echo htmlspecialchars($song->id, ENT_QUOTES, 'UTF-8'); ?>" />
    
    <input type="submit" name="submit_update_song" value="Update" />
</form>
2

Handle update in controller

application/controller/songs.php
public function updateSong()
{
    if (isset($_POST["submit_update_song"])) {
        // Call model update method
        $this->model->updateSong(
            $_POST["artist"],
            $_POST["track"],
            $_POST["link"],
            $_POST['song_id']
        );
    }
    
    // Redirect back to index
    header('location: ' . URL . 'songs/index');
}
3

Execute UPDATE query in model

application/model/model.php
public function updateSong($artist, $track, $link, $song_id)
{
    $sql = "UPDATE song SET artist = :artist, track = :track, link = :link WHERE id = :song_id";
    $query = $this->db->prepare($sql);
    $parameters = array(
        ':artist' => $artist,
        ':track' => $track,
        ':link' => $link,
        ':song_id' => $song_id
    );
    
    $query->execute($parameters);
}
The hidden song_id field is crucial - it tells the update method which record to modify.

Delete Operations

Remove Record

Delete operations via GET requests (URL links) are not production-ready. In real applications, use POST requests with CSRF tokens and confirmation dialogs.

Form Handling

POST vs GET

POST requests (forms with method="POST"):
  • Used for Create and Update operations
  • Data in $_POST array
  • Not visible in URL
  • Can handle large amounts of data
if (isset($_POST["submit_add_song"])) {
    $artist = $_POST["artist"];
    $track = $_POST["track"];
}
GET requests (URL parameters):
  • Used for Read and Delete operations
  • Data in URL and method parameters
  • Bookmarkable URLs
  • Limited data size
public function deleteSong($song_id)
{
    // $song_id comes from URL: /songs/deletesong/123
}

Form Validation

MINI uses HTML5 validation:
<input type="text" name="artist" required />
<input type="email" name="email" required />
<input type="url" name="website" />

Data Security

Input Security (Automatic)

PDO prepared statements automatically escape all input, preventing SQL injection. You don’t need to manually sanitize data before database operations.
// ✅ This is safe - PDO handles escaping
$this->model->addSong($_POST["artist"], $_POST["track"], $_POST["link"]);

Output Security (Required)

Always escape output in views:
// ✅ CORRECT - Prevents XSS attacks
<?php echo htmlspecialchars($song->artist, ENT_QUOTES, 'UTF-8'); ?>

// ❌ WRONG - Vulnerable to XSS
<?php echo $song->artist; ?>
Why htmlspecialchars()?
  • Converts special characters to HTML entities
  • ENT_QUOTES - Escapes both single and double quotes
  • UTF-8 - Proper character encoding

Common Patterns

Aggregate Queries

application/model/model.php
public function getAmountOfSongs()
{
    $sql = "SELECT COUNT(id) AS amount_of_songs FROM song";
    $query = $this->db->prepare($sql);
    $query->execute();
    
    return $query->fetch()->amount_of_songs;
}

Conditional Queries

public function searchSongs($search_term)
{
    $sql = "SELECT id, artist, track FROM song WHERE artist LIKE :search OR track LIKE :search";
    $query = $this->db->prepare($sql);
    $parameters = array(':search' => '%' . $search_term . '%');
    $query->execute($parameters);
    
    return $query->fetchAll();
}

Pagination

public function getSongsPaginated($offset, $limit)
{
    $sql = "SELECT id, artist, track FROM song LIMIT :limit OFFSET :offset";
    $query = $this->db->prepare($sql);
    $query->bindValue(':limit', $limit, PDO::PARAM_INT);
    $query->bindValue(':offset', $offset, PDO::PARAM_INT);
    $query->execute();
    
    return $query->fetchAll();
}
For LIMIT/OFFSET, use bindValue() with PDO::PARAM_INT instead of the parameters array.

URL Routing

MINI’s routing maps URLs to controller methods:
URL: /songs/index
→ Controller: Songs
→ Method: index()

URL: /songs/editsong/5
→ Controller: Songs
→ Method: editSong($song_id)
→ Parameter: $song_id = 5

URL: /songs/deletesong/5
→ Controller: Songs  
→ Method: deleteSong($song_id)
→ Parameter: $song_id = 5

Best Practices

All SQL queries should be in application/model/model.php, not in controllers.
// ✅ CORRECT
$songs = $this->model->getAllSongs();

// ❌ WRONG
$query = $this->db->prepare("SELECT * FROM song");
  • Create, Update, Delete should use POST requests
  • Only Read operations should use GET
  • Prevents accidental modifications from web crawlers
Prevents duplicate submissions when users refresh the page.
public function addSong()
{
    if (isset($_POST["submit_add_song"])) {
        $this->model->addSong(/* ... */);
    }
    
    // Always redirect
    header('location: ' . URL . 'songs/index');
}
Use htmlspecialchars() on every piece of user-generated content displayed in views.
  • Check for required fields
  • Validate data types and formats
  • Use HTML5 validation + server-side validation

Next Steps

AJAX Integration

Learn how to make CRUD operations dynamic with AJAX

Database Guide

Deep dive into database configuration and PDO usage

Build docs developers (and LLMs) love