Lightweight PHP application stack: HTTP routing, Twig views, PDO database layer, mail, cache, config, console, events, validation, and i18n.
composer require vortexphp/frameworkRequires PHP 8.2+, ext-mbstring, ext-pdo, and Twig 3. For SMTP with TLS/SSL, install ext-openssl (see composer.json suggest).
The framework expects a base path (your app root) with at least:
config/— configuration read byVortex\Config\Repositoryconfig/paths.php(optional) — return['migrations' => '…', 'models' => '…', 'controllers' => '…']relative to the project root; defaults aredb/migrations,app/Models, andapp/Http/Controllersapp/Routes/*.php— HTTP route files (required in order; register viaVortex\Routing\Route; optional->name('key')+route('key', $params))app/Routes/*Console.php— console registration files (Vortex::command(...)at top level, analogous toRoute::getfor HTTP)resources/views/— Twig templates (used byVortex\Application::boot(); override withconfig/view.phpkeypath)storage/cache/twig/— optional Twig cache whenapp.debugis false
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use Vortex\Application;
$app = Application::boot(__DIR__); // loads `.env`, registers core services; optional 2nd arg: ?callable $configure(Container, $basePath)
$app->run(); // or use Http\Kernel with global middleware from config- Define HTTP routes in
app/Routes/*.php. - Name routes with
->name('...')and generate URLs withroute('name', $params).
<?php
use Vortex\Http\Response;
use Vortex\Routing\Route;
Route::get('/', static fn (): Response => Response::html('Home'))->name('home');
Route::get('/posts/{id}', static function (string $id): Response {
return Response::json(['id' => $id]);
})->name('posts.show');
Route::post('/posts', static fn (): Response => Response::redirect(route('home')));- Use
Vortex\Auth\Authfor session login/logout ($rememberon login sets a signed cookie; requiresAPP_KEY). Vortex\Auth\Gatefor abilities and model policies;Vortex\Auth\AuthorizationExceptionwhen usingGate::authorize()(handled as 403).- Middleware:
Authenticate,RememberFromCookie, and subclassAuthorizeAbilityfor route protection; list class names underapp.middleware. Vortex\Auth\PasswordResetBrokerfor opaque reset tokens stored in SQL (you send mail and define routes).- In Twig:
auth_check(),auth_id(),auth_user(),gate_allows(...).
See src/Auth/README.md for config keys, middleware order, and a minimal password_reset_tokens schema.
- Extend
Vortex\Database\Modeland declare$fillablefor mass assignment. - Override
protected static ?string $tablewhen default pluralized snake_case naming is not desired. - Use
query()for filters, joins, grouping, eager loading, pagination, and bulk updates/deletes.
<?php
use Vortex\Database\Model;
final class Post extends Model
{
protected static ?string $table = 'posts';
protected static array $fillable = ['user_id', 'title', 'body'];
public function user(): ?User
{
/** @var User|null */
return $this->belongsTo(User::class, 'user_id');
}
/** @return list<Comment> */
public function comments(): array
{
/** @var list<Comment> */
return $this->hasMany(Comment::class, 'post_id');
}
}
$posts = Post::query()
->where('status', 'published')
->with(['user', 'comments'])
->orderBy('id', 'DESC')
->limit(10)
->get();- Migration classes live in
db/migrationsby default (or customconfig/paths.phpmigrationspath). - Each migration file must return a class extending
Vortex\Database\Schema\Migration. - Migration ID is the filename (without
.php).
<?php
use Vortex\Database\Schema\Migration;
use Vortex\Database\Schema\Schema;
return new class extends Migration {
public function up(): void
{
Schema::create('posts', static function ($table): void {
$table->id();
$table->string('title');
});
}
public function down(): void
{
Schema::dropIfExists('posts');
}
};Validator::make()accepts pipe strings and fluentVortex\Validation\Ruleobjects.
$result = Validator::make($data, [
'email' => Rule::required()->email()->max(255),
'password' => Rule::required()->min(8)->confirmed(),
]);Request::wantsJson()is true forAccept: application/jsonandX-Requested-With: XMLHttpRequest.Responseincludes helpers for common error responses and flash data:Response::notFound(),forbidden(),unauthorized(),error()->with(),->withMany(),->withErrors(),->withInput()
- Add
config/schedule.phpwithtasks(cron+ handlerclass; optionalwithout_overlapping,mutex_ttl, and top-levelmutex_storefor cache-backed overlap guards) and runphp vortex schedule:runfrom cron each minute (seesrc/Schedule/README.md). Optionalapp.timezonefor due-time evaluation.
- Implement
Vortex\Queue\Contracts\Job, push withVortex\Queue\Queue::push()after boot. Default driver uses a SQLjobstable; setqueue.drivertoredisandqueue.redisfor a Redis-backedQueueDriver(seesrc/Queue/README.md). - Optional
failed_jobstable andqueue:failed/queue:retryfor dead-letter handling (seesrc/Queue/README.md). - Run
php vortex queue:work(addonceto process one job and exit).
Use the Vortex\ namespace for framework types. See the test suite under tests/ for concrete usage patterns.
Testing HTTP in-process: Kernel::handle(Request::make('GET', '/path')) returns a Response without sending output; register ErrorRenderer on the container when using the full error stack (see tests/KernelHandleTest.php).
- Your application (anything you deploy) should commit
composer.lock. Runcomposer installin CI and production so installs match what you tested. - This package (
vortexphp/framework) is a library: it normally does not commit a rootcomposer.lock. Downstream apps resolve compatible versions fromcomposer.jsonwhen theycomposer require vortexphp/framework, and their own lock pins the tree. - Skeleton or template apps you ship (starter repos) should include a committed
composer.locksocomposer installreproduces the same dependency set for every new checkout, unless you intentionally want floating minors on each clone.
See CHANGELOG.md.
MIT.