diff --git a/.github/workflows/polish-the-code.yml b/.github/workflows/polish-the-code.yml index 5fd7646..1cb8ccd 100644 --- a/.github/workflows/polish-the-code.yml +++ b/.github/workflows/polish-the-code.yml @@ -33,11 +33,12 @@ jobs: # Limit the running time timeout-minutes: 10 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: # checkout PR HEAD commit ref: ${{ github.event.pull_request.head.sha }} - fetch-depth: 0 # required for merge-base check + # required for merge-base check + fetch-depth: 0 persist-credentials: false - uses: commit-check/commit-check-action@v2 env: @@ -54,7 +55,7 @@ jobs: # Note: https://docs.github.com/en/actions/using-workflows/reusing-workflows The strategy property is not supported in any job that calls a reusable workflow. php-composer-unit-stan: - uses: WorkOfStan/seablast-actions/.github/workflows/php-composer-dependencies-reusable.yml@v0.2.9 + uses: WorkOfStan/seablast-actions/.github/workflows/php-composer-dependencies-reusable.yml@v0.2.11 with: # JSON php-version: '["7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3", "8.4", "8.5"]' @@ -95,11 +96,11 @@ jobs: super-linter: needs: phpcs-phpcbf - uses: WorkOfStan/seablast-actions/.github/workflows/linter.yml@v0.2.9 + uses: WorkOfStan/seablast-actions/.github/workflows/linter.yml@v0.2.11 with: # exclude third-party code filter-regex-exclude: ".*/assets/uls/.*" runs-on: "ubuntu-latest" - # todo fix fix assets/seablast.css and then allow again CSS validation + # todo fix assets/seablast.css and then allow again CSS validation validate-css: false validate-spell-codespell: true diff --git a/.gitignore b/.gitignore index a4c1419..1bc0af0 100644 --- a/.gitignore +++ b/.gitignore @@ -3,13 +3,12 @@ !/cache/.htaccess /log/* !/log/.htaccess +/.composer-cache +/.*.cache /.sass-cache/ -/.php_cs.cache -/.php-cs-fixer.cache -/.phpunit.result.cache #disable private local files -*.local.php +*.local.* #disable composer-managed libraries /vendor/ diff --git a/.htaccess b/.htaccess index 7b52125..45492bc 100644 --- a/.htaccess +++ b/.htaccess @@ -9,6 +9,8 @@ RewriteRule ^src(/|$) - [R=404,L] RedirectMatch 404 \/tests\/ RedirectMatch 404 \/views\/ # hide these files +RedirectMatch 404 (^|/)composer\.(json|lock)$ +RedirectMatch 404 (^|/)\.phpunit\.result\.cache$ RedirectMatch 404 phpstan\.neon\.dist RedirectMatch 404 phpunit\.xml # hide files with these extensions @@ -17,4 +19,5 @@ RedirectMatch 404 \.neon$ RedirectMatch 404 \.sh$ RedirectMatch 404 \.yml$ # hide all the files in any directory that have no filename but only an extension (like .prettierignore) -RedirectMatch 404 /(\.[^.]+)$ +# hide all dotfiles in any directory, including multi-dot files like .phpunit.result.cache +RedirectMatch 404 (^|/)\.[^/]+$ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..e020966 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,375 @@ +# AGENTS.md + +This file is the maintainer and integration guide for `seablast/seablast`. +It is written for two audiences: + +- maintainers changing the framework itself +- applications that build on top of Seablast and need to understand its real runtime contract + +This document describes the current behavior of the codebase, including important caveats that are not obvious from the marketing-level `README.md` alone. + +## What Seablast Is + +Seablast is a composer-installed minimalist MVC runtime for PHP applications. +The core runtime is small and centered around four classes: + +1. `SeablastSetup` +2. `SeablastController` +3. `SeablastModel` +4. `SeablastView` + +Applications extend the framework mostly by: + +- providing configuration closures in `conf/app.conf.php` and `conf/app.conf.local.php` +- registering routes in `SeablastConstant::APP_MAPPING` +- implementing models that return `stdClass` +- overriding or inheriting Latte templates + +Optional integrations include: + +- `seablast/auth` +- `seablast/i18n` + +## Bootstrap and Request Lifecycle + +The runtime flow is: + +1. `defineAppDir.php` defines `APP_DIR`. + If the library is executed from `vendor/seablast/seablast`, `APP_DIR` resolves to the application root. +2. `index.php` loads `APP_DIR . '/vendor/autoload.php'`, enables Tracy, and creates `SeablastSetup`. +3. `SeablastSetup` builds a single `SeablastConfiguration` instance by loading configuration closures in precedence order. +4. `SeablastController` applies environment configuration, derives runtime values, starts the session, resolves the route, and enforces authentication/authorization. +5. `SeablastModel` instantiates the mapped application model, calls `knowledge()`, and always injects a CSRF token into the returned parameters. +6. `SeablastView` renders JSON, HTML, or redirect output and then exposes Tracy SQL and HTTP panels. + +When maintaining the framework, preserve this order. Session setup, Tracy wiring, runtime-derived configuration, and route resolution are coupled. + +## Configuration Layering + +`SeablastSetup` currently loads configuration files in this order, from lowest to highest priority: + +1. `conf/default.conf.php` +2. `APP_DIR/vendor/seablast/auth/conf/app.conf.php` +3. `APP_DIR/vendor/seablast/i18n/conf/app.conf.php` +4. `APP_DIR/conf/app.conf.php` +5. `APP_DIR/conf/app.conf.local.php` + +Each file must return a callable that accepts `SeablastConfiguration`. + +This means application config can override: + +- framework defaults +- auth defaults +- i18n defaults +- environment-local overrides + +If you change this order, you are changing a public integration contract. + +## Runtime Values Added by Core + +The controller populates several values at runtime that applications may read later: + +- `SeablastConstant::SB_APP_ROOT_ABSOLUTE_URL` +- `SeablastConstant::SB_GET_ARGUMENT_ID` +- `SeablastConstant::SB_GET_ARGUMENT_CODE` +- `SeablastConstant::ERROR_HTTP_CODE` +- `SeablastConstant::ERROR_MESSAGE` +- `SeablastConstant::USER_ID` +- `SeablastConstant::USER_ROLE_ID` +- `SeablastConstant::USER_GROUPS` +- `SeablastConstant::FLAG_USER_IS_AUTHENTICATED` + +Applications should treat these as framework-owned runtime state, not as values to preseed manually. + +## Route Contract + +Routes live in `SeablastConstant::APP_MAPPING` and are matched by exact path after: + +- removing the application base path from `REQUEST_URI` +- trimming a trailing slash +- defaulting an empty path to `/` + +The framework currently consumes these mapping keys: + +- `model`: fully qualified class name of the model +- `template`: Latte template name without `.latte` +- `roleIds`: comma-separated allow-list of numeric role IDs +- `id`: required GET parameter name, stored as `SB_GET_ARGUMENT_ID` +- `code`: required GET parameter name, validated and stored as `SB_GET_ARGUMENT_CODE` + +Notes: + +- Missing routes are converted to `/error`, not to a generic JSON error route. +- A route with `roleIds` requires a configured identity manager. +- `code` is intentionally restricted and rejects control characters, quotes, backslashes, semicolons, and SQL comment markers. + +If you add new mapping keys in core, document them in `README.md`, this file, and tests. + +## Model Contract for Applications + +Application models are expected to implement the same contract as `SeablastModelInterface`: + +- constructor signature: `__construct(SeablastConfiguration $configuration, Superglobals $superglobals)` +- method: `knowledge(): stdClass` + +`SeablastModel` instantiates the mapped class directly and then calls `knowledge()`. + +Special properties in the returned `stdClass`: + +- `rest`: when present, Seablast renders JSON instead of HTML +- `httpCode`: optional HTTP status code +- `redirectionUrl`: when present, Seablast sends a redirect and renders `redirection.latte` +- `title`: used by `BlueprintWeb.latte` as page title + +The framework also always adds: + +- `csrfToken` + +Important consequences: + +- A model must return `stdClass`, not an array. +- JSON responses can return `rest` as either an object or an array. +- Redirect codes are limited to `301`, `302`, `303`, `307`, and `308`. +- For non-REST HTML responses with `httpCode >= 400`, `SeablastView` forces the `error` template. + +## Generic JSON API Contract + +`GenericRestApiJsonModel` is the base implementation for JSON endpoints. + +Current behavior: + +- requires `REQUEST_METHOD` in `Superglobals->server` +- for real HTTP input, requires JSON `Content-Type` before reading the body +- rejects real or injected JSON input over 1 MiB before decoding +- reads JSON from `php://input`, or from `SeablastConstant::JSON_INPUT` when injected for tests +- accepts only a decoded JSON object +- requires `csrfToken` +- validates the token against the `sb_json` token ID + +Default error behavior: + +- `400` for invalid JSON or wrong payload shape +- `401` for missing or invalid CSRF token +- `413` for JSON request bodies over 1 MiB +- `415` for missing or non-JSON `Content-Type` + +Applications extending this class should call `parent::knowledge()` first and stop when it already returns `httpCode >= 400`. + +## View and Template Contract + +`SeablastView` injects one more template variable before rendering: + +- `configuration` + +Bundled template lookup behavior: + +- first try `../../..//