A PHP MVC framework with invokable controllers (Actions), Views are classes for data manipulation before rendering Templates, Models are partitioned between DAO and DTO objects. And Modular separation, similar to Django apps.
Go to file
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
.github/workflows initial commit 2025-12-14 17:10:01 -06:00
bin initial commit 2025-12-14 17:10:01 -06:00
src Implement M5 service providers, M6 MVC bases, and URL extension negotiation; update docs and tests 2025-12-15 16:08:57 -06:00
tests Implement M5 service providers, M6 MVC bases, and URL extension negotiation; update docs and tests 2025-12-15 16:08:57 -06:00
.editorconfig initial commit 2025-12-14 17:10:01 -06:00
.env.example initial commit 2025-12-14 17:10:01 -06:00
.gitattributes initial commit 2025-12-14 17:10:01 -06:00
.gitignore refactor(core): enforce SOLID across HTTP pipeline; add small contracts and defaults; align tests 2025-12-15 09:15:49 -06:00
composer.json initial commit 2025-12-14 17:10:01 -06:00
LICENSE Initial commit 2025-12-09 21:32:51 +00:00
MILESTONES.md Implement M5 service providers, M6 MVC bases, and URL extension negotiation; update docs and tests 2025-12-15 16:08:57 -06:00
phpstan.neon.dist initial commit 2025-12-14 17:10:01 -06:00
phred initial commit 2025-12-14 17:10:01 -06:00
phred.bat initial commit 2025-12-14 17:10:01 -06:00
README.md Implement M5 service providers, M6 MVC bases, and URL extension negotiation; update docs and tests 2025-12-15 16:08:57 -06:00

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