Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

7.4KB

Agent notes for this repo

Kept in sync with CLAUDE.md (same content, duplicated on purpose so both Claude Code and other agent tooling pick it up automatically — update both files together).

Operational gotchas discovered while building the warehouse app on top of the hand-rolled framework in framework/. Read this before re-deriving any of it from scratch — most of it was expensive to find the first time. See docs/framework.md for how the framework itself works (routing, models, views, etc.) — this file is about the environment and bugs, not the framework's intended API.

No local PHP/composer — everything runs through Docker

There is no PHP or Composer on the host. Use:

  • docker compose run --rm app <cmd> for one-off commands (migrations, tests, scratch scripts)
  • docker compose exec app <cmd> against the running stack (docker compose up -d)

.env is required and gitignored — copy it from .env.example before first run.

composer install needs --ignore-platform-reqs. The committed composer.lock predates the container's PHP 8.2 image: phpspec/prophecy requires PHP <8.1, and ext-zip (needed by dbrekelmans/bdi/php-webdriver) isn't installed. This is a pre-existing gap, not something to “fix” by upgrading deps unless asked.

Ports are a Windows-specific minefield

Windows reserves TCP ports dynamically for Hyper-V/WSL2 NAT. A reserved port fails to bind with ports are not available: ... forbidden by its access permissions — this looks like “something else is using it” but isn't; it's an OS-level exclusion, not a process conflict. Check before picking a port:

netsh interface ipv4 show excludedportrange protocol=tcp

8081, 9090, and others have been seen reserved on this machine already. Current mapping in docker-compose.yml: app → host 9092 (container 80), phpLiteAdmin → host 9091 (container 8081). The container-side ports must not change (80 = nginx, 8081 = phpliteadmin's nginx vhost) — only remap the host side, and check the exclusion list first.

docker/start-container.sh must stay LF

A Windows checkout previously gave it CRLF line endings, which breaks its shebang inside the Linux container (start-container: not found in docker logs pro-php-mvc-app, crash-looping). .gitattributes now forces *.sh to LF on checkout. If this error reappears, run file docker/start-container.sh to confirm, then sed -i 's/\r$//' docker/start-container.sh.

Stale root-owned cache files break the real app

docker compose run (one-off containers) executes as root. The real app service's php-fpm workers run as www-data. If you compile views or write files via docker compose run and then the running app can't overwrite them (Permission denied in AdvancedEngine::render(), storage/framework/views/), it's stale root-owned .php cache files. Fix: delete them (gitignored, regenerable) and let www-data recreate them:

docker exec pro-php-mvc-app find /var/www/html/storage/framework/views -maxdepth 1 -name '*.php' -delete

Testing gotchas

phpunit.xml registers ServerExtension, which always tries to auto-start php command.php serve before the suite (even for non-browser tests) and stop it after. This container has no pcntl extension, so both the start (ServeCommand::handleSignals()pcntl_async_signals()) and the stop (SIGTERM constant) crash with a fatal error after the tests themselves have already run — the crash happens in teardown, so a phpunit run always exits non-zero even when everything passed.

Workaround: bind a dummy TCP listener on the target APP_HOST:APP_PORT (check docker exec <app> php -r "echo getenv('APP_HOST').':'.getenv('APP_PORT');" — currently 0.0.0.0:80) before running phpunit, so ServerExtension::serverIsRunning() sees it as already running and skips starting/stopping the real (broken) one:

docker compose run --rm app sh -c "php -r 'stream_socket_server(\"tcp://0.0.0.0:80\"); sleep(120);' & sleep 1 && vendor/bin/phpunit; kill %1 2>/dev/null; true"

BrowserTest.php (Symfony Panther/Firefox) will still error — there's no geckodriver/Firefox in this image. That's a pre-existing gap, not a regression to chase.

php command.php serve is itself broken in this container for the same pcntl reason. Don't use it to “prove the app works” — hit a real running docker compose up stack over HTTP instead (curl, or a browser).

Framework bugs found (work around at the app level — don't patch framework/)

  1. Router doesn't disambiguate by HTTP method for dynamic paths, and matches unanchored. Framework\Routing\Route::matches() — once past the literal exact-match check, the regex built for a {param} route is checked with preg_match_all with no ^/$ anchors, and without checking $method at all. Consequences:

    • A shorter dynamic route (/items/view/{item}) matches as a prefix of any longer path (/items/view/{item}/receive) — whichever route is registered first wins, regardless of specificity or method.
    • Two routes on the identical dynamic path with different methods (GET show-form / POST submit) collide the same way — first-registered wins for both methods, so the POST silently runs the GET handler.

    Rule of thumb: every dynamic ({param}) route in app/routes.php needs a fully unique literal path. Never nest one dynamic route's path under another's, and never pair GET+POST on the same dynamic path. Static (no {}) paths are unaffected — they only ever use the exact-match branch. See the stock-action routes in app/routes.php (and the comment above them) for the working pattern: /items/receive-form/{item} (GET) vs /items/receive/{item} (POST), etc.

  2. PHP's empty("0") is true. The framework's required rule (RequiredRule::validate) is !empty($data[$field]), so a legitimate "0" value fails required. Don't use required on a field where 0 is valid (e.g. “set stock to zero”) — validate presence manually instead. See AdjustStockController::handle() for the pattern.

  3. No ORDER BY / aggregate support in Framework\Database\QueryBuilder. Sort/sum in PHP after ->all().

  4. No unique constraints in the migration DSL. Uniqueness (e.g. items.sku, locations.code) is not enforced anywhere — enforce at the app level if it starts to matter.

Email

Two drivers: postmark (API-based) and smtp (generic Swift_SmtpTransport — no new dependency, ships with swiftmailer/swiftmailer already). Switch via EMAIL_DRIVER in .env; config/email.php defaults to smtp.

Emails are always queued (app('queue')->push(...)), never sent synchronously. Nothing processes the queue automatically — run docker compose exec app php command.php queue:work (blocks, polls every second) or jobs just sit in the jobs table forever.

Domain model

  • locations, items, stock (one row per item+location, found-or-created via Stock::forItemAtLocation()), stock_movements (audit ledger: receive/withdraw/adjustment).
  • Auth gating is inline per-controller (if (!session()->has('user_id')) { return redirect(...); }) — the framework has no middleware concept; don't try to add one.
  • Low-stock alerts fire from WithdrawStockController/AdjustStockController after a stock change, when item.reorder_level > 0 && total <= reorder_level. Not de-duplicated — repeated withdrawals under threshold requeue repeated alerts.

Powered by TurnKey Linux.