• M5: Add ServiceProviderInterface and ProviderRepository; integrate providers into Kernel (register before container build, boot after); add RouteRegistry with clear(); add default core providers (Routing, Template, ORM, Flags, Testing) and AppServiceProvider; add contracts and default drivers (Template/Eyrie, Orm/Pairity, Flags/Flagpole, Testing/Codeception) • Routing: allow providers to contribute routes; add ProviderRouteTest • Config: add config/providers.php; extend config/app.php with driver keys; document env keys • M6: Introduce MVC bases: Controller, APIController (JSON helpers), ViewController (html + renderView helpers), View (transformData + renderer); add ViewWithDefaultTemplate and default-template flow; adjust method signatures to data-first and delegate template override to View • HTTP: Add UrlExtensionNegotiationMiddleware (opt-in via URL_EXTENSION_NEGOTIATION, whitelist via URL_EXTENSION_WHITELIST with default json|php|none); wire before ContentNegotiationMiddleware • Tests: add UrlExtensionNegotiationTest and MvcViewTest; ensure RouteRegistry::clear prevents duplicate routes in tests • Docs: Update README with M5 provider usage, M6 MVC examples and template selection conventions, and URL extension negotiation; mark M5 complete in MILESTONES; add M12 task to provide XML support and enable xml in whitelist by default
12 KiB
12 KiB
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
Acceptheader:Accept: application/vnd.api+jsonforces JSON:API for that request
- TESTING environment variables (.env)
TEST_RUNNER=codeceptionTEST_PATH=testsTEST_PATHis 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\ServiceProviderInterfacewithregister(ContainerBuilder)andboot(Container)methods. - Providers are loaded in deterministic order: core → app → modules, configured in
config/providers.php. - Swap packages by changing
.env/config/app.phpdrivers and enabling a provider.
- Driver keys (examples)
ORM_DRIVER=pairity|doctrineTEMPLATE_DRIVER=eyrie|twig|platesFLAGS_DRIVER=flagpole|unleashTEST_RUNNER=codeception
- Primary contracts
Template\Contracts\RendererInterfaceOrm\Contracts\ConnectionInterface(or repositories in future milestones)Flags\Contracts\FeatureFlagClientInterfaceTesting\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
- Feature Flags through
- Pluggability model (M5)
- 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
- Dependency Injection through
- 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.
- Defaults to
- Examples:
/users/1.json→ JSON response/users/1or/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\ApiResponseFactoryInterfaceto build responses consistently across formats. - The factory is negotiated per request (env default or
Acceptheader) and sets appropriateContent-Type. - Common methods:
ok(array $data),created(array $data, ?string $location),noContent(),error(int $status, string $title, ?string $detail, array $extra = []). - Example:
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]); } }
- Inject
- MVC controller bases (M6)
- For API endpoints inside modules, extend
Phred\Mvc\APIControllerand use response helpers: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\ViewControllerand delegate to a moduleView: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'); } }
- For API endpoints inside modules, extend
- VIEWS
- Classes for data manipulation/preparation before rendering Templates,
$this->render(<template_name>, <data_array>);to render a template.- Base class (M6): extend
Phred\Mvc\Viewin your module and optionally overridetransformData():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
"<h1>Hello {{upper}}</h1>"would produce<h1>Hello WORLD</h1>.
- 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')
- Use default template:
- Views own default template selection and presentation logic:
- Declare
protected string $template = 'default_name';or overridedefaultTemplate()for dynamic selection - The
Viewdecides which template to use when the controller does not pass an override
- Declare
- Rationale: keeps template/presentation decisions in the View layer; controllers only make explicit overrides when necessary (flags, A/B tests, special flows).
- Controllers always call
- Classes for data manipulation/preparation before rendering Templates,
- SERVICES
- for business logic.
- SERVICE PROVIDERS (M5)
- for dependency injection and runtime bootstrapping.
- Configure providers in
config/providers.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
RouteRegistryhelper: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.phpunderdrivers:TEMPLATE_DRIVER=eyrieORM_DRIVER=pairityFLAGS_DRIVER=flagpoleTEST_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 <name>// Creates a CLI command under `console/commandsphp phred create:module <name>// Creates a modulephp phred create:<module>:controller// Creates a controller in the specified modulephp phred create:<module>:model// Creates a model in the specified modulephp phred create:<module>:migration// Creates a migration in the specified modulephp phred create:<module>:seed// Creates a seeder in the specified modulephp phred create:<module>:test// Creates a test in the specified modulephp phred create:<module>:view/ Creates a view in the specified modulephp phred db:backup// Backup the databasephp phred db:restore -f <db_backup_file>// Restore the database from the specified backup filephp phred migrate [-m <module>]// Migrate entire project or modulephp phred migration:rollback [-m <module>]// Rollback entire project or modulephp phred seedphp phred seed:rollbackphp phred test[:<module>]// Test entire project or module- Runs tests using the configured test runner (dev only).
- Requires
require-devdependencies.
php phred run [-p <port>]- Spawns a local PHP webserver on port 8000 (unless specified otherwise using
-p)
- Spawns a local PHP webserver on port 8000 (unless specified otherwise using
- 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/commandsin 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 <name>)
Example:
<?php
use Phred\Console\Command;
use Symfony\Component\Console\Input\InputInterface as Input;
use Symfony\Component\Console\Output\OutputInterface as Output;
return new class extends Command {
protected string $command = 'hello:world';
protected string $description = 'Example user command';
public function handle(Input $in, Output $out): int { $out->writeln('Hello!'); return 0; }
};
Configuration and environment
- Phred uses
vlucas/phpdotenvto load a.envfile from your project root (loaded inbootstrap/app.php). - Access configuration anywhere via
Phred\Support\Config::get(<key>, <default>).- 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/*.phpand return arrays; dot keys are addressed as<file>.<path>(e.g.,app.timezone).
Common keys
APP_ENV(default from config/app.php:local)APP_DEBUG(true/false)APP_TIMEZONE(defaultUTC)API_FORMAT(rest|jsonapi; defaultrest)
API formats and negotiation
- Middleware
ContentNegotiationMiddlewaredetermines the active API format per request. - Precedence:
Accept: application/vnd.api+json→ JSON:API.env/configAPI_FORMAT(fallback torest)
- The chosen format is stored on the request as
phred.api_formatand used by the injectedApiResponseFactoryInterfaceto produce the correct response shape andContent-Type. - Demo endpoint:
GET /_phred/formatresponds with the active format;GET /_phred/healthreturns a simple JSON 200.
Examples
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'));