PulseMVC Documentation
Complete reference for the PulseMVC framework — routing, models, views, CLI and more.
Introduction
PulseMVC is a lightweight, fast, and expressive PHP 8.5 MVC framework built from scratch — zero bloat, full control. It offers attribute-based routing, an Active Record ORM, a powerful CLI, Twig 3 templates, and a suite of modern JavaScript UI modules, all in a single dependency-light package.
Philosophy
- Convention over configuration — sensible defaults, minimal setup.
- Attribute-driven routing — PHP 8 attributes replace route files.
- No magic — every entry point is traceable and readable.
- Minimal dependencies — only Twig 3 and phpdotenv are required.
Installation
Via Composer
composer create-project pulsemvc/framework my-app
cd my-app
Manual Clone
git clone https://github.com/pulsemvc/framework.git my-app
cd my-app
composer install
Environment Setup
cp .env.example .env
php mvc key:generate
Edit .env with your database credentials:
APP_NAME=PulseMVC
APP_ENV=local
APP_DEBUG=true
APP_URL=http://localhost:8000
APP_KEY= # generated above
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=pulsemvc
DB_USERNAME=root
DB_PASSWORD=secret
Database Setup
php mvc migrate
php mvc db:seed
Start Development Server
php mvc serve
# Listening on http://127.0.0.1:8000
Web Server (Apache)
Point your document root to the public/ directory. The included .htaccess handles URL rewriting:
<VirtualHost *:80>
ServerName myapp.local
DocumentRoot /var/www/my-app/public
<Directory /var/www/my-app/public>
AllowOverride All
Require all granted
</Directory>
</VirtualHost>
Web Server (Nginx)
server {
listen 80;
server_name myapp.local;
root /var/www/my-app/public;
index index.php;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass unix:/run/php/php8.5-fpm.sock;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
}
}
Configuration
All configuration is stored in .env and accessed via the env()
or config() helpers. There are no PHP config files to manage.
| Key | Default | Description |
|---|---|---|
APP_NAME | PulseMVC | Application display name |
APP_ENV | production | local | staging | production |
APP_DEBUG | false | Show detailed error pages |
APP_URL | http://localhost | Base URL for asset/URL generation |
APP_KEY | — | 32-byte base64 secret (required) |
APP_VERSION | 1.0.0 | Application version string |
APP_TIMEZONE | UTC | PHP default timezone |
DB_HOST | 127.0.0.1 | MySQL host |
DB_PORT | 3306 | MySQL port |
DB_DATABASE | — | Database name |
DB_USERNAME | root | Database user |
DB_PASSWORD | — | Database password |
SESSION_LIFETIME | 7200 | Session TTL in seconds |
CACHE_TTL | 3600 | Default cache TTL in seconds |
LOG_LEVEL | debug | debug | info | warning | error |
Directory Structure
my-app/
├── app/
│ ├── Controllers/ # HTTP controllers
│ ├── Middleware/ # Custom middleware classes
│ ├── Models/ # Active Record models
│ ├── Services/ # Business logic services
│ └── Views/ # Twig templates
│ ├── layouts/ # Base layouts (base, app, guest)
│ ├── partials/ # Reusable partials
│ ├── auth/ # Login / register pages
│ ├── errors/ # 404, 403, 500, maintenance
│ └── home/ # Public pages
│
├── core/ # Framework internals (do not edit)
│ ├── CLI/ # CLI commands & console
│ ├── Exceptions/ # HTTP exception classes
│ ├── Twig/ # Twig extensions
│ └── *.php # Router, Model, Cache, etc.
│
├── database/
│ ├── migrations/ # Database migration files
│ └── seeds/ # Seeder classes
│
├── public/
│ ├── index.php # Application entry point
│ ├── css/app.css # Compiled CSS
│ └── js/ # JS modules
│
├── storage/
│ ├── cache/ # File cache store
│ ├── logs/ # Application logs
│ └── backups/ # Database backups
│
├── .env # Environment variables (not committed)
├── .env.example # Template for .env
├── composer.json
└── mvc # CLI entry point
Routing
Routes are defined using PHP 8 attributes directly on controller methods.
No route file is needed — the framework scans app/Controllers/ automatically.
Basic Route
#[Route('/hello', method: 'GET', name: 'hello')]
public function hello(Request $request): Response
{
return $this->view('hello');
}
Route Parameters
// Required segment — matches any value
#[Route('/users/{id}', method: 'GET')]
public function show(Request $request, int $id): Response {}
// Constrained with regex
#[Route('/posts/{slug:[a-z0-9-]+}', method: 'GET')]
public function post(Request $request, string $slug): Response {}
// Optional segment
#[Route('/archive/{year?}', method: 'GET')]
public function archive(Request $request, ?int $year = null): Response {}
HTTP Methods
#[Route('/users', method: 'POST', name: 'users.store')]
#[Route('/users/{id}', method: 'PUT', name: 'users.update')]
#[Route('/users/{id}', method: 'DELETE', name: 'users.destroy')]
// Multiple methods
#[Route('/api/data', method: ['GET', 'HEAD'])]
Route Groups
#[RouteGroup(prefix: '/admin', namePrefix: 'admin.', middleware: ['auth', 'admin'])]
class AdminController extends BaseController
{
#[Route('/dashboard', method: 'GET', name: 'dashboard')]
// resolves to: GET /admin/dashboard name: admin.dashboard
public function dashboard(Request $r): Response {}
}
Named Routes & URL Generation
// In PHP
route('users.show', ['id' => 5]); // → /users/5
{# In Twig #}
{{ route('users.show', {id: 5}) }}
Middleware on Routes
#[Route('/account', method: 'GET', middleware: ['auth'])]
#[Route('/admin', method: 'GET', middleware: ['auth', 'admin'])]
// AJAX-only route
#[Route('/api/search', method: 'GET', ajax: true)]
Controllers
Creating a Controller
php mvc make:controller PostController
Full Example
namespace App\Controllers;
#[RouteGroup(prefix: '/posts', namePrefix: 'posts.')]
class PostController extends BaseController
{
#[Route('', method: 'GET', name: 'index')]
public function index(Request $r): Response
{
$posts = Post::where('published', 1)->paginate(15);
return $this->view('posts/index', compact('posts'));
}
#[Route('', method: 'POST', name: 'store')]
public function store(Request $r): Response
{
$data = $this->validate($r->all(), [
'title' => 'required|string|max:200',
'body' => 'required|string',
]);
$post = Post::create($data);
return Response::redirect(route('posts.show', ['id' => $post->id]))
->with('success', 'Post created!');
}
}
BaseController Helpers
| Method | Description |
|---|---|
$this->view(template, data) | Render a Twig template |
$this->json(data, status) | Return a JSON response |
$this->validate(data, rules) | Validate input — redirects back on failure |
$this->redirect(url) | Return a redirect response |
$this->back() | Redirect to previous URL |
Middleware
Creating Middleware
php mvc make:middleware AuthMiddleware
Middleware Class
namespace App\Middleware;
class AuthMiddleware implements MiddlewareInterface
{
public function handle(Request $request, Closure $next): Response
{
if (!auth_check()) {
return Response::redirect(route('auth.login'))
->with('warning', 'Please sign in.');
}
return $next($request);
}
}
Registering Middleware Aliases
// In Application::boot() or a bootstrap file:
$app->middleware()->alias('auth', App\Middleware\AuthMiddleware::class);
$app->middleware()->alias('admin', App\Middleware\AdminMiddleware::class);
$app->middleware()->alias('guest', App\Middleware\GuestMiddleware::class);
Request
Accessing Input
$request->input('name'); // POST / GET value
$request->input('age', 18); // with default
$request->all(); // all input as array
$request->only(['name', 'email']); // whitelist keys
$request->except(['_token']); // exclude keys
$request->has('email'); // key present & non-empty
$request->query('page', 1); // query string only
$request->json(); // decoded JSON body
Request Information
$request->method(); // GET / POST / PUT / PATCH / DELETE
$request->path(); // /users/5
$request->url(); // http://example.com/users/5
$request->ip(); // client IP address
$request->isAjax(); // true for XMLHttpRequest
$request->isJson(); // Content-Type: application/json
$request->header('Accept'); // request header
File Uploads
$file = $request->file('avatar');
if ($file?->isValid()) {
$path = $file->store(storage_path('uploads'));
// $path = storage/uploads/uuid.ext
}
Response
// View response
return $this->view('home/index', ['user' => $user]);
// JSON response
return Response::json(['ok' => true], 201);
// Redirect
return Response::redirect(route('home.index'));
// Redirect back with flash message
return Response::back()
->with('success', 'Saved!')
->withInput();
// Custom status & headers
return Response::make('Not Found', 404)
->header('X-Custom', 'value');
Models
Creating a Model
php mvc make:model Post
Model Definition
namespace App\Models;
use Core\Model;
class Post extends Model
{
protected string $table = 'posts';
protected array $fillable = ['title', 'body', 'published'];
protected array $hidden = [];
protected array $casts = ['published' => 'bool', 'views' => 'int'];
protected bool $timestamps = true;
protected bool $softDeletes = true;
}
CRUD Operations
// Create
$post = Post::create(['title' => 'Hello', 'body' => '...']);
// Find
$post = Post::find(1); // by primary key
$post = Post::findOrFail(1); // throws 404
$post = Post::where('slug', $slug)->first();
// Update
$post->update(['title' => 'Updated']);
// Delete (soft if $softDeletes = true)
$post->delete();
$post->forceDelete(); // permanent
$post->restore(); // un-delete
// Bulk
Post::all();
Post::count();
Post::where('published', 1)->get();
Available Casts
| Cast | PHP Type |
|---|---|
int / integer | int |
float | float |
bool / boolean | bool |
string | string |
array | array (JSON decode) |
json | array/object (JSON decode) |
datetime | DateTimeImmutable |
Query Builder
Select Queries
$users = Post::where('published', 1)
->where('user_id', $userId)
->orderBy('created_at', 'desc')
->limit(10)
->get();
// Or-where
Post::where('status', 'draft')->orWhere('status', 'review')->get();
// Where in
Post::whereIn('id', [1, 2, 3])->get();
// Where null
Post::whereNull('deleted_at')->get();
// Pagination
$posts = Post::paginate(15); // Paginator instance
Raw DB Access
$db = app('db');
$rows = $db->select('SELECT * FROM posts WHERE id = ?', [1]);
$db->statement('TRUNCATE TABLE sessions');
// Query builder via table
$db->table('users')->where('active', 1)->get();
Aggregates
Post::count();
Post::where('published', 1)->count();
Post::max('views');
Post::sum('views');
Post::avg('rating');
Migrations
Create & Run
php mvc make:migration create_posts_table
php mvc migrate
php mvc migrate:rollback
php mvc migrate:fresh # drop all + re-run
php mvc migrate:status
Migration Class
use Core\Migration;
use Core\SchemaBuilder;
class CreatePostsTable extends Migration
{
public function up(SchemaBuilder $schema): void
{
$schema->create('posts', function (Blueprint $t) {
$t->id();
$t->unsignedBigInteger('user_id');
$t->string('title');
$t->text('body');
$t->boolean('published')->default(false);
$t->timestamps();
$t->softDeletes();
});
}
public function down(SchemaBuilder $schema): void
{
$schema->dropIfExists('posts');
}
}
Column Types
| Method | SQL Type |
|---|---|
id() | BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY |
string(col, len) | VARCHAR(255) |
text(col) | TEXT |
longText(col) | LONGTEXT |
integer(col) | INT |
unsignedBigInteger(col) | BIGINT UNSIGNED |
boolean(col) | TINYINT(1) |
decimal(col, p, s) | DECIMAL(8,2) |
json(col) | JSON |
timestamp(col) | TIMESTAMP |
timestamps() | created_at + updated_at |
softDeletes() | deleted_at TIMESTAMP NULL |
Seeders
php mvc make:seeder PostSeeder
php mvc db:seed
php mvc db:seed PostSeeder # run one seeder
class PostSeeder extends Seeder
{
public function run(): void
{
for ($i = 1; $i <= 20; $i++) {
Post::create([
'title' => "Post #{$i}",
'body' => 'Lorem ipsum ...',
'published' => true,
]);
}
}
}
Views / Twig
PulseMVC uses Twig 3 for all templates. Templates live in app/Views/.
Rendering
// In a controller:
return $this->view('posts/show', ['post' => $post]);
// Or via helper:
return view('posts/show', compact('post'));
Extending a Layout
{% extends 'layouts/app.html.twig' %}
{% block title %}My Page{% endblock %}
{% block content %}
<h1>Hello, {{ user.name }}!</h1>
{% endblock %}
Available Twig Functions
| Function | Description |
|---|---|
asset(path) | URL to a public asset |
route(name, params) | Generate named route URL |
csrf_field() | Hidden CSRF input field |
csrf_meta() | CSRF meta tag for AJAX |
flash(key) | Get a single flash value |
flash_messages() | All flash alert messages array |
toast_messages() | All toast notification data |
old(field) | Repopulate form field |
errors(field) | Validation errors for field |
has_errors(field) | True if field has errors |
auth() | Current user or null |
auth_check() | True if user logged in |
config(key) | Read env/config value |
now() | Current datetime string |
Global Twig Variables
{{ app_name }} {# APP_NAME from .env #}
{{ app_version }} {# APP_VERSION #}
{{ app_env }} {# APP_ENV #}
{{ app_debug }} {# APP_DEBUG #}
{{ current_user }} {# auth() result #}
Validation
In Controllers
$data = $this->validate($request->all(), [
'name' => 'required|string|min:2|max:100',
'email' => 'required|email|unique:users,email',
'password' => 'required|min:8|confirmed',
'role' => 'required|in:admin,editor,viewer',
'age' => 'nullable|integer|between:18,120',
'avatar' => 'nullable|file|mimes:jpg,png|max:2048',
]);
Manual Validator
$v = new Validator($data, $rules, messages: [
'email.unique' => 'This email is already in use.',
]);
if ($v->fails()) {
return Response::back()
->withErrors($v)
->withInput();
}
$clean = $v->validated(); // only validated fields
All Validation Rules
| Rule | Description |
|---|---|
required | Field must be present and non-empty |
nullable | Field may be empty / null |
string | Must be a string |
integer | Must be an integer |
numeric | Integer or float |
boolean | 1/0/true/false |
email | Valid email format |
url | Valid URL |
ip | Valid IPv4 or IPv6 |
min:n | Min length (string) or value (number) |
max:n | Max length or value |
between:min,max | Length or value in range |
in:a,b,c | Value must be in list |
not_in:a,b | Value must not be in list |
regex:/pattern/ | Custom regex match |
unique:table,col | Value not in database |
exists:table,col | Value must exist in database |
confirmed | Field must match field_confirmation |
date | Parseable date string |
before:date | Date before given date |
after:date | Date after given date |
file | Uploaded file |
mimes:jpg,png | Allowed mime extensions |
Displaying Errors in Twig
<div class="form-group {% if has_errors('email') %}has-error{% endif %}">
<input type="email" name="email" value="{{ old('email') }}">
{% if has_errors('email') %}
{% for error in errors('email') %}
<span class="field-error">{{ error }}</span>
{% endfor %}
{% endif %}
</div>
Custom Rules
Validator::extend(
'phone',
fn($field, $value) => preg_match('/^\+?[0-9]{10,15}$/', $value),
'The :field must be a valid phone number.'
);
// Use as: 'phone' => 'required|phone'
Session & Flash
Session Usage
$sess = session();
$sess->set('key', 'value');
$sess->get('key');
$sess->has('key');
$sess->forget('key');
$sess->flush(); // clear everything
$sess->regenerate(); // new session ID
// Shorthand helper
session('user_id', 42); // write
session('user_id'); // read
Flash Messages
// Controller: set flash
return Response::back()
->with('success', 'Profile saved!')
->with('warning', 'Email not verified.');
// Template: display flash
{% include 'partials/alerts.html.twig' %}
{# Or manually: #}
{% for msg in flash_messages() %}
<div class="alert alert-{{ msg.type }}">{{ msg.message }}</div>
{% endfor %}
Toast Notifications
// From PHP controller
session()->toast('Profile updated!', 'success', 4000);
// From JavaScript
Toast.show('success', 'Saved!', '', 4000);
JavaScript Modules
All modules are auto-loaded via layouts/base.html.twig. No bundler required.
Toast — toast.js
Toast.show('success', 'Done!', 'optional detail', 4000);
Toast.show('error', 'Something went wrong');
Toast.show('warning', 'Low disk space');
Toast.show('info', 'New version available');
Modal — modal.js
<!-- HTML -->
<button data-modal-open="confirm-dialog">Open Modal</button>
<div id="confirm-dialog" class="modal" aria-hidden="true">
<div class="modal-overlay" data-modal-close></div>
<div class="modal-box modal-md">
<div class="modal-header">
<h2 class="modal-title">Confirm</h2>
<button class="modal-close" data-modal-close>×</button>
</div>
<div class="modal-body">Are you sure?</div>
</div>
</div>
// JavaScript
Modal.open('confirm-dialog');
Modal.close('confirm-dialog');
// Programmatic creation
Modal.create({ title: 'Alert', body: '<p>Content</p>', size: 'sm' });
Confirm Dialog — confirm.js
<!-- Auto-intercept via attribute -->
<button data-confirm="Are you sure you want to delete this?">Delete</button>
// Programmatic (returns Promise<boolean>)
const ok = await Confirm.show({
title: 'Delete post?',
message: 'This action cannot be undone.',
confirmText: 'Yes, delete',
type: 'danger', // danger | warning | info | success
});
if (ok) deletePost();
Tooltip — tooltip.js
<button data-tooltip="Save changes" data-tooltip-placement="top">
Save
</button>
<!-- placements: top | bottom | left | right (auto-flips at viewport edge) -->
Dropdown — dropdown.js
<div class="dropdown">
<button class="dropdown-trigger">Actions ▾</button>
<ul class="dropdown-menu">
<li><a href="/edit">Edit</a></li>
<li><a href="/delete" data-confirm="Delete?">Delete</a></li>
</ul>
</div>
Tabs — tabs.js
<div data-tabs data-tabs-hash>
<div role="tablist">
<button role="tab" data-target="tab-1" aria-selected="true">General</button>
<button role="tab" data-target="tab-2">Security</button>
</div>
<div id="tab-1" role="tabpanel">General content</div>
<div id="tab-2" role="tabpanel" hidden>Security content</div>
</div>
Infinite Scroll — infinite.js
<!-- Auto mode: append when sentinel enters viewport -->
<div data-infinite data-infinite-url="/api/posts">
<!-- items rendered here -->
</div>
<div data-infinite-sentinel></div>
<!-- Server must return: { "html": "<...>", "nextPage": 2 } -->
Charts — chart.js
<!-- HTML attribute init -->
<canvas data-chart='{"type":"bar","labels":["Jan","Feb","Mar"],
"datasets":[{"label":"Revenue","data":[120,95,180]}]}'></canvas>
// JavaScript API
const chart = Chart.create('myCanvas', {
type: 'line',
labels: ['Jan', 'Feb', 'Mar'],
datasets: [{ label: 'Users', data: [42, 67, 88] }],
});
chart.update({ data: [50, 75, 92] });
chart.destroy();
Cache
File-based cache with TTL, tagging, and a static facade. Cache files live in storage/cache/.
Basic Usage
// Store for 60 minutes
Cache::put('users.count', $count, 3600);
// Retrieve (returns null if missing/expired)
$count = Cache::get('users.count');
// Remember (fetch-or-store)
$users = Cache::remember('users.all', 3600, fn() => User::all());
// Store forever
Cache::forever('config.settings', $settings);
// Delete
Cache::forget('users.count');
Cache::flush(); // clear entire cache
Tagged Cache
// Write with tags
Cache::tags(['posts'])->put('posts.all', $posts, 3600);
Cache::tags(['posts', 'user-1'])->put('posts.user.1', $data, 1800);
// Flush all posts cache at once
Cache::tags(['posts'])->flush();
CLI
php mvc cache:clear # all
php mvc cache:clear --type=twig
php mvc cache:clear --type=route
Authentication
Login
$userService = new UserService(app('db'), session());
$user = $userService->attempt($email, $password, $remember);
if ($user) {
return Response::redirect(route('dashboard'));
}
return Response::back()->with('error', 'Invalid credentials.');
Check Auth & Get User
auth_check(); // bool
auth(); // User model or null
auth('id'); // user ID
auth('is_admin'); // attribute shortcut
In Twig Templates
{% if auth_check() %}
Welcome, {{ auth().name }}!
{% if auth().is_admin %}<span class="badge">Admin</span>{% endif %}
{% else %}
<a href="{{ route('auth.login') }}">Sign in</a>
{% endif %}
Protecting Routes
// Route-level middleware
#[Route('/dashboard', method: 'GET', middleware: ['auth'])]
// Group-level middleware
#[RouteGroup(prefix: '/admin', middleware: ['auth', 'admin'])]
Logging
Log files are written to storage/logs/ by channel (one file per day).
logger('User logged in'); // debug
logger('Payment failed', 'error'); // error
logger('Import complete', 'info', ['rows' => 1240]); // with context
$log = logger(); // Logger instance
$log->info('...');
$log->warning('...');
$log->error('...');
php mvc log:clear # delete all .log files
Global Helpers
| Helper | Description |
|---|---|
app(?string) | Application / container resolution |
env(key, default) | Read .env value |
config(key, default) | Alias for env() |
base_path(path) | Absolute path from project root |
app_path(path) | Absolute path from app/ |
storage_path(path) | Absolute path from storage/ |
public_path(path) | Absolute path from public/ |
database_path(path) | Absolute path from database/ |
asset(path) | Public asset URL |
url(path) | Absolute URL for path |
route(name, params) | Named route URL |
redirect(url) | Redirect Response |
back() | Redirect to Referer |
view(template, data) | Render Twig template |
request(?key) | Request instance or input value |
session(?key, ?value) | Session read/write |
logger(?msg, level) | Logger / log a message |
auth(?key) | Current user or attribute |
auth_check() | Is user logged in? |
csrf_token() | Current CSRF token string |
old(field, default) | Previous form input |
errors(?field) | Validation error messages |
has_errors(?field) | True if field has errors |
abort(status, msg) | Throw an HTTP exception |
now(format) | Current datetime string |
dd(...vars) | Dump and die (debug) |
dump(...vars) | Dump without dying |
CLI Overview
All commands run via the mvc entry point in the project root.
php mvc # list all commands
php mvc help serve # command help
Available Commands
| Command | Description |
|---|---|
serve [--host] [--port] | Start PHP built-in server |
key:generate | Generate APP_KEY in .env |
route:list | List all registered routes |
route:clear | Delete route cache |
cache:clear [--type] | Clear file cache |
log:clear | Delete all log files |
migrate | Run pending migrations |
migrate:rollback | Reverse last migration batch |
migrate:reset | Roll back all migrations |
migrate:fresh | Drop all tables & re-migrate |
migrate:status | Show migration status |
db:seed [class] | Run seeders |
db:backup [--tables] [--keep] | Backup database to file |
backup:list [--manifest] | List database backups |
backup:restore [file] [--latest] | Restore a backup |
make:controller Name | Scaffold a controller |
make:model Name | Scaffold a model |
make:migration name | Create a migration file |
make:middleware Name | Scaffold middleware |
make:seeder Name | Scaffold a seeder |
make:service Name | Scaffold a service class |
make:command Name | Scaffold a custom command |
Make Commands
Scaffolding generators create ready-to-edit stub files in the correct directories.
php mvc make:controller UserController
# → app/Controllers/UserController.php
php mvc make:model Post
# → app/Models/Post.php
php mvc make:migration add_bio_to_users
# → database/migrations/2026_03_31_120000_add_bio_to_users.php
php mvc make:middleware RateLimitMiddleware
# → app/Middleware/RateLimitMiddleware.php
php mvc make:seeder PostSeeder
# → database/seeds/PostSeeder.php
php mvc make:service PaymentService
# → app/Services/PaymentService.php
php mvc make:command SendReportCommand
# → app/Console/SendReportCommand.php
Database Commands
Backup
# Full backup (gzipped)
php mvc db:backup
# Specific tables
php mvc db:backup --tables=users,posts
# Keep last N backups
php mvc db:backup --keep=7
# Custom filename, no gzip
php mvc db:backup --filename=pre-deploy --no-gzip
# Dry run (shows command without executing)
php mvc db:backup --dry-run
List & Restore
php mvc backup:list
php mvc backup:list --manifest
php mvc backup:restore # interactive prompt
php mvc backup:restore --latest # newest backup
php mvc backup:restore backup.sql.gz # specific file
php mvc backup:restore --latest --force # skip confirmation
Custom Commands
php mvc make:command SendReportCommand
namespace App\Console;
use Core\CLI\BaseCommand;
class SendReportCommand extends BaseCommand
{
protected string $name = 'report:send';
protected string $description = 'Send the weekly report email';
public function handle(): int
{
$dry = $this->option('dry-run', false);
$this->info('Generating report...');
if (!$dry) {
// send email logic
$this->success('Report sent!');
} else {
$this->warning('Dry run — no email sent.');
}
return 0;
}
}
BaseCommand API
| Method | Description |
|---|---|
$this->argument(name) | Positional argument value |
$this->option(name, default) | Flag/option value |
$this->info(msg) | Print cyan info line |
$this->success(msg) | Print green success line |
$this->warning(msg) | Print yellow warning line |
$this->error(msg) | Print red error line |
$this->line(msg) | Print plain line |
$this->ask(prompt) | Prompt for text input |
$this->confirm(prompt) | Prompt yes/no → bool |
$this->choice(prompt, options) | Numbered list choice |
$this->table(headers, rows) | Render ASCII table |
Register the Command
// In core/CLI/Console.php — add to $commands array:
App\Console\SendReportCommand::class,
0 8 * * 1 /usr/bin/php /var/www/myapp/mvc report:send