From 4528b04502166b41ced2f968cfbb8d23ce7bb7ef Mon Sep 17 00:00:00 2001 From: Caio Fernandes Date: Sat, 10 Oct 2026 20:13:56 -0300 Subject: [PATCH] =?UTF-8?q?feat!(SPEC-107):=20src/=20layout=20=E2=80=94=20?= =?UTF-8?q?src/Controllers,=20src/Routers,=20config/bootstrap?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - app/Controllers -> src/Controllers (PSR-4 App\ -> src/), routes -> src/Routers, bootstrap/app.php -> config/bootstrap/app.php. - ConfigLayoutTest: every config/*.php must return an array (the core loads them all). Closes #17 Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 15 ++++++++ README.md | 21 ++++++----- composer.json | 2 +- {bootstrap => config/bootstrap}/app.php | 10 ++++-- public/index.php | 2 +- {app => src}/Controllers/ApiController.php | 0 {app => src}/Controllers/UserController.php | 0 {routes => src/Routers}/api.php | 2 +- tests/ApiTest.php | 4 +-- tests/ConfigLayoutTest.php | 40 +++++++++++++++++++++ 10 files changed, 80 insertions(+), 16 deletions(-) rename {bootstrap => config/bootstrap}/app.php (73%) rename {app => src}/Controllers/ApiController.php (100%) rename {app => src}/Controllers/UserController.php (100%) rename {routes => src/Routers}/api.php (97%) create mode 100644 tests/ConfigLayoutTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index ef69ea3..968a182 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,21 @@ All notable changes to the PivotPHP Skeleton project will be documented in this The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [4.0.0] - 2026-10-10 + +### Changed (breaking — template layout) + +- **New layout** (SPEC-107): `app/Controllers/` → `src/Controllers/`, `routes/api.php` → + `src/Routers/api.php`, `bootstrap/app.php` → `config/bootstrap/app.php`. PSR-4 now maps `App\` to + `src/` (namespaces unchanged). Projects already created are not affected; to adopt the layout in an + existing project, move the files and update `composer.json` (`"App\\": "src/"`) and the `require` + paths in `public/index.php` and the tests. + +### Added + +- `tests/ConfigLayoutTest.php`: every `config/*.php` must return a configuration array — the core loads + them all, so scripts (like the bootstrap) must live in a subdirectory. + ## [3.0.0] - 2026-10-10 ### Changed diff --git a/README.md b/README.md index ca82219..8064cfd 100644 --- a/README.md +++ b/README.md @@ -32,14 +32,19 @@ in `UserController` with your persistence layer. ## Structure ``` -app/Controllers/ ApiController (container-injected), UserController (CRUD example) -bootstrap/app.php builds the application: config, middlewares, controllers, routes -config/app.php app.* configuration, read from the environment -public/index.php HTTP entry point -routes/api.php routes -tests/ApiTest.php tests through the real application ($app->handle()) +config/ + app.php app.* configuration, read from the environment + bootstrap/app.php builds the application: config, middlewares, controllers, routes +public/index.php HTTP entry point +src/ + Controllers/ ApiController (container-injected), UserController (CRUD example) + Routers/api.php routes +tests/ApiTest.php tests through the real application ($app->handle()) ``` +Every `config/*.php` is loaded by the core as a configuration file (returning an array). Scripts such +as the bootstrap live in a subdirectory (`config/bootstrap/`), which the core does not scan. + ## Configuration `.env` is loaded before `config/`; real environment variables take precedence. @@ -54,7 +59,7 @@ tests/ApiTest.php tests through the real application ($app->handle()) ## Writing routes ```php -// routes/api.php +// src/Routers/api.php $app->get('/hello/:name', fn ($req, $res) => $res->json(['hello' => $req->param('name')])); $app->post('/items', function ($req, $res) { @@ -65,7 +70,7 @@ $app->post('/items', function ($req, $res) { Handlers receive `PivotPHP\Http\ExpressRequest`/`ExpressResponse` and must return the response. Controllers are array callables (`[UserController::class, 'index']`); register controllers that need -constructor dependencies in the container (see `bootstrap/app.php`). +constructor dependencies in the container (see `config/bootstrap/app.php`). Security middlewares (CORS, security headers, JWT, CSRF, rate limiting) come from [`pivotphp/security`](https://github.com/PivotPHP/pivotphp-security); this template enables CORS. diff --git a/composer.json b/composer.json index 334ae7d..77fece9 100644 --- a/composer.json +++ b/composer.json @@ -32,7 +32,7 @@ }, "autoload": { "psr-4": { - "App\\": "app/" + "App\\": "src/" } }, "autoload-dev": { diff --git a/bootstrap/app.php b/config/bootstrap/app.php similarity index 73% rename from bootstrap/app.php rename to config/bootstrap/app.php index ef49293..1833fda 100644 --- a/bootstrap/app.php +++ b/config/bootstrap/app.php @@ -5,6 +5,10 @@ /** * Builds the application. Used by public/index.php (HTTP) and by the tests. * + * Keep this file in config/bootstrap/: the core loads every config/*.php (one level, no + * subdirectories) as a configuration file, so a script directly in config/ would be executed + * while the Application is being built. + * * Boot order: boot() loads .env and config/ first, so the configuration is available * when middlewares and routes are registered. */ @@ -15,9 +19,9 @@ use PivotPHP\Security\Cors\CorsConfig; use PivotPHP\Security\Cors\CorsMiddleware; -require_once __DIR__ . '/../vendor/autoload.php'; +require_once __DIR__ . '/../../vendor/autoload.php'; -$app = Application::create(dirname(__DIR__)); +$app = Application::create(dirname(__DIR__, 2)); $app->boot(); $config = $app->getConfig(); @@ -32,6 +36,6 @@ // dependencies are instantiated directly. $app->singleton(ApiController::class, fn () => new ApiController($app)); -require __DIR__ . '/../routes/api.php'; +require __DIR__ . '/../../src/Routers/api.php'; return $app; diff --git a/public/index.php b/public/index.php index 7ff1a59..a9485e8 100644 --- a/public/index.php +++ b/public/index.php @@ -3,6 +3,6 @@ declare(strict_types=1); /** @var \PivotPHP\Core\Core\Application $app */ -$app = require __DIR__ . '/../bootstrap/app.php'; +$app = require __DIR__ . '/../config/bootstrap/app.php'; $app->run(); diff --git a/app/Controllers/ApiController.php b/src/Controllers/ApiController.php similarity index 100% rename from app/Controllers/ApiController.php rename to src/Controllers/ApiController.php diff --git a/app/Controllers/UserController.php b/src/Controllers/UserController.php similarity index 100% rename from app/Controllers/UserController.php rename to src/Controllers/UserController.php diff --git a/routes/api.php b/src/Routers/api.php similarity index 97% rename from routes/api.php rename to src/Routers/api.php index 08b9cb6..fc5b1fc 100644 --- a/routes/api.php +++ b/src/Routers/api.php @@ -3,7 +3,7 @@ declare(strict_types=1); /** - * API routes. $app is the PivotPHP\Core\Core\Application built in bootstrap/app.php. + * API routes. $app is the PivotPHP\Core\Core\Application built in config/bootstrap/app.php. * * Handlers receive PivotPHP\Http\ExpressRequest ($req) and ExpressResponse ($res) and must * return the response. diff --git a/tests/ApiTest.php b/tests/ApiTest.php index 999570e..29a2ec4 100644 --- a/tests/ApiTest.php +++ b/tests/ApiTest.php @@ -10,7 +10,7 @@ use Psr\Http\Message\ResponseInterface; /** - * Exercises the real application (bootstrap/app.php) through Application::handle(). + * Exercises the real application (config/bootstrap/app.php) through Application::handle(). */ final class ApiTest extends TestCase { @@ -36,7 +36,7 @@ protected function setUp(): void $_ENV[$key] = $value; } - $this->app = require __DIR__ . '/../bootstrap/app.php'; + $this->app = require __DIR__ . '/../config/bootstrap/app.php'; $this->factory = new Psr17Factory(); } diff --git a/tests/ConfigLayoutTest.php b/tests/ConfigLayoutTest.php new file mode 100644 index 0000000..bf63811 --- /dev/null +++ b/tests/ConfigLayoutTest.php @@ -0,0 +1,40 @@ +assertNotEmpty($files); + + foreach ($files as $file) { + // Checked before require: a bootstrap script here would build the app recursively. + $source = (string) file_get_contents($file); + $this->assertDoesNotMatchRegularExpression( + '/Application::create|new\s+Application\b|vendor\/autoload\.php/', + $source, + basename($file) . ' looks like a script; move it to a subdirectory (e.g. config/bootstrap/)' + ); + + $value = (static fn (string $path): mixed => require $path)($file); + $this->assertIsArray($value, basename($file) . ' must return a configuration array'); + } + } + + public function testBootstrapLivesInASubdirectory(): void + { + $this->assertFileExists(dirname(__DIR__) . '/config/bootstrap/app.php'); + $this->assertFileDoesNotExist(dirname(__DIR__) . '/config/bootstrap.php'); + } +}