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.)
<?phpnamespaceApp\Controllers;
useCore\Attributes\Route;
useCore\Attributes\Controller;
useCore\Http\Request;
#[Controller(prefix: '/api')]
class ApiController extends BaseController
{
#[Route('/users', 'GET', name: 'users.index')]
publicfunctionindex(): array
{
return ['users' => []];
}
#[Route('/users/{id}', 'GET')]
publicfunctionshow(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:
publicfunctionindex()
{
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.
<?phpnamespaceApp\Models;
useCore\Model;
useCore\Relations\HasMany;
class User extends Model
{
// Optional: Override table nameprotectedstring$table = 'users';
// Define relationshipspublicfunctionposts(): HasMany
{
return$this->hasMany(Post::class);
}
}// Create$user = User::create([
'name' => 'John Doe',
'email' => 'john@example.com'
]);
// 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 modelpublicfunctionuser(): BelongsTo
{
return$this->belongsTo(User::class);
}
// Eager load posts with users$users = User::with('posts')->get();
foreach ($usersas$user) {
// Relationships are accessible as propertiesforeach ($user->postsas$post) {
echo$post->title;
}
}The framework includes a simple migration system to manage your database schema.
Create a new file in the migrations/ directory.
<?phpuseCore\Database\Migration;
class CreatePostsTable extends Migration
{
publicfunctionup()
{
$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();
}
publicfunctiondown()
{
$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<?phpnamespaceApp\Services;
useCore\Attributes\Service;
#[Service(singleton: true)]
class UserService
{
publicfunction__construct(
privateUser$user
) {}
publicfunctiongetAllUsers(): array
{
return$this->user->all();
}
}<?phpnamespaceApp\Middleware;
useCore\Http\Request;
useCore\Http\Response;
class AuthMiddleware implements MiddlewareInterface
{
publicfunctionhandle(Request$request): ?Response
{
if (!$request->header('Authorization')) {
return (newResponse())->status(401)->json(['error' => 'Unauthorized']);
}
returnnull; // 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":"john@example.com"}'- PHP 8.0 or higher
- PDO extension
- Composer
MIT License
Pull requests are welcome. For major changes, please open an issue first.