DALT.PHP
Guides

Building a Blog

Complete tutorial - build a blog from scratch with DALT.PHP

Let's build a complete blog application from scratch using DALT.PHP. You'll learn routing, database queries, CRUD operations, and security best practices.

What You'll Build

A fully functional blog with:

  • List all posts
  • View single post
  • Create new post
  • Edit existing post
  • Delete post
  • Basic validation

Prerequisites

  • DALT.PHP installed and running
  • Completed the Quick Start guide
  • Basic understanding of PHP and SQL

Time Required: 30-45 minutes

Step 1: Create the Database Table

First, create a migration for the posts table.

Generate Migration

php artisan make:migration create_posts_table

This creates a file like database/migrations/20240315120000_create_posts_table.sql.

Write the SQL

Open the migration file and replace the content with:

-- Migration: create_posts_table
-- Created: 2024-03-15

CREATE TABLE IF NOT EXISTS posts (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    title VARCHAR(255) NOT NULL,
    body TEXT NOT NULL,
    published BOOLEAN DEFAULT 0,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

-- Index for faster queries
CREATE INDEX IF NOT EXISTS idx_posts_published ON posts(published);

Run Migration

php artisan migrate

You should see:

Running migration: 20240315120000_create_posts_table.sql
✓ Success

Ran 1 migrations.

Step 2: Create Controllers

Now create controllers for each blog operation.

Create Controllers Directory

mkdir -p app/Http/controllers/posts

Index Controller (List All Posts)

Create app/Http/controllers/posts/index.php:

<?php

$db = App::resolve(Core\Database::class);

$posts = $db->query('SELECT * FROM posts ORDER BY created_at DESC')->get();

view('posts/index.view.php', [
    'posts' => $posts,
    'title' => 'All Posts'
]);

Show Controller (View Single Post)

Create app/Http/controllers/posts/show.php:

<?php

$db = App::resolve(Core\Database::class);

$post = $db->query('SELECT * FROM posts WHERE id = ?', [$_GET['id']])->findOrFail();

view('posts/show.view.php', [
    'post' => $post,
    'title' => $post['title']
]);

Create Controller (Show Form)

Create app/Http/controllers/posts/create.php:

<?php

view('posts/create.view.php', [
    'title' => 'Create Post'
]);

Store Controller (Save New Post)

Create app/Http/controllers/posts/store.php:

<?php

$db = App::resolve(Core\Database::class);

// Validate input
$errors = [];

if (empty($_POST['title'])) {
    $errors['title'] = 'Title is required';
}

if (empty($_POST['body'])) {
    $errors['body'] = 'Body is required';
}

if (!empty($errors)) {
    view('posts/create.view.php', [
        'errors' => $errors,
        'old' => $_POST
    ]);
    exit;
}

// Insert post
$db->query('INSERT INTO posts (title, body, published) VALUES (?, ?, ?)', [
    $_POST['title'],
    $_POST['body'],
    isset($_POST['published']) ? 1 : 0
]);

// Redirect with success message
Core\Session::flash('success', 'Post created successfully!');
header('Location: /posts');
exit;

Edit Controller (Show Edit Form)

Create app/Http/controllers/posts/edit.php:

<?php

$db = App::resolve(Core\Database::class);

$post = $db->query('SELECT * FROM posts WHERE id = ?', [$_GET['id']])->findOrFail();

view('posts/edit.view.php', [
    'post' => $post,
    'title' => 'Edit Post'
]);

Update Controller (Save Changes)

Create app/Http/controllers/posts/update.php:

<?php

$db = App::resolve(Core\Database::class);

// Validate input
$errors = [];

if (empty($_POST['title'])) {
    $errors['title'] = 'Title is required';
}

if (empty($_POST['body'])) {
    $errors['body'] = 'Body is required';
}

if (!empty($errors)) {
    $post = $db->query('SELECT * FROM posts WHERE id = ?', [$_GET['id']])->findOrFail();
    view('posts/edit.view.php', [
        'post' => $post,
        'errors' => $errors
    ]);
    exit;
}

// Update post
$db->query('UPDATE posts SET title = ?, body = ?, published = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?', [
    $_POST['title'],
    $_POST['body'],
    isset($_POST['published']) ? 1 : 0,
    $_GET['id']
]);

Core\Session::flash('success', 'Post updated successfully!');
header('Location: /posts');
exit;

Destroy Controller (Delete Post)

Create app/Http/controllers/posts/destroy.php:

<?php

$db = App::resolve(Core\Database::class);

$db->query('DELETE FROM posts WHERE id = ?', [$_POST['id']]);

Core\Session::flash('success', 'Post deleted successfully!');
header('Location: /posts');
exit;

Step 3: Add Routes

Register all blog routes in routes/routes.php:

<?php

global $router;

// Existing routes
$router->get('/', 'welcome.php');

// Blog routes
$router->get('/posts', 'posts/index.php');
$router->get('/posts/create', 'posts/create.php');
$router->post('/posts', 'posts/store.php');
$router->get('/posts/{id}', 'posts/show.php');
$router->get('/posts/{id}/edit', 'posts/edit.php');
$router->patch('/posts/{id}', 'posts/update.php');
$router->delete('/posts/{id}', 'posts/destroy.php');

Route Order Matters! Put specific routes (/posts/create) before generic routes (/posts/{id}).

Step 4: Create Views

Create view templates for the blog interface.

Create Views Directory

mkdir -p resources/views/posts

Index View (List Posts)

Create resources/views/posts/index.view.php:

<!DOCTYPE html>
<html>
<head>
    <title><?= $title ?></title>
</head>
<body>
    <h1>Blog Posts</h1>
    
    <?php if (Core\Session::has('success')): ?>
        <p style="color: green;"><?= Core\Session::get('success') ?></p>
    <?php endif; ?>
    
    <a href="/posts/create">Create New Post</a>
    
    <?php if (empty($posts)): ?>
        <p>No posts yet. <a href="/posts/create">Create one!</a></p>
    <?php else: ?>
        <?php foreach ($posts as $post): ?>
            <article>
                <h2>
                    <a href="/posts/<?= $post['id'] ?>">
                        <?= htmlspecialchars($post['title']) ?>
                    </a>
                </h2>
                <p><?= htmlspecialchars(substr($post['body'], 0, 200)) ?>...</p>
                <small>
                    <?= $post['published'] ? 'Published' : 'Draft' ?> | 
                    <?= $post['created_at'] ?>
                </small>
            </article>
            <hr>
        <?php endforeach; ?>
    <?php endif; ?>
</body>
</html>

Show View (Single Post)

Create resources/views/posts/show.view.php:

<!DOCTYPE html>
<html>
<head>
    <title><?= htmlspecialchars($post['title']) ?></title>
</head>
<body>
    <a href="/posts"> Back to all posts</a>
    
    <article>
        <h1><?= htmlspecialchars($post['title']) ?></h1>
        <small>
            <?= $post['published'] ? 'Published' : 'Draft' ?> | 
            <?= $post['created_at'] ?>
        </small>
        
        <div>
            <?= nl2br(htmlspecialchars($post['body'])) ?>
        </div>
        
        <hr>
        
        <a href="/posts/<?= $post['id'] ?>/edit">Edit</a>
        
        <form method="POST" action="/posts/<?= $post['id'] ?>" style="display: inline;">
            <input type="hidden" name="_method" value="DELETE">
            <button type="submit" onclick="return confirm('Are you sure?')">Delete</button>
        </form>
    </article>
</body>
</html>

Create View (New Post Form)

Create resources/views/posts/create.view.php:

<!DOCTYPE html>
<html>
<head>
    <title>Create Post</title>
</head>
<body>
    <h1>Create New Post</h1>
    
    <a href="/posts"> Back to all posts</a>
    
    <form method="POST" action="/posts">
        <div>
            <label>Title:</label>
            <input type="text" name="title" value="<?= htmlspecialchars($old['title'] ?? '') ?>" required>
            <?php if (isset($errors['title'])): ?>
                <p style="color: red;"><?= $errors['title'] ?></p>
            <?php endif; ?>
        </div>
        
        <div>
            <label>Body:</label>
            <textarea name="body" rows="10" required><?= htmlspecialchars($old['body'] ?? '') ?></textarea>
            <?php if (isset($errors['body'])): ?>
                <p style="color: red;"><?= $errors['body'] ?></p>
            <?php endif; ?>
        </div>
        
        <div>
            <label>
                <input type="checkbox" name="published" <?= isset($old['published']) ? 'checked' : '' ?>>
                Published
            </label>
        </div>
        
        <button type="submit">Create Post</button>
    </form>
</body>
</html>

Edit View (Edit Post Form)

Create resources/views/posts/edit.view.php:

<!DOCTYPE html>
<html>
<head>
    <title>Edit Post</title>
</head>
<body>
    <h1>Edit Post</h1>
    
    <a href="/posts"> Back to all posts</a>
    
    <form method="POST" action="/posts/<?= $post['id'] ?>">
        <input type="hidden" name="_method" value="PATCH">
        
        <div>
            <label>Title:</label>
            <input type="text" name="title" value="<?= htmlspecialchars($post['title']) ?>" required>
            <?php if (isset($errors['title'])): ?>
                <p style="color: red;"><?= $errors['title'] ?></p>
            <?php endif; ?>
        </div>
        
        <div>
            <label>Body:</label>
            <textarea name="body" rows="10" required><?= htmlspecialchars($post['body']) ?></textarea>
            <?php if (isset($errors['body'])): ?>
                <p style="color: red;"><?= $errors['body'] ?></p>
            <?php endif; ?>
        </div>
        
        <div>
            <label>
                <input type="checkbox" name="published" <?= $post['published'] ? 'checked' : '' ?>>
                Published
            </label>
        </div>
        
        <button type="submit">Update Post</button>
    </form>
</body>
</html>

Step 5: Test Your Blog

Make sure your development server is running, then test all the functionality.

Start the Server

If not already running:

php artisan serve

You should see:

Starting development server: http://127.0.0.1:8000

Visit the Blog

Open http://localhost:8000/posts

You should see "No posts yet" with a link to create one.

Create a Post

  1. Click "Create New Post"
  2. Fill in title and body
  3. Check "Published" if you want
  4. Click "Create Post"

You should be redirected to the posts list with a success message.

View a Post

Click on a post title to view the full post.

Edit a Post

  1. Click "Edit" on a post
  2. Modify the content
  3. Click "Update Post"

Delete a Post

  1. Click "Delete" on a post
  2. Confirm the deletion

The post should be removed.

What You Learned

Congratulations! You've built a complete blog application. Here's what you learned:

1. Database Migrations

CREATE TABLE IF NOT EXISTS posts (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    title VARCHAR(255) NOT NULL,
    body TEXT NOT NULL
);

You learned how to create tables with proper data types and constraints.

2. CRUD Operations

  • Create: INSERT INTO posts (...) VALUES (...)
  • Read: SELECT * FROM posts WHERE id = ?
  • Update: UPDATE posts SET ... WHERE id = ?
  • Delete: DELETE FROM posts WHERE id = ?

3. Prepared Statements

$db->query('SELECT * FROM posts WHERE id = ?', [$_GET['id']]);

Always use prepared statements to prevent SQL injection!

4. RESTful Routing

GET    /posts index  (list all)
GET    /posts/create create (show form)
POST   /posts store  (save new)
GET    /posts/{id}       show   (view one)
GET    /posts/{id}/edit edit   (show form)
PATCH  /posts/{id}       update (save changes)
DELETE /posts/{id}       destroy (delete)

5. Form Validation

$errors = [];
if (empty($_POST['title'])) {
    $errors['title'] = 'Title is required';
}

Always validate user input before saving to database.

6. Flash Messages

Core\Session::flash('success', 'Post created!');

Flash messages persist for one request - perfect for success/error messages after redirects.

7. XSS Prevention

<?= htmlspecialchars($post['title']) ?>

Always escape output to prevent XSS attacks.

Security Best Practices

Your blog implements several security measures:

1. SQL Injection Prevention

Using prepared statements:

$db->query('SELECT * FROM posts WHERE id = ?', [$_GET['id']]);

Never do this:

$db->query("SELECT * FROM posts WHERE id = {$_GET['id']}");

2. XSS Prevention

Escaping output:

<?= htmlspecialchars($post['title']) ?>

Never do this:

<?= $post['title'] ?>  // Vulnerable to XSS!

3. CSRF Protection (Optional Enhancement)

Add CSRF protection to forms:

// In routes
$router->post('/posts', 'posts/store.php')->only('csrf');

// In form
<input type="hidden" name="_token" value="<?= $_SESSION['_csrf'] ?>">

Next Steps

Add Features

  1. Comments - Let users comment on posts
  2. Categories - Organize posts by category
  3. Search - Search posts by title/body
  4. Pagination - Show 10 posts per page
  5. Authentication - Require login to create/edit posts

Improve UI

  1. Add CSS - Style your blog with Tailwind or custom CSS
  2. Rich Text Editor - Use TinyMCE or similar
  3. Image Uploads - Add featured images to posts

Learn More

Congratulations! You've built a complete blog application from scratch. You now understand CRUD operations, routing, validation, and security best practices.

API Best Practices

Your API implements several best practices:

1. Consistent Response Format

{
  "success": true,
  "data": { ... }
}

or

{
  "error": "Error message"
}

Clients always know what to expect.

2. Proper HTTP Methods

  • GET - Read data (safe, idempotent)
  • POST - Create new resource
  • PATCH - Partial update
  • DELETE - Remove resource

3. Meaningful Status Codes

  • 2xx - Success
  • 4xx - Client error
  • 5xx - Server error

4. Resource-Based URLs

/api/tasks       ✅ Good (resource-based)
/api/getTasks    ❌ Bad (action-based)

5. JSON Content Type

header('Content-Type: application/json');

Tells clients the response is JSON.

Advanced Features

Add Filtering

// GET /api/tasks?completed=1
$completed = $_GET['completed'] ?? null;

$sql = 'SELECT * FROM tasks';
$params = [];

if ($completed !== null) {
    $sql .= ' WHERE completed = ?';
    $params[] = $completed;
}

$tasks = $db->query($sql, $params)->get();

Add Pagination

// GET /api/tasks?page=1&limit=10
$page = $_GET['page'] ?? 1;
$limit = $_GET['limit'] ?? 10;
$offset = ($page - 1) * $limit;

$tasks = $db->query(
    'SELECT * FROM tasks LIMIT ? OFFSET ?',
    [$limit, $offset]
)->get();

$total = $db->query('SELECT COUNT(*) as count FROM tasks')->find()['count'];

json_success([
    'tasks' => $tasks,
    'pagination' => [
        'page' => $page,
        'limit' => $limit,
        'total' => $total,
        'pages' => ceil($total / $limit)
    ]
]);

Add Authentication

// Require API key
$apiKey = $_SERVER['HTTP_X_API_KEY'] ?? null;

if ($apiKey !== 'your-secret-key') {
    json_error('Unauthorized', 401);
}

Add CORS Headers

// Allow cross-origin requests
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PATCH, DELETE');
header('Access-Control-Allow-Headers: Content-Type, X-API-Key');

Testing with Postman

Instead of curl, you can use Postman:

  1. Download Postman
  2. Create a new collection "Task API"
  3. Add requests for each endpoint
  4. Test all CRUD operations

Next Steps

Enhance Your API

  1. Add authentication - Require login to access API
  2. Add rate limiting - Prevent abuse
  3. Add versioning - /api/v1/tasks
  4. Add documentation - OpenAPI/Swagger
  5. Add tests - Automated API tests

Learn More

Congratulations! You've built a complete REST API with proper HTTP methods, status codes, and JSON responses. You now understand how to build backend APIs that can be consumed by any frontend.

On this page