Use this skill for MindVisionCode PHP framework architecture, routing, dispatching, controllers, actions, ViewModels, templates, and HTTP request/response flow.
This project is a small PHP MVC framework called MindVisionCode PHP.
It is intentionally inspired by a Classic ASP MVC framework style:
Do not turn this into Laravel, Symfony, Slim, CakePHP, or another large framework.
Browser
→ public/index.php
→ Request
→ Dispatcher
→ Router
→ Route
→ Controller
→ ViewModel/Repository/Service
→ View
→ Response
Preferred structure:
project-root/
public/
index.php
src/
Controller/
Service/
Repository/
Entity/
ValueObject/
ViewModel/
Http/
Routing/
Validation/
Database/
Migration/
templates/
config/
tests/
var/
cache/
logs/
vendor/
composer.json
Rules:
public/ is the web root.src/, config/, tests/, vendor/, .ai/, or .env files through the web server.src/.var/ or another ignored runtime directory.Install dependencies:
composer install
Regenerate autoload files:
composer dump-autoload
Run local server:
php -S localhost:8000 -t public
Run basic tests:
php tests/run.php
Core\App::resolveArgs() injects constructor and action parameters by type. Any service registered via $app->bind(SomeClass::class, $instance) in public/index.php is automatically injected when an action declares a typed parameter of that class.
Core\Request is registered as a binding in public/index.php before dispatch, so controller actions can declare it as a parameter without calling Request::capture() manually:
public function index(Request $request): Response
{
$search = $request->input('search', '');
// ...
}
Route segment parameters (e.g. {id}) are still resolved by name before the binding lookup.
To make a service injectable, register it once at bootstrap:
// public/index.php
$app->bind(Database::class, database());
Then declare it as a typed parameter in any action:
public function index(Database $db): Response
{
// $db is injected automatically
}
Binding concrete instances: Use $app->instance($name, $obj) to bind a specific object by key. Instances take precedence over bindings when resolving.
Auto-wiring: Use $app->make(SomeClass::class) to resolve a class with its constructor dependencies injected automatically:
$repo = $app->make(EmployeeRepository::class);
// $app checks bindings first, then instantiates the class and resolves its constructor
Test isolation: Call $app->clear() to reset all bindings and instances between test runs.
Do not call Request::capture() inside action bodies. Declare the parameter instead.
requirePost($request) to guard POST-only actions. It returns ?Response (null when the method is POST, a 405 Response otherwise). Always return it immediately if non-null:if ($guard = $this->requirePost($request)) {
return $guard;
}
new Repository(database()) on every method call:private ?EmployeeRepository $employees = null;
private function employees(): EmployeeRepository
{
if ($this->employees === null) {
$this->employees = new EmployeeRepository(database());
}
return $this->employees;
}
View paths are set in config/view.php:
return [
'views_path' => __DIR__ . '/../app/Views',
'layout_path' => __DIR__ . '/../app/Views/layouts/app.php',
];
core/View.php reads this file lazily on first use and caches the result. To change where views or the layout live, edit config/view.php — do not edit core/View.php. This follows the same pattern as config/database.php.
Keep presentation separate from business logic.
Rules:
Plain PHP template example:
<h1><?= e($pageTitle) ?></h1>
<ul>
<?php foreach ($users as $user): ?>
<li><?= e($user->name()) ?></li>
<?php endforeach; ?>
</ul>
Core\Router exposes one method per HTTP verb:
| Method | HTTP verb |
|---|---|
$router->get($path, $handler) |
GET |
$router->post($path, $handler) |
POST |
$router->put($path, $handler) |
PUT |
$router->patch($path, $handler) |
PATCH |
$router->delete($path, $handler) |
DELETE |
$router->add($method, $path, $handler) |
Any verb |
HTML forms only support GET and POST. To route a form submission to a PUT, PATCH, or DELETE handler, add a hidden _method field:
<form method="POST" action="/employees/42">
<?= csrf_field() ?>
<input type="hidden" name="_method" value="PUT">
<!-- fields -->
</form>
Core\Request::method() checks for this field (and the X-HTTP-Method-Override header from JavaScript clients) when the base method is POST, and returns the overridden verb. Only PUT, PATCH, and DELETE are accepted as override values — all others are ignored.
Rules:
X-Forwarded-For unless configured behind a trusted proxy.Example POST guard using the framework helper:
public function store(Request $request): Response
{
if ($guard = $this->requirePost($request)) {
return $guard;
}
// POST-only logic here
}
When adding features, preserve the small-framework character:
Powered by TurnKey Linux.