Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 13 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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) {
Expand All @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
},
"autoload": {
"psr-4": {
"App\\": "app/"
"App\\": "src/"
}
},
"autoload-dev": {
Expand Down
10 changes: 7 additions & 3 deletions bootstrap/app.php → config/bootstrap/app.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand All @@ -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();
Expand All @@ -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;
2 changes: 1 addition & 1 deletion public/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
File renamed without changes.
File renamed without changes.
2 changes: 1 addition & 1 deletion routes/api.php → src/Routers/api.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions tests/ApiTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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();
}

Expand Down
40 changes: 40 additions & 0 deletions tests/ConfigLayoutTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

declare(strict_types=1);

namespace Tests;

use PHPUnit\Framework\TestCase;

/**
* The core loads every config/*.php (one level) as a configuration file. A script placed there
* (e.g. the bootstrap) would run while the Application is being built — keep scripts in
* subdirectories such as config/bootstrap/.
*/
final class ConfigLayoutTest extends TestCase
{
public function testEveryTopLevelConfigFileReturnsAnArray(): void
{
$files = glob(dirname(__DIR__) . '/config/*.php');
$this->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');
}
}
Loading