# Phred A PHP MVC framework: * Intended for projects of all sizes, and solo or team development. * The single router call per controller style makes it easy for teamwork without stepping on each others toes. * REQUIREMENTS * PHP 8.1+ * Primarily meant for Apache/Nginx webservers, will look into supporting other webservers in the future. * PSR-4 autoloading. * Installed through Composer (`composer create-project getphred/phred`) * Environment variables (.env) for configuration. * Supports two API formats (with content negotiation) * Pragmatic REST (default) * JSON:API * Choose via .env: * `API_FORMAT=rest` (plain JSON responses, RFC7807 error format) * `API_FORMAT=jsonapi` (JSON:API compliant documents and error objects) * Or negotiate per request using the `Accept` header: * `Accept: application/vnd.api+json` forces JSON:API for that request * TESTING environment variables (.env) * `TEST_RUNNER=codeception` * `TEST_PATH=tests` * `TEST_PATH` is relative to both project root and each module root. * Dependency Injection * Fully Pluggable, but ships with defaults: * Pluggability model (M5) * Core depends on Phred contracts and PSRs; concrete implementations are provided by Service Providers. * Providers implement `Phred\Support\Contracts\ServiceProviderInterface` with `register(ContainerBuilder)` and `boot(Container)` methods. * Providers are loaded in deterministic order: core → app → modules, configured in `config/providers.php`. * Swap packages by changing `.env` / `config/app.php` drivers and enabling a provider. * Driver keys (examples) * `ORM_DRIVER=pairity|doctrine` * `TEMPLATE_DRIVER=eyrie|twig|plates` * `FLAGS_DRIVER=flagpole|unleash` * `TEST_RUNNER=codeception` * Primary contracts * `Template\Contracts\RendererInterface` * `Orm\Contracts\ConnectionInterface` (or repositories in future milestones) * `Flags\Contracts\FeatureFlagClientInterface` * `Testing\Contracts\TestRunnerInterface`. * Default Plug-ins * Feature Flags through `getphred/flagpole` * ORM through `getphred/pairity` (handles migrations, seeds, and db access) * Unit Testing through `codeception/codeception` * Testing is provided as a CLI dev capability only; it is not part of the HTTP request lifecycle. * Template Engine through `getphred/eyrie` * Other dependencies: * Dependency Injection through `php-di/php-di` * Static Analysis through `phpstan/phpstan` * Code Style Enforcement through `friendsofphp/php-cs-fixer` * Logging through `monolog/monolog` * Config and environment handling through `vlucas/phpdotenv` * HTTP client through `guzzlehttp/guzzle` * URL extension negotiation (optional) * Opt-in middleware that parses a trailing URL extension and hints content negotiation. * Enable via env: `URL_EXTENSION_NEGOTIATION=true` (default true) * Control allowed extensions via: `URL_EXTENSION_WHITELIST="json|php|none"` * Defaults to `json|php|none`. XML support will be added in M12. * Examples: * `/users/1.json` → JSON response * `/users/1` or `/users/1.php` → HTML (views) by convention * Extensible: future formats can be added with a factory and whitelisting. * CONTROLLERS * Invokable controllers (Actions), * Single router call per controller, * `public function __invoke(Request $request)` method entry point on controller class, * Response helpers via dependency injection: * Inject `Phred\Http\Contracts\ApiResponseFactoryInterface` to build responses consistently across formats. * The factory is negotiated per request (env default or `Accept` header) and sets appropriate `Content-Type`. * Common methods: `ok(array $data)`, `created(array $data, ?string $location)`, `noContent()`, `error(int $status, string $title, ?string $detail, array $extra = [])`. * Example: ```php use Phred\Http\Contracts\ApiResponseFactoryInterface as Responses; use Psr\Http\Message\ServerRequestInterface as Request; use Phred\Http\Middleware\ContentNegotiationMiddleware as Negotiation; final class ExampleController { public function __construct(private Responses $responses) {} public function __invoke(Request $request) { $format = $request->getAttribute(Negotiation::ATTR_API_FORMAT, 'rest'); return $this->responses->ok(['format' => $format]); } } ``` * MVC controller bases (M6) * For API endpoints inside modules, extend `Phred\Mvc\APIController` and use response helpers: ```php use Phred\Mvc\APIController; use Psr\Http\Message\ServerRequestInterface as Request; final class UserShowController extends APIController { public function __invoke(Request $request) { $user = ['id' => 123, 'name' => 'Ada']; return $this->ok(['user' => $user]); } } ``` * For HTML endpoints inside modules, extend `Phred\Mvc\ViewController` and delegate to a module `View`: ```php use Phred\Mvc\ViewController; use Psr\Http\Message\ServerRequestInterface as Request; final class HomePageController extends ViewController { public function __invoke(Request $request, HomePageView $view) { // domain data (normally from a Service) $data = ['title' => 'Welcome', 'name' => 'world']; // Use the view's default template by omitting the template param return $this->renderView($view, $data); // Or override template explicitly via 3rd param: // return $this->renderView($view, $data, 'home'); } } ``` * VIEWS * Classes for data manipulation/preparation before rendering Templates, * `$this->render(, );` to render a template. * Base class (M6): extend `Phred\Mvc\View` in your module and optionally override `transformData()`: ```php use Phred\Mvc\View; final class HomePageView extends View { protected string $template = 'home'; // default template protected function transformData(array $data): array { $data['upper'] = strtoupper($data['name'] ?? ''); return $data; } } ``` * With the default Eyrie renderer, a simple template string `"

