Phred/README.md
Funky Waddle 7d4265d60e Implement M5 service providers, M6 MVC bases, and URL extension negotiation; update docs and tests
• 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
2025-12-15 16:08:57 -06:00

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 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:
        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:
        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:
        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(<template_name>, <data_array>); to render a template.
      • Base class (M6): extend Phred\Mvc\View in your module and optionally override transformData():
        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')
      • 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:
      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:
      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 <name> // Creates a CLI command under `console/commands
    • php phred create:module <name> // Creates a module
    • php phred create:<module>:controller // Creates a controller in the specified module
    • php phred create:<module>:model // Creates a model in the specified module
    • php phred create:<module>:migration // Creates a migration in the specified module
    • php phred create:<module>:seed // Creates a seeder in the specified module
    • php phred create:<module>:test // Creates a test in the specified module
    • php phred create:<module>:view / Creates a view in the specified module
    • php phred db:backup // Backup the database
    • php phred db:restore -f <db_backup_file> // Restore the database from the specified backup file
    • php phred migrate [-m <module>] // Migrate entire project or module
    • php phred migration:rollback [-m <module>] // Rollback entire project or module
    • php phred seed
    • php phred seed:rollback
    • php phred test[:<module>] // Test entire project or module
      • Runs tests using the configured test runner (dev only).
      • Requires require-dev dependencies.
    • php phred run [-p <port>]
      • 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 <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/phpdotenv to load a .env file from your project root (loaded in bootstrap/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/*.php and 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 (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

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'));