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_tableThis 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 migrateYou 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/postsIndex 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/postsIndex 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 serveYou should see:
Starting development server: http://127.0.0.1:8000Visit the Blog
Open http://localhost:8000/posts
You should see "No posts yet" with a link to create one.
Create a Post
- Click "Create New Post"
- Fill in title and body
- Check "Published" if you want
- 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
- Click "Edit" on a post
- Modify the content
- Click "Update Post"
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
- Comments - Let users comment on posts
- Categories - Organize posts by category
- Search - Search posts by title/body
- Pagination - Show 10 posts per page
- Authentication - Require login to create/edit posts
Improve UI
- Add CSS - Style your blog with Tailwind or custom CSS
- Rich Text Editor - Use TinyMCE or similar
- Image Uploads - Add featured images to posts
Learn More
- Working with Database - Advanced SQL queries
- Building an API - Create a REST API
- Framework Internals - Understand how it works
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 resourcePATCH- Partial updateDELETE- Remove resource
3. Meaningful Status Codes
2xx- Success4xx- Client error5xx- 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:
- Download Postman
- Create a new collection "Task API"
- Add requests for each endpoint
- Test all CRUD operations
Next Steps
Enhance Your API
- Add authentication - Require login to access API
- Add rate limiting - Prevent abuse
- Add versioning -
/api/v1/tasks - Add documentation - OpenAPI/Swagger
- Add tests - Automated API tests
Learn More
- Working with Database - Advanced SQL queries
- Building a Blog - Web interface for your API
- Framework Internals - How routing works
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.