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 0cb49c71df Refactor M7 module scaffolding, route inclusion, and tests; implement providers discovery; fix URL extension negotiation; clean docs
• Add Service Providers loading from config/providers.php and merge with runtime config; ensure AppServiceProvider boots and contributes routes
• Create RouteGroups and guard module route includes in routes/web.php; update Kernel to auto-mount module routes and apply provider routes
• Implement create:module as a console Command (extends Phred\Console\Command):
	◦ Args: name, prefix; Flags: --update-composer, --no-dump
	◦ Stable root resolution (dirname(DIR, 2)); robust args/flags handling under ArrayInput
	◦ Scaffolds module dirs (Controllers, Views, Templates, Routes, Providers, etc.), ensures Controllers exists, adds .gitkeep
	◦ Writes Provider, View, Controller, Template stubs (fix variable interpolation via placeholders)
	◦ Appends guarded include snippet to routes/web.php
	◦ Optional composer PSR-4 mapping update (+ backup) and optional autoload dump
	◦ Prevents providers.php corruption via name validation and existence checks
• Add URL extension negotiation middleware tweaks:
	◦ Only set Accept for .json (and future .xml), never for none/php
	◦ Never override existing Accept header
• Add MVC base classes (Controller, APIController, ViewController, View, ViewWithDefaultTemplate); update ViewController signature and View render contract
• Add tests:
	◦ CreateModuleCommandTest with setup/teardown to snapshot/restore routes/web.php and composer.json; asserts scaffold and PSR-4 mapping
	◦ ProviderRouteTest for provider-contributed route
	◦ UrlExtensionNegotiationTest sets API_FORMAT=rest and asserts content-type behavior
	◦ MvcViewTest validates transformData+render
• Fix config/providers.php syntax and add comment placeholder for modules
• Update README: M5/M6/M7 docs, MVC examples, template selection conventions, modules section, URL extension negotiation, and module creation workflow
• Update MILESTONES.md: mark M6/M7 complete; add M8 task for register:orm; note M12 XML extension support
2025-12-16 16:14:22 -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 Refactor M7 module scaffolding, route inclusion, and tests; implement providers discovery; fix URL extension negotiation; clean docs 2025-12-16 16:14:22 -06:00
tests Refactor M7 module scaffolding, route inclusion, and tests; implement providers discovery; fix URL extension negotiation; clean docs 2025-12-16 16:14:22 -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 Refactor M7 module scaffolding, route inclusion, and tests; implement providers discovery; fix URL extension negotiation; clean docs 2025-12-16 16:14:22 -06:00
composer.json.bak Refactor M7 module scaffolding, route inclusion, and tests; implement providers discovery; fix URL extension negotiation; clean docs 2025-12-16 16:14:22 -06:00
LICENSE Initial commit 2025-12-09 21:32:51 +00:00
MILESTONES.md Refactor M7 module scaffolding, route inclusion, and tests; implement providers discovery; fix URL extension negotiation; clean docs 2025-12-16 16:14:22 -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 Refactor M7 module scaffolding, route inclusion, and tests; implement providers discovery; fix URL extension negotiation; clean docs 2025-12-16 16:14:22 -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.
  • MODULES (M7)
    • Django-style: all user Controllers, Views, Templates, Services, etc. live inside modules.
    • Suggested module layout (ORM-agnostic):
      • modules/<Module>/Controllers/
      • modules/<Module>/Views/
      • modules/<Module>/Templates/
      • modules/<Module>/Services/
      • modules/<Module>/Models/ (domain models, ORM-neutral)
      • modules/<Module>/Repositories/ (interfaces consumed by services)
      • modules/<Module>/Persistence/Pairity/ (DAOs, DTOs, mappers, repo implementations)
      • modules/<Module>/Persistence/Eloquent/ (Eloquent models, repo implementations)
      • modules/<Module>/Database/Migrations/ (canonical migrations for the module)
      • modules/<Module>/Routes/web.php and api.php
      • modules/<Module>/Providers/*ServiceProvider.php
    • Swap ORM driver (ORM_DRIVER=pairity|eloquent) without changing services/controllers: providers bind repository interfaces to driver-specific implementations. Migrations remain a single canonical set per module.
    • Creating a Module
      • With prompt (interactive):
        php phred create:module Blog
        # Enter URL prefix for module 'Blog' [/blog]:
        
        • The command will scaffold modules/Blog/*, register the provider in config/providers.php (modules section), and append an inclusion snippet to routes/web.php mounting the module at /blog.
      • Without prompt (explicit prefix argument):
        php phred create:module Blog /blog
        # or
        php phred create:module Blog --prefix=/blog
        
      • After creation, add PSR-4 to composer.json and dump autoload:
        {
          "autoload": {
            "psr-4": {
              "Project\\\\Modules\\\\Blog\\\\": "modules/Blog/"
            }
          }
        }
        
        composer dump-autoload
        
      • Route inclusion pattern in routes/web.php (added automatically):
        use Phred\\Http\\Routing\\RouteGroups;
        use Phred\\Http\\Router;
        
        // Module 'Blog' mounted at '/blog'
        RouteGroups::include($router, '/blog', function (Router $router) {
            /** @noinspection PhpIncludeInspection */
            (static function ($router) { require __DIR__ . '/../modules/Blog/Routes/web.php'; })($router);
        });
        
      • Define module-local routes relative to the module in modules/Blog/Routes/web.php:
        use Phred\\Http\\Router;
        use Project\\Modules\\Blog\\Controllers as C;
        
        $router->get('/', C\\HomeController::class);          // -> /blog
        $router->get('/posts', C\\PostIndexController::class); // -> /blog/posts
        
  • 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'));