A simple PHP MVC framework.
- Attribute-Based Routing - Use PHP attributes for clean route definitions
- Dependency Injection - Automatic DI container with singleton support
- Middleware System - Flexible middleware with attribute support
- Database Abstraction - PDO wrapper with ActiveRecord-style ORM
- Twig Templates - Powerful and secure template engine support
-
Clone the repository
git clone <repository-url> cd php-mvc-framework
-
Install dependencies
composer install
-
Setup environment
cp .env.example .env
-
Create database (for SQLite)
touch storage/database.sqlite
-
Start development server
composer serve
-
Visit your application Open http://localhost:8000 in your browser
mvc-framework/
├── app/ # Application code
│ ├── Controllers/ # HTTP controllers
│ ├── Models/ # Data models
│ ├── Services/ # Business logic
│ └── Middleware/ # HTTP middleware
├── config/ # Configuration files
├── core/ # Framework core
│ ├── Attributes/ # PHP attributes
│ └── Http/ # HTTP components
├── public/ # Web server document root
├── views/ # Template files
└── storage/ # App storage (logs, cache, etc.)
<?php
namespace App\Controllers;
use Core\Attributes\Route;
use Core\Attributes\Controller;
use Core\Http\Request;
#[Controller(prefix: '/api')]
class ApiController extends BaseController
{
#[Route('/users', 'GET', name: 'users.index')]
public function index(): array
{
return ['users' => []];
}
#[Route('/users/{id}', 'GET')]
public function show(int $id): array
{
return ['user' => ['id' => $id]];
}
}The framework uses Twig as its template engine. Templates are located in the views/ directory.
In your controller:
public function index()
{
return $this->view()->render('home', [
'name' => 'John Doe'
]);
}<!DOCTYPE html>
<html>
<head>
<title>{{ app_name }}</title>
</head>
<body>
<h1>Hello, {{ name }}!</h1>
<p>Welcome to our simple MVC framework.</p>
</body>
</html>The framework includes a powerful ActiveRecord-style ORM with support for relationships and eager loading.
Models should extend Core\Model. The table name is automatically derived from the class name (snake_case + plural), or can be explicitly defined.
<?php
namespace App\Models;
use Core\Model;
use Core\Relations\HasMany;
class User extends Model
{
// Optional: Override table name
protected string $table = 'users';
// Define relationships
public function posts(): HasMany
{
return $this->hasMany(Post::class);
}
}// Create
$user = User::create([
'name' => 'John Doe',
'email' => '[email protected]'
]);
// Read
$users = User::all();
$user = User::find(1);
$activeUsers = User::where('status', 'active');
// Update
$user->name = 'Jane Doe';
$user->save();
// Delete
$user->delete();Support for hasMany and belongsTo relationships with efficient eager loading to solve the N+1 problem.
// Define the inverse relationship in Post model
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
// Eager load posts with users
$users = User::with('posts')->get();
foreach ($users as $user) {
// Relationships are accessible as properties
foreach ($user->posts as $post) {
echo $post->title;
}
}The framework includes a simple migration system to manage your database schema.
Create a new file in the migrations/ directory.
<?php
use Core\Database\Migration;
class CreatePostsTable extends Migration
{
public function up()
{
$table = $this->table('posts');
$table->addColumn('id', 'id') // Helper for auto-incrementing primary key
->addColumn('user_id', 'integer')
->addColumn('title', 'string')
->addColumn('body', 'text')
->addColumn('created_at', 'datetime')
->foreign('user_id', 'id', 'users') // Foreign key
->create();
}
public function down()
{
$this->table('posts')->drop();
}
}Use the console script to run migrations.
# Run all pending migrations
php bin/console migrate
# Rollback the last migration batch
php bin/console migrate:rollback
# Rollback all migrations and run them again
php bin/console migrate:refresh
# Drop all tables and re-run all migrations
php bin/console migrate:fresh<?php
namespace App\Services;
use Core\Attributes\Service;
#[Service(singleton: true)]
class UserService
{
public function __construct(
private User $user
) {}
public function getAllUsers(): array
{
return $this->user->all();
}
}<?php
namespace App\Middleware;
use Core\Http\Request;
use Core\Http\Response;
class AuthMiddleware implements MiddlewareInterface
{
public function handle(Request $request): ?Response
{
if (!$request->header('Authorization')) {
return (new Response())->status(401)->json(['error' => 'Unauthorized']);
}
return null; // Continue
}
}Edit config/database.php to configure your database connection:
return [
'default' => 'mysql',
'connections' => [
'mysql' => [
'driver' => 'mysql',
'host' => 'localhost',
'database' => 'your_database',
'username' => 'your_username',
'password' => 'your_password',
],
],
];Edit config/app.php for application settings:
return [
'name' => 'Your App Name',
'debug' => true,
'controllers' => [
\App\Controllers\HomeController::class,
\App\Controllers\UserController::class,
],
];curl -X GET http://localhost:8000/api/userscurl -X POST http://localhost:8000/api/users \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your-token" \
-d '{"name":"John Doe","email":"[email protected]"}'- PHP 8.0 or higher
- PDO extension
- Composer
MIT License
Pull requests are welcome. For major changes, please open an issue first.