A fast, lightweight PHP MVC framework designed for developers who prefer clean architecture, file-based routing, and a Vue.js-friendly front-end workflow.
- Smart Routing — File-based or explicit route definitions with full HTTP verb support.
- Query Builder — Active record database layer with full isolation between models, transactions, seeding, and JSON backups.
- Compiled View Engine — A blade-like template compiler that writes to disk cache (no
eval()), with Vue-style component slots and layout composition. - Migration System — Generate, run, and rollback schema changes via CLI.
- Yamato CLI — Code generation and database management from the command line.
- Encryption & Hashing — Built-in tools for encoding strings.
- PHP 7.0 or higher
- A web server (Apache or Nginx)
- Composer
git clone https://github.com/strifejeyz/framework.git
cd framework
composer installCopy and configure your environment:
# Edit app/config/application.php — set APP_NAME, APP_KEY, BASE_URL, etc.
# Edit app/config/database.php — set host, database, username, password/
├── app/
│ ├── config/ # Application and database configuration
│ ├── controllers/ # Controller classes
│ ├── migrations/ # Database schema migration files
│ ├── models/ # Model classes extending QueryBuilder
│ ├── seeders/ # Database seeders
│ ├── views/ # Template files (.php)
│ └── routes.php # Explicit route definitions
├── assets/
│ └── css/ # Public stylesheets
├── kernel/ # Framework core — do not modify
│ ├── database/ # QueryBuilder and connection drivers
│ ├── security/ # Encryption utilities
│ └── View.php # Template engine (compiler + slots)
├── storage/
│ ├── cache/ # Compiled view cache
│ ├── backups/ # JSON database backups
│ └── logs/ # Application logs
├── index.php # Application entry point
└── yamato # CLI entry point
Key constants in app/config/application.php:
| Constant | Default | Purpose |
|---|---|---|
APP_NAME |
'Strife App' |
Application name used in views |
DEV_MODE |
TRUE |
Enables dev-only tools — set FALSE in production |
FILE_BASED_ROUTING |
true |
Toggle between file-based and explicit routing |
CACHED_VIEWS |
FALSE |
Skip view recompilation on each request |
MAINTENANCE_MODE |
FALSE |
Serves 503 to all requests when TRUE |
Enable in app/config/application.php:
const FILE_BASED_ROUTING = true;With file-based routing, URLs are automatically mapped to controllers and methods:
| URL | Resolves to |
|---|---|
/home/index |
HomeController::index() |
/book/show |
BookController::show() |
/user/profile |
UserController::profile() |
No manual route registration required. Any HTTP method is accepted.
Set FILE_BASED_ROUTING = false and define routes in app/routes.php:
get('/users', 'UsersController@index');
post('/users/store', 'UsersController@store');
put('/users/update', 'UsersController@update');
patch('/users/modify', 'UsersController@modify');
delete('/users/remove','UsersController@destroy');Tip: To use
PUT,PATCH, orDELETEfrom an HTML form, add a hidden_methodinput field:<form method="POST"> <input type="hidden" name="_method" value="DELETE"> </form>
Named routes:
get('users-list -> /users', 'UsersController@index');Specific (literal) routes always beat wildcard routes, regardless of the order they are defined in routes.php. The router automatically sorts routes by specificity before matching — so this is always safe:
get('/:any', 'HomeController@index'); // registered first — does NOT hijack /about-us
get('/about-us', 'HomeController@about');/about-us correctly resolves to HomeController@about every time.
When pairing Strife with a frontend SPA, define a /:any catch-all to serve your app shell for any unmatched single-segment URL. Vue Router or React Router then handles client-side navigation:
// API routes first
get('/api/books', 'Api\BookController@index');
post('/api/books', 'Api\BookController@store');
// SPA shell — catch everything else and let the frontend router decide
get('/:any', 'HomeController@index');Note:
/:anyonly matches URLs with exactly one path segment. Multi-segment API routes like/api/booksor/api/books/42are never affected.
When DEV_MODE = TRUE, a route inspector is available at /_routes. It lists every registered endpoint with its HTTP method, URL pattern, handler, and route name. Filter by method or search by URL in real time.
Set DEV_MODE = FALSE before deploying to production — the /_routes endpoint is not registered at all when disabled.
Create controllers in app/controllers/. Class names must be PascalCase and suffixed with Controller.
<?php
class BookController
{
public function index()
{
$books = Book::get();
return render('book', compact('books'));
}
public function show()
{
$book = Book::find($_GET['id']);
return render('book/show', compact('book'));
}
}Generate a controller with Yamato:
php yamato create:controller BookModels live in app/models/ and extend Kernel\Database\QueryBuilder. Always specify the target table.
<?php
use Kernel\Database\QueryBuilder as Model;
class Book extends Model
{
protected static $table = "books";
}Generate a model with Yamato:
php yamato create:model Book books// Get all rows
$books = Book::get();
// Get the first row
$book = Book::first();
// Find by primary key
$book = Book::find(1);
// Find or throw an error
$book = Book::findOrFail(1);// Single condition
Book::where('author', '=', 'John')->get();
// OR condition
Book::where('author', '=', 'John')->orWhere('author', '=', 'Jane')->get();
// IN list
Book::whereIn('status', ['active', 'pending'])->get();
// Between two values
Book::whereBetween('created', $start, $end)->get();
// Check if any row matches
Book::where('title', '=', 'Dune')->exists(); // true/falseBook::select(['title', 'author'])
->where('author', '=', 'Frank Herbert')
->order('title', 'ASC')
->limit(10, 0)
->get();Book::count();
Book::min('price');
Book::max('price');
Book::sum('price');
Book::avg('price');
Book::distinct('author')->get();Book::insert([
'title' => 'Dune',
'author' => 'Frank Herbert',
]);
// Insert, excluding certain fields
Book::insertExcept($data, ['csrf_token', 'submit']);// Update by primary key
Book::update(['title' => 'New Title'], $id);
// Increment / Decrement a column value
Book::where('id', '=', 1)->increment('views', 1);
Book::where('id', '=', 1)->decrement('stock', 5);// Delete by condition
Book::where('id', '=', 1)->delete();
// Delete all rows
Book::delete();// Single value
$title = Book::pull('title');
// Multiple columns as array
$info = Book::pull('title', 'author');Book::join('authors')
->on('books.author_id = authors.id')
->get();Book::transact();
try {
Book::insert(['title' => 'Draft']);
Book::commit();
} catch (Exception $e) {
Book::rollback();
}// Exports table data to storage/backups/ as JSON
Book::backup();
// Restores from the most recent backup
Book::restore();Important: Each model class maintains its own isolated query state. Using
User::find(1)followed byBook::find(2)will not cause any result contamination between the two.
Templates live in app/views/. The framework uses a compiled view engine — all custom syntax is converted to standard PHP and written to storage/cache/ as real files. No eval(). No OPCache penalty.
// In a controller
return render('book', compact('books'));
// Equivalent full call
return View::render('book', compact('books'));Define your page shell in a single layout file (app/views/layouts/layout.php):
<!DOCTYPE html>
<html>
<head>
<title>@show('title', 'Default Title')</title>
</head>
<body>
<header>
@show('header')
</header>
<main>
@show
</main>
<footer>
@show('footer', '<p>© 2026</p>')
</footer>
</body>
</html>In your view file, wrap content with @layout / @endlayout and fill slots with @slot / @endslot:
@layout('layouts/layout')
@slot('title')
Books — My App
@endslot
@slot('header')
<h1>Book Library</h1>
@endslot
{{-- Default slot: any content outside @slot tags goes into @show --}}
<div class="content">
<p>Hello from the main content area!</p>
</div>
@slot('footer')
<p>Custom footer text</p>
@endslot
@endlayoutDirective reference:
| Directive | Purpose |
|---|---|
@layout('path/to/layout') |
Set the parent layout for this view |
@endlayout |
Close the layout and trigger rendering |
@slot('name') |
Start filling a named slot |
@endslot |
End the named slot (resume default slot) |
@show('name') |
Output a named slot in the layout |
@show('name', 'fallback') |
Output slot or a fallback string if empty |
@show |
Output the default (unnamed) slot |
{{-- HTML-escaped output (safe) --}}
{{ $variable }}
{{-- Raw, unescaped output --}}
{! $variable !}{if ($user->isAdmin())}
<p>Welcome, admin.</p>
{elseif ($user->isEditor())}
<p>Welcome, editor.</p>
{else}
<p>Welcome, guest.</p>
{endif}{foreach ($books as $book)}
<li>{{ $book->title }}</li>
{endforeach}
{for ($i = 0; $i < 5; $i++)}
<p>Item {{ $i }}</p>
{endfor}
{while ($condition)}
<p>Looping...</p>
{endwhile}{{-- Include and compile another template --}}
@render('partials/navbar')
{{-- Include a raw PHP file (no compilation) --}}
@get('partials/sidebar')Since @ is the directive prefix, use @@ to output a literal @ in HTML:
<p>Contact us at info@@gmail.com</p>Renders as:
<p>Contact us at info@gmail.com</p>The older @extend / @stop layout system is still supported for backward compatibility:
@extend('layouts/frontend')
<p>Page content here</p>
@stop()Generate a migration:
php yamato create:migration create_books_table booksEdit the generated file in app/migrations/, then run:
php yamato db:migrate # Apply all pending migrations
php yamato db:rollback # Undo all migrationsphp yamato| Command | Description |
|---|---|
php yamato create:controller Name |
Create a new controller |
php yamato create:model Name table |
Create a new model |
php yamato create:migration Name table |
Create a migration file |
php yamato create:key |
Generate a random application key |
| Command | Description |
|---|---|
php yamato db:migrate |
Run all migrations |
php yamato db:rollback |
Rollback all migrations |
php yamato db:backup |
Backup table data to JSON |
php yamato db:restore |
Restore from last backup |
php yamato db:seed |
Run database seeders |
| Command | Description |
|---|---|
php yamato clear:logs |
Delete log files |
php yamato clear:cache |
Delete compiled view cache |
php yamato clear:all |
Delete logs, cache, and backups |
php yamato hash:encode string |
Hash a string |
php yamato encryption:encode string |
Encrypt a string |
php yamato encryption:decode string |
Decrypt a string |
- Fork the repository.
- Create a new branch for your feature or bugfix.
- Submit a pull request with a clear description of your changes.
Strife is open-source software licensed under the MIT License.
For questions or issues, open a ticket on GitHub Issues.