A lightweight PHP MVC framework built for fast, enjoyable web development. LaraCore provides routing, query building, form validation, migrations, seeders, CLI tooling, mail service, and REST API support — all without heavy dependencies.
Note: This framework is actively under development.
- Requirements
- Features
- Installation
- Directory Structure
- Configuration
- Routing
- Controllers
- Models
- Views & Layouts
- Validation
- Middleware
- Database Migrations
- Seeders & Factories
- Authentication & Sessions
- Mail Service
- REST API
- Helper Functions
- CLI Commands
- Roadmap
- License
- PHP >= 7.4 (8.0+ recommended)
- Composer
- MySQL 5.7+
- Apache (mod_rewrite enabled) or Nginx
- Simple and fast routing with named routes and parameter binding
- Custom query builder with PDO prepared statements
- MVC architecture with base Controller, Model, and View classes
- Multi-layout view template system
- Form validation with built-in rules
- CSRF token support
- Middleware pipeline (auth, guest, admin, API auth)
- Mail service with SMTP support (PHPMailer) and Mailable classes
- REST API support with optional bearer token authentication
- Environment variables via custom
.envloader - Database migrations with fluent Blueprint API
- Database seeders and factories
- Session management and password hashing
- Flash messages and input sanitization helpers
- CLI tool (
laracore) for code generation and migrations - Docker support (PHP 8.2 + Apache + MySQL 8.0 + Mailpit)
- Full ORM with relationships
- User management system
- Multi-authentication system
- Blade-style templating
1. Clone the repository
git clone https://github.com/mdabbas-cse/php-mvc-framework.git
cd php-mvc-framework2. Install dependencies
composer install3. Set up environment
cp .env.example .envEdit .env with your settings:
APP_NAME=Laracore
APP_ENV=local
APP_DEBUG=true
APP_URL=http://localhost:8000/
APP_DEFAULT_LAYOUT=default
APP_TIME_ZONE=Asia/Dhaka
DB_HOST=localhost
DB_PORT=3306
DB_DATABASE=laracore
DB_USERNAME=root
DB_PASSWORD=
MAIL_HOST=smtp.gmail.com
MAIL_PORT=587
MAIL_ENCRYPTION=tls
MAIL_USERNAME=your@gmail.com
MAIL_PASSWORD=your-app-password
MAIL_FROM_ADDRESS=your@gmail.com
MAIL_FROM_NAME=Laracore4. Run migrations
php laracore migrate5. Start the development server
php laracore serve
# or
php -S localhost:8000Visit http://localhost:8000
The project ships with a fully configured Docker environment — PHP 8.2 + Apache, MySQL 8.0, and Mailpit for local email testing.
1. Copy the environment file
cp .env.example .env2. Build and start all containers
docker compose up -d3. Run migrations
docker compose exec app php laracore migrateServices:
| Service | URL / Connection |
|---|---|
| App (PHP/Apache) | http://localhost:8000 |
| Mailpit web UI | http://localhost:8025 |
| MySQL (from host) | localhost:3307 |
Useful Docker commands:
docker compose up -d # Start all services
docker compose down # Stop all services
docker compose logs -f app # Stream app logs
docker compose exec app bash # Shell into the app container
docker compose exec app php laracore migrateConnect to MySQL from a GUI tool (TablePlus, DBeaver, etc.):
- Host:
localhost, Port:3307 - Database:
laracore, Username:laracore, Password:secret
The app container talks to MySQL over the internal Docker network at
db:3306— no config changes needed.
php-mvc-framework/
├── app/
│ ├── Http/
│ │ ├── Controllers/
│ │ │ ├── HomeController.php
│ │ │ ├── UserController.php
│ │ │ ├── Api/
│ │ │ │ └── UserApiController.php
│ │ │ └── Auth/
│ │ │ └── AuthController.php
│ │ ├── Middlewares/
│ │ │ ├── AuthMiddleware.php
│ │ │ ├── GuestMiddleware.php
│ │ │ ├── AdminMiddleware.php
│ │ │ └── AuthApiMiddleware.php
│ │ └── Kernel.php
│ ├── Mail/
│ │ └── WelcomeMail.php
│ ├── Models/
│ │ └── Users.php
│ └── Providers/
│ ├── AppServiceProvider.php
│ └── RouteServiceProvider.php
├── config/
│ └── Config.php
├── database/
│ ├── migrations/
│ ├── Seeders/
│ └── Factories/
├── docker/
│ └── apache/
│ └── 000-default.conf
├── framework/
│ ├── Application.php
│ ├── Bootstrap.php
│ ├── Controller.php
│ ├── Request.php
│ ├── Response.php
│ ├── View.php
│ ├── Sessions.php
│ ├── Validation.php
│ ├── Helpers.php
│ ├── Mail/
│ │ ├── Mailable.php
│ │ └── Mailer.php
│ ├── Routers/
│ ├── Db/
│ ├── Console/
│ └── Helpers/
├── resources/
│ ├── views/
│ │ ├── layouts/
│ │ ├── auth/
│ │ ├── mail/
│ │ │ └── welcome.php
│ │ └── *.php
│ └── assets/
│ ├── css/
│ └── js/
├── routes/
│ ├── web.php
│ └── api.php
├── storage/
│ ├── logs/
│ ├── cache/
│ └── sessions/
├── .env
├── .env.example
├── .dockerignore
├── .htaccess
├── Dockerfile
├── docker-compose.yml
├── index.php
├── laracore
└── composer.json
All configuration is in config/Config.php, driven by .env values.
Key .env variables:
# Application
APP_NAME=Laracore
APP_ENV=local
APP_DEBUG=true
APP_URL=http://localhost:8000/
APP_DEFAULT_LAYOUT=default
APP_TIME_ZONE=Asia/Dhaka
# Database
DB_HOST=localhost
DB_PORT=3306
DB_DATABASE=laracore
DB_USERNAME=root
DB_PASSWORD=
# Mail
MAIL_HOST=mailpit
MAIL_PORT=1025
MAIL_ENCRYPTION=
MAIL_USERNAME=
MAIL_PASSWORD=
MAIL_FROM_ADDRESS=hello@example.com
MAIL_FROM_NAME=Laracore
# API Token (optional)
API_TOKEN_CHECK=false
API_TOKEN_KEY=your-secret-keyAccess config values in code:
use LaraCore\Framework\Configuration;
$mailConfig = Configuration::get('mail');
$allConfig = Configuration::all();Define routes in routes/web.php for web and routes/api.php for API endpoints.
Router::get($uri, $action);
Router::post($uri, $action);
Router::put($uri, $action);
Router::delete($uri, $action);// Closure
Router::get('/', function (Request $request, Response $response) {
return view('welcome');
});
// Controller method
Router::get('/users/{id}', [UserController::class, 'show']);
// View shorthand (renders view directly, no controller)
Router::get('/about', 'about');// Basic
Router::get('/user/{id}', [UserController::class, 'show']);
// Regex-constrained
Router::get('/post/{id:\d+}', [PostController::class, 'show']);
// Access inside controller
$id = $request->getParam('id');Router::get('/dashboard', [DashboardController::class, 'index'])
->middleware('auth')
->name('dashboard');
// Generate URL
$url = Router::route('dashboard');
// Redirect by name
return $response->route('dashboard');php laracore make:controller UserControllernamespace LaraCore\App\Http\Controllers;
use LaraCore\Framework\Controller;
use LaraCore\Framework\Request;
use LaraCore\Framework\Response;
use LaraCore\App\Models\Users;
class UserController extends Controller
{
public function index(Request $request, Response $response)
{
$users = (new Users())->getAll();
return $this->view('users.index', compact('users'));
}
public function store(Request $request, Response $response)
{
$this->validation($request, [
'name' => ['required'],
'email' => ['required', 'email'],
]);
if ($this->isValidate()) {
$user = new Users();
$user->loadData($request->all());
$user->save();
return $response->redirect('/users');
}
return $this->view('users.create');
}
}| Method | Description |
|---|---|
$this->view($name, $data) |
Render a view |
$this->setLayout($layout) |
Override default layout |
$this->validation($request, $rules) |
Run validation |
$this->isValidate() |
Check if validation passed |
$this->errors() |
Get validation errors array |
php laracore make:model Usernamespace LaraCore\App\Models;
use LaraCore\Framework\Db\DataModel;
class Users extends DataModel
{
protected $table = 'users';
protected $fillable = ['name', 'email', 'password'];
public function tableName(): string { return $this->table; }
public function attributes(): array { return $this->fillable; }
}CRUD:
$model = new Users();
// Create
$model->name = 'John';
$model->email = 'john@example.com';
$model->password = Hash::make('secret');
$model->save();
// Read
$user = $model->find(1);
$all = $model->getAll();
$one = $model->findOne(['email' => 'a@b.com']);
$rows = $model->selectWhere(['name', 'email'], ['active' => 1]);
// Update
$user = $model->find(1);
$user->name = 'Jane';
$user->update(1);
// Delete
$model->delete(1);
// Load array into model properties
$model->loadData($request->all());Views live in resources/views/, layouts in resources/views/layouts/.
// Render with data
return view('home', ['title' => 'Home Page']);
return $this->view('home', ['title' => 'Home Page']);Layout (resources/views/layouts/default.php):
<!DOCTYPE html>
<html>
<head>
<title><?= $this->siteTitle() ?></title>
<?= $this->content('head') ?>
</head>
<body>
<?= $this->content('body') ?>
<?= $this->content('script') ?>
</body>
</html>View (resources/views/home.php):
<?php $this->setSiteTile('Home'); ?>
<?php $this->start('head'); ?>
<link rel="stylesheet" href="<?= css('style.css') ?>">
<?php $this->end(); ?>
<?php $this->start('body'); ?>
<h1>Hello, <?= $title ?>!</h1>
<?php if ($this->hasError('email')): ?>
<span class="error"><?= $this->error('email') ?></span>
<?php endif; ?>
<input type="email" name="email" value="<?= old('email') ?>">
<?php $this->end(); ?>$this->validation($request, [
'firstname' => ['required'],
'email' => ['required', 'email', ['unique', 'class' => Users::class]],
'password' => ['required', ['min', 'min' => 8], ['max', 'max' => 64]],
'confirmPassword' => ['required', ['match', 'match' => 'password']],
]);
if ($this->isValidate()) {
// proceed
}| Rule | Usage |
|---|---|
required |
Field must not be empty |
email |
Must be a valid email address |
min |
['min', 'min' => 8] — minimum length |
max |
['max', 'max' => 64] — maximum length |
match |
['match', 'match' => 'fieldName'] — must equal another field |
unique |
['unique', 'class' => Model::class] — must not exist in DB |
Register aliases in app/Http/Kernel.php:
public static $middlewareAliases = [
'auth' => \LaraCore\App\Http\Middlewares\AuthMiddleware::class,
'guest' => \LaraCore\App\Http\Middlewares\GuestMiddleware::class,
'admin' => \LaraCore\App\Http\Middlewares\AdminMiddleware::class,
'authApi' => \LaraCore\App\Http\Middlewares\AuthApiMiddleware::class,
];Create a middleware:
namespace LaraCore\App\Http\Middlewares;
use LaraCore\Framework\Request;
class AuthMiddleware
{
public function handle(Request $request, $next)
{
if (!isset($_SESSION['user'])) {
return redirect()->route('login');
}
return $next($request);
}
}Attach to a route:
Router::get('/dashboard', [DashController::class, 'index'])->middleware('auth');php laracore make:migration create_posts_tablenamespace LaraCore\Database\Migrations;
use LaraCore\Framework\Db\Migrations\Blueprint;
use LaraCore\Framework\Db\Migrations\Migration;
class Post_2024_01_01_000000 extends Migration
{
public function up()
{
$this->create('posts', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->string('slug')->unique();
$table->text('content')->nullable();
$table->integer('user_id');
$table->boolean('status')->default(true);
$table->timestamps();
});
}
public function down()
{
$this->drop('posts');
}
}Blueprint column types:
| Method | SQL Type |
|---|---|
$table->id() |
INT PRIMARY KEY AUTO_INCREMENT |
$table->string($name, $length) |
VARCHAR |
$table->integer($name) |
INT |
$table->bigInteger($name) |
BIGINT |
$table->text($name) |
TEXT |
$table->boolean($name) |
TINYINT(1) |
$table->timestamps() |
created_at, updated_at |
Modifiers: .nullable() .default($value) .unique() .comment($text)
php laracore migrate # Run all pending migrations
php laracore migration:rollback # Roll back all migrations// database/Factories/UserFactory.php
class UserFactory extends Factory
{
public function definition(): array
{
return [
'name' => 'John Doe',
'email' => 'john@example.com',
'password' => 'secret',
];
}
public function create(): void
{
$this->make(Users::class, $this->definition());
}
public static function new(): static { return new static(); }
}// database/Seeders/UserSeeder.php
class UserSeeder extends Seeder
{
public function run(): void
{
UserFactory::new()->create();
}
}php laracore db:seed # Run DatabaseSeeder
php laracore db:seeder UserSeeder # Run a specific seederuse LaraCore\Framework\Sessions;
use LaraCore\Framework\Helpers\Hash;
// Store user in session
Session::set('user', $user);
$user = Session::get('user');
// Password hashing
$hash = Hash::make('plaintext');
if (Hash::verify('plaintext', $hash)) { /* authenticated */ }// Flash messages
$flash = new FlashMessages();
$flash->setFlash('success', 'Profile updated.');
echo $flash->getFlash('success');The mail service is built on PHPMailer. Create a Mailable class, point it at a view, and call send().
# Manually create in app/Mail/// app/Mail/WelcomeMail.php
namespace LaraCore\App\Mail;
use LaraCore\Framework\Mail\Mailable;
class WelcomeMail extends Mailable
{
public function __construct(private array $user) {}
public function build(): self
{
return $this
->subject('Welcome to ' . $_ENV['APP_NAME'])
->view('mail.welcome', ['user' => $this->user]);
}
}// In a controller
use LaraCore\App\Mail\WelcomeMail;
// Fluent API
(new WelcomeMail($user))->to($user['email'], $user['name'])->send();
// Using the helper
send_mail((new WelcomeMail($user))->to($user['email']));| Method | Description |
|---|---|
->to($email, $name) |
Set recipient |
->from($email, $name) |
Override sender (falls back to config) |
->subject($text) |
Set email subject |
->view($name, $data) |
Render a PHP view as the HTML body |
->html($html) |
Set raw HTML body directly |
->text($text) |
Set plain-text fallback body |
->attach($path, $name) |
Attach a file |
->send() |
Build and send the email |
Mail views live in resources/views/mail/ and are plain PHP files (no layout wrapping):
// resources/views/mail/welcome.php
<!DOCTYPE html>
<html>
<body>
<h1>Welcome, <?= htmlspecialchars($user['name']) ?>!</h1>
<p>Your account has been created.</p>
</body>
</html># Local development (Docker — Mailpit, no auth needed)
MAIL_HOST=mailpit
MAIL_PORT=1025
MAIL_ENCRYPTION=
MAIL_USERNAME=
MAIL_PASSWORD=
# Production (e.g. Gmail)
MAIL_HOST=smtp.gmail.com
MAIL_PORT=587
MAIL_ENCRYPTION=tls
MAIL_USERNAME=you@gmail.com
MAIL_PASSWORD=your-app-password
MAIL_FROM_ADDRESS=hello@example.com
MAIL_FROM_NAME=LaracoreIn Docker, all sent emails are captured by Mailpit at
http://localhost:8025— nothing is actually delivered.
Define API routes in routes/api.php. All routes are automatically prefixed with /api and return JSON headers.
Router::get('/users', [UserApiController::class, 'index']);
Router::get('/users/{id}', [UserApiController::class, 'show']);
Router::post('/users', [UserApiController::class, 'store']);class UserApiController extends Controller
{
public function index(Request $request, Response $response)
{
return $response->json(['success' => true, 'data' => (new Users())->getAll()]);
}
}Optional token auth — enable in .env:
API_TOKEN_CHECK=true
API_TOKEN_KEY=your-secret-keyClient must send: Authorization: Bearer your-secret-key
// Views & assets
view('home', $data)
assets('img/logo.png') // /resources/assets/img/logo.png
css('style.css') // /resources/assets/css/style.css
js('app.js') // /resources/assets/js/app.js
app_url('/about') // full URL from APP_URL
// Redirection
redirect()->redirect('/login')
redirect()->route('user.show', ['id' => 1])
// Forms
old('email') // previously submitted value
errors('email') // validation error for field
// Mail
send_mail($mailable) // send a Mailable instance
// Paths
base_path('storage/logs') // absolute path from project root
// Debug
dd($var1, $var2) // styled dump and die
// Passwords
Hash::make($password)
Hash::verify($plain, $hash)
// Input sanitization
Input::sanitize($value) // trim + stripslashes + htmlspecialchars# Development
php laracore serve
# Code generation
php laracore make:controller ControllerName
php laracore make:model ModelName
php laracore make:migration create_table_name
# Migrations
php laracore migrate
php laracore migration:rollback
# Seeders
php laracore db:seed
php laracore db:seeder SeederClassName
# API
php laracore generate:api-key- Full ORM with relationships (hasOne, hasMany, belongsTo)
- User management system
- Multi-authentication system
- Blade-style templating
- Route groups and prefixes
- Query caching
LaraCore is open-sourced software licensed under the MIT license.