Hello {{upper}}

"` would produce `

Hello WORLD

`. * Template selection conventions (M6) * Controllers always call `renderView($view, $data, ?$templateOverride)`: - Use default template: `renderView($view, $data)` (no third parameter) - Override template explicitly: `renderView($view, $data, 'template_name')` * Views own default template selection and presentation logic: - Declare `protected string $template = 'default_name';` or override `defaultTemplate()` for dynamic selection - The `View` decides which template to use when the controller does not pass an override * Rationale: keeps template/presentation decisions in the View layer; controllers only make explicit overrides when necessary (flags, A/B tests, special flows). * SERVICES * for business logic. * SERVICE PROVIDERS (M5) * for dependency injection and runtime bootstrapping. * Configure providers in `config/providers.php`: ```php return [ 'core' => [ Phred\Providers\Core\RoutingServiceProvider::class, Phred\Providers\Core\TemplateServiceProvider::class, Phred\Providers\Core\OrmServiceProvider::class, Phred\Providers\Core\FlagsServiceProvider::class, Phred\Providers\Core\TestingServiceProvider::class, ], 'app' => [ Phred\Providers\AppServiceProvider::class, ], 'modules' => [], ]; ``` * Add routes from a provider using the `RouteRegistry` helper: ```php use Phred\Http\Routing\RouteRegistry; use Phred\Http\Router; RouteRegistry::add(static function ($collector, Router $router): void { $router->get('/hello', fn() => ['message' => 'Hello from a provider']); }); ``` * Select drivers via env or `config/app.php` under `drivers`: - `TEMPLATE_DRIVER=eyrie` - `ORM_DRIVER=pairity` - `FLAGS_DRIVER=flagpole` - `TEST_RUNNER=codeception` * Defaults are provided by core providers and can be swapped by changing these keys and adding alternate providers. * MIGRATIONS * for database changes. * Modular separation, similar to Django apps. * Nested Models * Nested Controllers * Nested Views * Nested Services * Nested Migrations * Nested Service Providers * Nested Routes * Nested Templates * Nested Tests * CLI Helper called phred * `php phred create:command ` // Creates a CLI command under `console/commands * `php phred create:module ` // Creates a module * `php phred create::controller` // Creates a controller in the specified module * `php phred create::model` // Creates a model in the specified module * `php phred create::migration` // Creates a migration in the specified module * `php phred create::seed` // Creates a seeder in the specified module * `php phred create::test` // Creates a test in the specified module * `php phred create::view` / Creates a view in the specified module * `php phred db:backup` // Backup the database * `php phred db:restore -f ` // Restore the database from the specified backup file * `php phred migrate [-m ]` // Migrate entire project or module * `php phred migration:rollback [-m ]` // Rollback entire project or module * `php phred seed` * `php phred seed:rollback` * `php phred test[:]` // Test entire project or module * Runs tests using the configured test runner (dev only). * Requires `require-dev` dependencies. * `php phred run [-p ]` * Spawns a local PHP webserver on port 8000 (unless specified otherwise using `-p`) * CLI Helper is extendable through CLI Commands. Command discovery * Core commands (bundled with Phred) are discovered from `src/commands`. * User/project commands are discovered from `console/commands` in your project root. Run the CLI: ``` php phred list ``` Add your own command by creating a PHP file under `console/commands`, returning an instance of `Phred\Console\Command` (or an anonymous class extending it). (Or by running `php phred create:command `) Example: ``` writeln('Hello!'); return 0; } }; ``` Configuration and environment - Phred uses `vlucas/phpdotenv` to load a `.env` file from your project root (loaded in `bootstrap/app.php`). - Access configuration anywhere via `Phred\Support\Config::get(, )`. - Precedence: environment variables > config files > provided default. - Keys may be provided in either UPPER_SNAKE (e.g., `APP_ENV`) or dot.notation (e.g., `app.env`). - Config files live under `config/*.php` and return arrays; dot keys are addressed as `.` (e.g., `app.timezone`). Common keys - `APP_ENV` (default from config/app.php: `local`) - `APP_DEBUG` (`true`/`false`) - `APP_TIMEZONE` (default `UTC`) - `API_FORMAT` (`rest` | `jsonapi`; default `rest`) API formats and negotiation - Middleware `ContentNegotiationMiddleware` determines the active API format per request. - Precedence: 1. `Accept: application/vnd.api+json` → JSON:API 2. `.env`/config `API_FORMAT` (fallback to `rest`) - The chosen format is stored on the request as `phred.api_format` and used by the injected `ApiResponseFactoryInterface` to produce the correct response shape and `Content-Type`. - Demo endpoint: `GET /_phred/format` responds with the active format; `GET /_phred/health` returns a simple JSON 200. Examples ```php use Phred\Support\Config; $env = Config::get('APP_ENV', 'local'); // reads from env, then config/app.php, else 'local' $tz = Config::get('app.timezone', 'UTC'); // reads nested key from config files $fmt = strtolower(Config::get('API_FORMAT', 'rest')); ```