```
/
This example shows a simple way to implement URL routing in a small, framework-style MVC setup while keeping the router independent from any specific application logic.
The Router class only concerns itself with mapping an HTTP method and a URL pattern to a controller action. Routes are registered by the application (for example in index.php), which makes the router itself reusable and easy to extend by app developers without modifying its internals. Dynamic segments such as /posts/{id} are converted to regular expressions and passed as parameters to the controller method.
The front controller acts as a single entry point, bootstraps the environment, defines the routes, and dispatches the current request. This follows the same front-controller pattern used by most MVC frameworks.
Controllers remain thin and delegate business logic to services, while repositories handle persistence via PDO. Views are simple PHP templates with no routing or database logic. This separation keeps responsibilities clear and allows new resources or applications to be added by registering new routes and controllers, which fits well with a resource-oriented architecture.
<?php
namespace Core;
class Router
{
private $routes = [];
public function get($path, $action)
{
$this->addRoute('GET', $path, $action);
}
public function post($path, $action)
{
$this->addRoute('POST', $path, $action);
}
private function addRoute($method, $path, $action)
{
$pattern = preg_replace('/\{id\}/', '([0-9]+)', $path);
$pattern = "#^$pattern$#";
$this->routes[$method][$pattern] = $action;
}
public function dispatch($path, $method)
{
$method = strtoupper($method);
$path = $path ?: '/';
foreach ($this->routes[$method] ?? [] as $pattern => $action) {
if (preg_match($pattern, $path, $matches)) {
// Extract numeric parameters (skip full match at index 0)
$params = [];
for ($i = 1; $i < count($matches); $i++) {
$params[] = (int)$matches[$i];
}
$this->call($action, $params);
return;
}
}
http_response_code(404);
echo 'Page not found';
}
private function call($action, $params)
{
$parts = explode('@', $action);
$controller = new $parts[0]();
call_user_func_array([$controller, $parts[1]], $params);
}
}
<?php
require_once dirname(__DIR__) . '/vendor/autoload.php';
require_once dirname(__DIR__) . '/config/config.php';
use App\Controllers\PostsController;
use Core\Router;
// Simple exception handler (optional but fine)
set_exception_handler(function (Throwable $e) {
http_response_code(500);
echo 'An error occurred.';
});
$router = new Router();
// Define routes
$router->get('/', PostsController::class . '@index');
$router->get('/posts', PostsController::class . '@index');
$router->get('/posts/create', PostsController::class . '@create');
$router->post('/posts', PostsController::class . '@store');
$router->get('/posts/{id}', PostsController::class . '@show');
$router->get('/posts/{id}/edit', PostsController::class . '@edit');
$router->post('/posts/{id}/update', PostsController::class . '@update');
$router->post('/posts/{id}/delete', PostsController::class . '@destroy');
$path = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?: '/';
$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
$router->dispatch($path, $method);
<?php
namespace Core;
use PDO;
use PDOException;
class Database
{
public static function getConnection(): PDO
{
static $pdo = null;
if ($pdo instanceof PDO) {
return $pdo;
}
$dsn = 'mysql:host=' . DB_HOST . ';dbname=' . DB_NAME . ';charset=utf8mb4';
try {
$pdo = new PDO($dsn, DB_USER, DB_PASS, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
} catch (PDOException $e) {
throw new \RuntimeException('Database connection failed: ' . $e->getMessage());
}
return $pdo;
}
}
<?php
namespace Core;
class Controller
{
protected function view(string $template, array $data = []): void
{
$path = dirname(__DIR__) . '/app/Views/' . $template . '.php';
if (!file_exists($path)) {
throw new \RuntimeException("View {$template} not found");
}
extract($data, EXTR_SKIP);
require $path;
}
}
<h1>Create Post</h1>
<form action="<?php echo BASE_URL; ?>/posts" method="post">
<div>
<label>Title</label>
<input type="text" name="title" value="<?php echo $title; ?>">
<?php if (!empty($errors['title'])): ?>
<p><?php echo $errors['title']; ?></p>
<?php endif; ?>
</div>
<div>
<label>Body</label>
<textarea name="body"><?php echo $body; ?></textarea>
<?php if (!empty($errors['body'])): ?>
<p><?php echo $errors['body']; ?></p>
<?php endif; ?>
</div>
<button type="submit">Save</button>
</form>
<p><a href="<?php echo BASE_URL; ?>/posts">Back to posts</a></p>
<h1>Edit Post</h1>
<form action="<?php echo BASE_URL; ?>/posts/<?php echo $id; ?>/update" method="post">
<div>
<label>Title</label>
<input type="text" name="title" value="<?php echo $title; ?>">
<?php if (!empty($errors['title'])): ?>
<p><?php echo $errors['title']; ?></p>
<?php endif; ?>
</div>
<div>
<label>Body</label>
<textarea name="body"><?php echo $body; ?></textarea>
<?php if (!empty($errors['body'])): ?>
<p><?php echo $errors['body']; ?></p>
<?php endif; ?>
</div>
<button type="submit">Update</button>
</form>
<p><a href="<?php echo BASE_URL; ?>/posts">Back to posts</a></p>
<h1>Posts</h1>
<p><a href="<?php echo BASE_URL; ?>/posts/create">Create Post</a></p>
<?php if (empty($posts)): ?>
<p>No posts yet.</p>
<?php else: ?>
<ul>
<?php foreach ($posts as $post): ?>
<li>
<strong><?php echo $post['title']; ?></strong>
<div>
<a href="<?php echo BASE_URL; ?>/posts/<?php echo $post['id']; ?>">View</a>
<a href="<?php echo BASE_URL; ?>/posts/<?php echo $post['id']; ?>/edit">Edit</a>
<form action="<?php echo BASE_URL; ?>/posts/<?php echo $post['id']; ?>/delete" method="post" style="display:inline;">
<button type="submit">Delete</button>
</form>
</div>
</li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
<h1><?php echo $post['title']; ?></h1>
<p><?php echo nl2br($post['body']); ?></p>
<p><a href="<?php echo BASE_URL; ?>/posts">Back to posts</a></p>
<?php
namespace App\Controllers;
use App\Services\PostService;
use Core\Controller;
class PostsController extends Controller
{
private PostService $postService;
public function __construct()
{
$this->postService = new PostService();
}
// Show all posts
public function index(): void
{
$posts = $this->postService->getAllPosts();
$this->view('posts/index', ['posts' => $posts]);
}
// Show a single post
public function show(int $id): void
{
$post = $this->postService->getPostById($id);
if (!$post) {
$this->notFound();
return;
}
$this->view('posts/show', ['post' => $post]);
}
// Show form to create new post
public function create(): void
{
$this->view('posts/create', [
'title' => '',
'body' => '',
'errors' => []
]);
}
// Save new post
public function store(): void
{
$errors = $this->postService->validatePostData($_POST);
if (!empty($errors)) {
$this->view('posts/create', [
'title' => $_POST['title'] ?? '',
'body' => $_POST['body'] ?? '',
'errors' => $errors
]);
return;
}
$this->postService->createPost($_POST);
$this->redirect('/posts');
}
// Show form to edit post
public function edit(int $id): void
{
$post = $this->postService->getPostById($id);
if (!$post) {
$this->notFound();
return;
}
$this->view('posts/edit', [
'id' => $id,
'title' => $post['title'],
'body' => $post['body'],
'errors' => []
]);
}
// Update existing post
public function update(int $id): void
{
$errors = $this->postService->validatePostData($_POST);
if (!empty($errors)) {
$this->view('posts/edit', [
'id' => $id,
'title' => $_POST['title'] ?? '',
'body' => $_POST['body'] ?? '',
'errors' => $errors
]);
return;
}
$this->postService->updatePost($id, $_POST);
$this->redirect('/posts');
}
// Delete post
public function destroy(int $id): void
{
$this->postService->deletePost($id);
$this->redirect('/posts');
}
// Show 404 error
private function notFound(): void
{
http_response_code(404);
echo 'Post not found';
}
// Redirect to a path
private function redirect(string $path): void
{
$url = rtrim(BASE_URL, '/') . $path;
header('Location: ' . $url);
exit;
}
}
<?php
namespace App\Interfaces;
interface PostRepositoryInterface
{
public function all(): array;
public function find(int $id): ?array;
public function create(array $data): bool;
public function update(int $id, array $data): bool;
public function delete(int $id): bool;
}
<?php
namespace App\Models;
use App\Interfaces\PostRepositoryInterface;
use Core\Database;
use PDO;
class PostRepository implements PostRepositoryInterface
{
private PDO $db;
public function __construct()
{
$this->db = Database::getConnection();
}
public function all(): array
{
$stmt = $this->db->query('SELECT id, title, body, created_at FROM posts ORDER BY created_at DESC');
return $stmt->fetchAll() ?: [];
}
public function find(int $id): ?array
{
$stmt = $this->db->prepare('SELECT id, title, body, created_at FROM posts WHERE id = :id');
$stmt->execute([':id' => $id]);
$post = $stmt->fetch();
return $post ?: null;
}
public function create(array $data): bool
{
$stmt = $this->db->prepare('INSERT INTO posts (title, body) VALUES (:title, :body)');
return $stmt->execute([
':title' => $data['title'] ?? '',
':body' => $data['body'] ?? '',
]);
}
public function update(int $id, array $data): bool
{
$stmt = $this->db->prepare('UPDATE posts SET title = :title, body = :body WHERE id = :id');
return $stmt->execute([
':id' => $id,
':title' => $data['title'] ?? '',
':body' => $data['body'] ?? '',
]);
}
public function delete(int $id): bool
{
$stmt = $this->db->prepare('DELETE FROM posts WHERE id = :id');
return $stmt->execute([':id' => $id]);
}
}
<?php
namespace App\Services;
use App\Interfaces\PostRepositoryInterface;
use App\Models\PostRepository;
class PostService
{
private PostRepositoryInterface $repository;
public function __construct()
{
$this->repository = new PostRepository();
}
public function getAllPosts(): array
{
return $this->repository->all();
}
public function getPostById(int $id): ?array
{
return $this->repository->find($id);
}
public function createPost(array $data): bool
{
// Validation is done in controller, but we ensure data is clean
return $this->repository->create([
'title' => trim($data['title'] ?? ''),
'body' => trim($data['body'] ?? ''),
]);
}
public function updatePost(int $id, array $data): bool
{
// Validation is done in controller, but we ensure data is clean
return $this->repository->update($id, [
'title' => trim($data['title'] ?? ''),
'body' => trim($data['body'] ?? ''),
]);
}
public function deletePost(int $id): bool
{
return $this->repository->delete($id);
}
public function validatePostData(array $data): array
{
$errors = [];
$title = trim($data['title'] ?? '');
$body = trim($data['body'] ?? '');
if ($title == null) {
$errors['title'] = 'Title is required';
}
if ($body == null) {
$errors['body'] = 'Body is required';
}
return $errors;
}
}
```
This example shows a simple way to implement URL routing in a small, framework-style MVC setup while keeping the router independent from any specific application logic.
The Router class only concerns itself with mapping an HTTP method and a URL pattern to a controller action. Routes are registered by the application (for example in index.php), which makes the router itself reusable and easy to extend by app developers without modifying its internals. Dynamic segments such as `/posts/{id}` are converted to regular expressions and passed as parameters to the controller method.
The front controller acts as a single entry point, bootstraps the environment, defines the routes, and dispatches the current request. This follows the same front-controller pattern used by most MVC frameworks.
Controllers remain thin and delegate business logic to services, while repositories handle persistence via PDO. Views are simple PHP templates with no routing or database logic. This separation keeps responsibilities clear and allows new resources or applications to be added by registering new routes and controllers, which fits well with a resource-oriented architecture.
```php
<?php
namespace Core;
class Router
{
private $routes = [];
public function get($path, $action)
{
$this->addRoute('GET', $path, $action);
}
public function post($path, $action)
{
$this->addRoute('POST', $path, $action);
}
private function addRoute($method, $path, $action)
{
$pattern = preg_replace('/\{id\}/', '([0-9]+)', $path);
$pattern = "#^$pattern$#";
$this->routes[$method][$pattern] = $action;
}
public function dispatch($path, $method)
{
$method = strtoupper($method);
$path = $path ?: '/';
foreach ($this->routes[$method] ?? [] as $pattern => $action) {
if (preg_match($pattern, $path, $matches)) {
// Extract numeric parameters (skip full match at index 0)
$params = [];
for ($i = 1; $i < count($matches); $i++) {
$params[] = (int)$matches[$i];
}
$this->call($action, $params);
return;
}
}
http_response_code(404);
echo 'Page not found';
}
private function call($action, $params)
{
$parts = explode('@', $action);
$controller = new $parts[0]();
call_user_func_array([$controller, $parts[1]], $params);
}
}
````
```php
<?php
require_once dirname(__DIR__) . '/vendor/autoload.php';
require_once dirname(__DIR__) . '/config/config.php';
use App\Controllers\PostsController;
use Core\Router;
// Simple exception handler (optional but fine)
set_exception_handler(function (Throwable $e) {
http_response_code(500);
echo 'An error occurred.';
});
$router = new Router();
// Define routes
$router->get('/', PostsController::class . '@index');
$router->get('/posts', PostsController::class . '@index');
$router->get('/posts/create', PostsController::class . '@create');
$router->post('/posts', PostsController::class . '@store');
$router->get('/posts/{id}', PostsController::class . '@show');
$router->get('/posts/{id}/edit', PostsController::class . '@edit');
$router->post('/posts/{id}/update', PostsController::class . '@update');
$router->post('/posts/{id}/delete', PostsController::class . '@destroy');
$path = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?: '/';
$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
$router->dispatch($path, $method);
```
```php
<?php
namespace Core;
use PDO;
use PDOException;
class Database
{
public static function getConnection(): PDO
{
static $pdo = null;
if ($pdo instanceof PDO) {
return $pdo;
}
$dsn = 'mysql:host=' . DB_HOST . ';dbname=' . DB_NAME . ';charset=utf8mb4';
try {
$pdo = new PDO($dsn, DB_USER, DB_PASS, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
} catch (PDOException $e) {
throw new \RuntimeException('Database connection failed: ' . $e->getMessage());
}
return $pdo;
}
}
```
```php
<?php
namespace Core;
class Controller
{
protected function view(string $template, array $data = []): void
{
$path = dirname(__DIR__) . '/app/Views/' . $template . '.php';
if (!file_exists($path)) {
throw new \RuntimeException("View {$template} not found");
}
extract($data, EXTR_SKIP);
require $path;
}
}
<h1>Create Post</h1>
<form action="<?php echo BASE_URL; ?>/posts" method="post">
<div>
<label>Title</label>
<input type="text" name="title" value="<?php echo $title; ?>">
<?php if (!empty($errors['title'])): ?>
<p><?php echo $errors['title']; ?></p>
<?php endif; ?>
</div>
<div>
<label>Body</label>
<textarea name="body"><?php echo $body; ?></textarea>
<?php if (!empty($errors['body'])): ?>
<p><?php echo $errors['body']; ?></p>
<?php endif; ?>
</div>
<button type="submit">Save</button>
</form>
<p><a href="<?php echo BASE_URL; ?>/posts">Back to posts</a></p>
<h1>Edit Post</h1>
<form action="<?php echo BASE_URL; ?>/posts/<?php echo $id; ?>/update" method="post">
<div>
<label>Title</label>
<input type="text" name="title" value="<?php echo $title; ?>">
<?php if (!empty($errors['title'])): ?>
<p><?php echo $errors['title']; ?></p>
<?php endif; ?>
</div>
<div>
<label>Body</label>
<textarea name="body"><?php echo $body; ?></textarea>
<?php if (!empty($errors['body'])): ?>
<p><?php echo $errors['body']; ?></p>
<?php endif; ?>
</div>
<button type="submit">Update</button>
</form>
<p><a href="<?php echo BASE_URL; ?>/posts">Back to posts</a></p>
<h1>Posts</h1>
<p><a href="<?php echo BASE_URL; ?>/posts/create">Create Post</a></p>
<?php if (empty($posts)): ?>
<p>No posts yet.</p>
<?php else: ?>
<ul>
<?php foreach ($posts as $post): ?>
<li>
<strong><?php echo $post['title']; ?></strong>
<div>
<a href="<?php echo BASE_URL; ?>/posts/<?php echo $post['id']; ?>">View</a>
<a href="<?php echo BASE_URL; ?>/posts/<?php echo $post['id']; ?>/edit">Edit</a>
<form action="<?php echo BASE_URL; ?>/posts/<?php echo $post['id']; ?>/delete" method="post" style="display:inline;">
<button type="submit">Delete</button>
</form>
</div>
</li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
<h1><?php echo $post['title']; ?></h1>
<p><?php echo nl2br($post['body']); ?></p>
<p><a href="<?php echo BASE_URL; ?>/posts">Back to posts</a></p>
```
```php
<?php
namespace App\Controllers;
use App\Services\PostService;
use Core\Controller;
class PostsController extends Controller
{
private PostService $postService;
public function __construct()
{
$this->postService = new PostService();
}
// Show all posts
public function index(): void
{
$posts = $this->postService->getAllPosts();
$this->view('posts/index', ['posts' => $posts]);
}
// Show a single post
public function show(int $id): void
{
$post = $this->postService->getPostById($id);
if (!$post) {
$this->notFound();
return;
}
$this->view('posts/show', ['post' => $post]);
}
// Show form to create new post
public function create(): void
{
$this->view('posts/create', [
'title' => '',
'body' => '',
'errors' => []
]);
}
// Save new post
public function store(): void
{
$errors = $this->postService->validatePostData($_POST);
if (!empty($errors)) {
$this->view('posts/create', [
'title' => $_POST['title'] ?? '',
'body' => $_POST['body'] ?? '',
'errors' => $errors
]);
return;
}
$this->postService->createPost($_POST);
$this->redirect('/posts');
}
// Show form to edit post
public function edit(int $id): void
{
$post = $this->postService->getPostById($id);
if (!$post) {
$this->notFound();
return;
}
$this->view('posts/edit', [
'id' => $id,
'title' => $post['title'],
'body' => $post['body'],
'errors' => []
]);
}
// Update existing post
public function update(int $id): void
{
$errors = $this->postService->validatePostData($_POST);
if (!empty($errors)) {
$this->view('posts/edit', [
'id' => $id,
'title' => $_POST['title'] ?? '',
'body' => $_POST['body'] ?? '',
'errors' => $errors
]);
return;
}
$this->postService->updatePost($id, $_POST);
$this->redirect('/posts');
}
// Delete post
public function destroy(int $id): void
{
$this->postService->deletePost($id);
$this->redirect('/posts');
}
// Show 404 error
private function notFound(): void
{
http_response_code(404);
echo 'Post not found';
}
// Redirect to a path
private function redirect(string $path): void
{
$url = rtrim(BASE_URL, '/') . $path;
header('Location: ' . $url);
exit;
}
}
```
```php
<?php
namespace App\Interfaces;
interface PostRepositoryInterface
{
public function all(): array;
public function find(int $id): ?array;
public function create(array $data): bool;
public function update(int $id, array $data): bool;
public function delete(int $id): bool;
}
```
```php
<?php
namespace App\Models;
use App\Interfaces\PostRepositoryInterface;
use Core\Database;
use PDO;
class PostRepository implements PostRepositoryInterface
{
private PDO $db;
public function __construct()
{
$this->db = Database::getConnection();
}
public function all(): array
{
$stmt = $this->db->query('SELECT id, title, body, created_at FROM posts ORDER BY created_at DESC');
return $stmt->fetchAll() ?: [];
}
public function find(int $id): ?array
{
$stmt = $this->db->prepare('SELECT id, title, body, created_at FROM posts WHERE id = :id');
$stmt->execute([':id' => $id]);
$post = $stmt->fetch();
return $post ?: null;
}
public function create(array $data): bool
{
$stmt = $this->db->prepare('INSERT INTO posts (title, body) VALUES (:title, :body)');
return $stmt->execute([
':title' => $data['title'] ?? '',
':body' => $data['body'] ?? '',
]);
}
public function update(int $id, array $data): bool
{
$stmt = $this->db->prepare('UPDATE posts SET title = :title, body = :body WHERE id = :id');
return $stmt->execute([
':id' => $id,
':title' => $data['title'] ?? '',
':body' => $data['body'] ?? '',
]);
}
public function delete(int $id): bool
{
$stmt = $this->db->prepare('DELETE FROM posts WHERE id = :id');
return $stmt->execute([':id' => $id]);
}
}
```
```php
<?php
namespace App\Services;
use App\Interfaces\PostRepositoryInterface;
use App\Models\PostRepository;
class PostService
{
private PostRepositoryInterface $repository;
public function __construct()
{
$this->repository = new PostRepository();
}
public function getAllPosts(): array
{
return $this->repository->all();
}
public function getPostById(int $id): ?array
{
return $this->repository->find($id);
}
public function createPost(array $data): bool
{
// Validation is done in controller, but we ensure data is clean
return $this->repository->create([
'title' => trim($data['title'] ?? ''),
'body' => trim($data['body'] ?? ''),
]);
}
public function updatePost(int $id, array $data): bool
{
// Validation is done in controller, but we ensure data is clean
return $this->repository->update($id, [
'title' => trim($data['title'] ?? ''),
'body' => trim($data['body'] ?? ''),
]);
}
public function deletePost(int $id): bool
{
return $this->repository->delete($id);
}
public function validatePostData(array $data): array
{
$errors = [];
$title = trim($data['title'] ?? '');
$body = trim($data['body'] ?? '');
if ($title == null) {
$errors['title'] = 'Title is required';
}
if ($body == null) {
$errors['body'] = 'Body is required';
}
return $errors;
}
}
```