# MVC Framework Skill ## Purpose Use this skill for MindVisionCode PHP framework architecture, routing, dispatching, controllers, actions, ViewModels, templates, and HTTP request/response flow. --- ## Project Identity This project is a small PHP MVC framework called **MindVisionCode PHP**. It is intentionally inspired by a Classic ASP MVC framework style: - Central dispatcher - Controllers and actions - ViewModels - Repository classes - Simple validation - Database migrations - Small, readable files - Minimal dependencies Do not turn this into Laravel, Symfony, Slim, CakePHP, or another large framework. --- ## Request Flow ```text Browser → public/index.php → Request → Dispatcher → Router → Route → Controller → ViewModel/Repository/Service → View → Response ``` --- ## Project Structure Preferred structure: ```text 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. - Do not expose `src/`, `config/`, `tests/`, `vendor/`, `.ai/`, or `.env` files through the web server. - Put application code under `src/`. - Put generated cache/log files under `var/` or another ignored runtime directory. - Keep secrets outside the web root. --- ## Development Commands Install dependencies: ```bash composer install ``` Regenerate autoload files: ```bash composer dump-autoload ``` Run local server: ```bash php -S localhost:8000 -t public ``` Run basic tests: ```bash php tests/run.php ``` --- ## Framework Coding Rules - Keep code simple and readable. - Prefer small classes. - Use typed properties and return types where practical. - Avoid hidden magic. - Do not add dependencies without a clear reason. - Preserve the framework style. - Explain any architectural changes. --- ## Service and Request Injection `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: ```php 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: ```php // public/index.php $app->bind(Database::class, database()); ``` Then declare it as a typed parameter in any action: ```php public function index(Database $db): Response { // $db is injected automatically } ``` Do not call `Request::capture()` inside action bodies. Declare the parameter instead. --- ## Controller Rules - Keep controllers thin. - Validate request method and request shape at the boundary. - Do not put database query details directly in controllers when a repository or service is more appropriate. - Do not put template rendering logic inside business services. - Return or produce a response through the framework’s response mechanism. - Use `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: ```php if ($guard = $this->requirePost($request)) { return $guard; } ``` - Verify CSRF **before** field validation on any state-changing action. CSRF failure is a security gate, not a form validation error. See the Security skill for the helper pattern. - When a controller uses a repository across multiple methods, store it as a nullable property and lazy-initialize once — do not call `new Repository(database())` on every method call: ```php private ?EmployeeRepository $employees = null; private function employees(): EmployeeRepository { if ($this->employees === null) { $this->employees = new EmployeeRepository(database()); } return $this->employees; } ``` --- ## ViewModel Rules - Use ViewModels to shape data for views. - Keep ViewModels simple and explicit. - Avoid passing raw database rows directly into complex templates when a ViewModel would make the template clearer. - Do not put database access inside ViewModels unless the existing project convention explicitly does that. --- ## View Configuration View paths are set in `config/view.php`: ```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`. --- ## Templates and Views Keep presentation separate from business logic. Rules: - Do not query the database from templates. - Do not place business rules in templates. - Escape output by default. - Prefer simple view models or arrays passed into templates. - Use a template engine with automatic escaping only if it fits the project and does not make the framework unnecessarily large. Plain PHP template example: ```php