- Introduce small interfaces and default adapters (DIP): - Support\Contracts\ConfigInterface + Support\DefaultConfig - Http\Contracts\ErrorFormatNegotiatorInterface + Http\Support\DefaultErrorFormatNegotiator - Http\Contracts\RequestIdProviderInterface + Http\Support\DefaultRequestIdProvider - Http\Contracts\ExceptionToStatusMapperInterface + Http\Support\DefaultExceptionToStatusMapper - Kernel: bind new contracts in the container; keep DelegatingApiResponseFactory wiring - ContentNegotiationMiddleware: depend on ConfigInterface + negotiator; honor Accept for JSON:API - ProblemDetailsMiddleware: inject negotiator + config; split into small helpers; deterministic content negotiation; stable Whoops HTML; include X-Request-Id - DispatchMiddleware: SRP refactor into small methods; remove hidden coupling; normalize non-Response returns - Add/adjust tests: - tests/ErrorHandlingTest.php for problem details, JSON:API errors, and Whoops HTML - tests/ContentNegotiationTest.php for format selection - tests/MakeCommandTest.php aligned with create:command scaffolder - Docs/Meta: update README and MILESTONES; .gitignore to ignore .junie.json No runtime behavior changes intended beyond clearer DI boundaries and content-negotiation determinism. All tests green.
7.7 KiB
7.7 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
- Core depends on Phred contracts (
Phred\Contracts\*) and PSRs - Concrete implementations are provided by Service Providers.
- Swap packages by changing
.envand enabling a provider.
- Core depends on Phred contracts (
- Driver keys (examples)
ORM_DRIVER=pairity|doctrineTEMPLATE_DRIVER=eyrie|twig|platesFLAGS_DRIVER=flagpole|unleashTEST_RUNNER=codeception
- Primary contracts
Template\RendererInterfaceOrm\EntityManagerInterface(or repositories)Flags\FeatureFlagClientInterfaceTesting\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
- 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
- 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
- VIEWS
- Classes for data manipulation/preparation before rendering Templates,
$this->render(<template_name>, <data_array>);to render a template.
- Classes for data manipulation/preparation before rendering Templates,
- SERVICES
- for business logic.
- SERVICE PROVIDERS
- for dependency injection.
- 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'));