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
18 changes: 9 additions & 9 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# Webware Log — Copilot Agent Instructions
# Webware Log: Copilot Agent Instructions

## Read this before acting — fleet working agreements
## Read this before acting: fleet working agreements

`vendor/webware/webware-tools/agent-working-agreements.md` is authoritative for how work is done
in this fleet, and it overrides anything else in this file or in any local note. Read it before
acting, and again before reporting. The non-negotiables:

- Do exactly the scope asked — only the files, branches and repositories named. No adjacent edits,
- Do exactly the scope asked - only the files, branches and repositories named. No adjacent edits,
no cleanup, no onboarding, no surveys.
- Anything else you notice gets one line at most, with no proposed fix.
- Never close a reply with a condensed restatement of it, and never present an inference as a
Expand All @@ -21,22 +21,22 @@ PHPUnit 13 enforces a strict separation between mocks and stubs. Violating these

- **Use `createStub()`** when the test double only needs to return values (`method()->willReturn()`, `method()->willReturnCallback()`). No expectations are configured.
- **Use `createMock()`** only when the test verifies behavior with `expects()` (e.g. `expects($this->once())`, `expects($this->never())`).
- **Never** call `createMock()` without also calling `expects()` on at least one method — PHPUnit 13 will issue a notice.
- **Never** call `createMock()` without also calling `expects()` on at least one method - PHPUnit 13 will issue a notice.
- **Remove** `/** @var ClassName&MockObject */` annotations and `MockObject` intersection types on variables created with `createStub()`.
- **Remove** the `use PHPUnit\Framework\MockObject\MockObject;` import from any file where no `createMock()` + `expects()` usage remains.

### Examples

```php
// CORRECT — stub returns value, no expectations
// CORRECT - stub returns value, no expectations
$container = $this->createStub(ContainerInterface::class);
$container->method('get')->willReturnCallback(static fn(string $id): mixed => ...);

// CORRECT — mock verifies behavior with expects()
// CORRECT - mock verifies behavior with expects()
$handler = $this->createMock(ErrorHandler::class);
$handler->expects($this->once())->method('attachListener');

// WRONG — createMock() with no expects() triggers PHPUnit notice
// WRONG - createMock() with no expects() triggers PHPUnit notice
$logger = $this->createMock(Logger::class);
$logger->method('withName')->willReturn($logger); // should be createStub()
```
Expand All @@ -45,6 +45,6 @@ $logger->method('withName')->willReturn($logger); // should be createStub()

`phpunit.xml.dist` has `requireCoverageMetadata="true"`. Every test class **must** have:

1. `#[CoversClass(ClassName::class)]` — one per source class under test.
2. `#[CoversMethod(ClassName::class, 'methodName')]` — one per public/protected method exercised.
1. `#[CoversClass(ClassName::class)]` - one per source class under test.
2. `#[CoversMethod(ClassName::class, 'methodName')]` - one per public/protected method exercised.
3. `use PHPUnit\Framework\Attributes\CoversClass;` and `use PHPUnit\Framework\Attributes\CoversMethod;` imports.
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
#
# Builds a self-contained, interactive PHP development environment carrying
# the Webware toolchain: PHP CLI, Composer, Mago, and Xdebug. Developers work
# inside this container — either through the VS Code Dev Container (backed by
# inside this container: either through the VS Code Dev Container (backed by
# compose.yml) or by `docker compose up -d` + `docker compose exec tooling`.
# The host machine (Windows, WSL, Linux, or macOS) never needs a native PHP
# toolchain.
Expand Down
10 changes: 5 additions & 5 deletions compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
# This compose file is the single source of truth for the environment. Two
# ways to use it:
#
# 1. VS Code Dev Container — "Reopen in Container" uses
# 1. VS Code Dev Container: "Reopen in Container" uses
# .devcontainer/devcontainer.json, which is *backed by* this file (it
# references the `tooling` service rather than defining its own).
#
Expand Down Expand Up @@ -55,21 +55,21 @@ services:
- "host.docker.internal:host-gateway"
volumes:
# Bind-mount the source so edits are visible on both the host and the
# container. vendor/ is NOT overridden — it is the host's own copy.
# container. vendor/ is NOT overridden - it is the host's own copy.
# Composer/installs inside the container write straight into the
# developer's working tree on disk.
- .:/app
# The Composer cache lives in a named volume (Linux-native I/O, fast on
# Windows) so downloads survive container rebuilds. It is only a cache
# — the developer's real artifacts all live on the host.
# - the developer's real artifacts all live on the host.
- composer-cache:/tmp/composer-cache
depends_on:
mysql:
condition: service_healthy
working_dir: /app

# -------------------------------------------------------------------------
# MySQL — required by the PhpDbHandler integration tests. Credentials mirror
# MySQL - required by the PhpDbHandler integration tests. Credentials mirror
# the CI `db-env-json` (root, empty password, `webware_log_test` database).
# The test schema and data are created by the PHPUnit ListenerExtension
# (MysqlFixtureLoader) at test time, not by this service.
Expand All @@ -91,7 +91,7 @@ services:
- mysql-data:/var/lib/mysql

# -------------------------------------------------------------------------
# phpMyAdmin — web UI for the `mysql` service above. Reachable at
# phpMyAdmin - web UI for the `mysql` service above. Reachable at
# http://localhost:8080 (auto-login as root). Note: root uses an empty
# password, matching the test/CI convention.
# -------------------------------------------------------------------------
Expand Down
54 changes: 27 additions & 27 deletions docs/Project_Architecture_Blueprint.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Project Architecture Blueprint — `webware/webware-log`
# Project Architecture Blueprint: `webware/webware-log`

> **Current stable:** 0.0.x (branch: `add-phpdb-handler`)
> **Next target:** 0.1.0 — see [Roadmap](#14-roadmap--version-scope) for planned changes
> **Next target:** 0.1.0 - see [Roadmap](#14-roadmap--version-scope) for planned changes
> **License:** BSD-3-Clause
> **PHP requirement:** ~8.4.0 || ~8.5.0 || ~8.6.0

Expand Down Expand Up @@ -36,7 +36,7 @@
- Provides Monolog handlers that persist log records to a relational database using either `laminas-db` or `php-db/phpdb`.
- Decorates Mezzio's built-in `ErrorHandler` so that uncaught exceptions are automatically logged.

> **Note — Laminas MVC:** The Laminas team is retiring the MVC framework. All MVC-specific integration (`Runtime::Mvc`, `AbstractController` listener identifiers) was removed in 0.1.0, and the Laminas EventManager bridge listener has since been removed entirely — PSR-14 is the only supported dispatch path.
> **Note - Laminas MVC:** The Laminas team is retiring the MVC framework. All MVC-specific integration (`Runtime::Mvc`, `AbstractController` listener identifiers) was removed in 0.1.0, and the Laminas EventManager bridge listener has since been removed entirely - PSR-14 is the only supported dispatch path.

### Guiding Principles

Expand Down Expand Up @@ -71,7 +71,7 @@

## 2. Architecture Visualization

### C4 — Context Diagram
### C4: Context Diagram

```
┌──────────────────────────────────────────────────────────────────────┐
Expand All @@ -90,7 +90,7 @@
└──────────────────────────────────────────────────────────────────────┘
```

### C4 — Component Diagram
### C4: Component Diagram

```
┌─────────────────────────────────────────────────────────────────────────────┐
Expand Down Expand Up @@ -132,7 +132,7 @@
└─────────────────────────────────────────────────────────────────────────────┘
```

### Data Flow — Request Log Entry
### Data Flow: Request Log Entry

```
HTTP Request
Expand All @@ -156,7 +156,7 @@ LaminasDbHandler | PhpDbHandler .write(LogRecord)
MySQL log table
```

### Data Flow — PSR-14 Log Event
### Data Flow: PSR-14 Log Event

```
Handler / Middleware / Service
Expand All @@ -172,7 +172,7 @@ Psr3LogPsr14Listener.__invoke(LogEvent)
Monolog\Logger.log() → handler chain → DB
```

### Data Flow — Uncaught Exception
### Data Flow: Uncaught Exception

```
Mezzio ErrorHandler.process()
Expand Down Expand Up @@ -233,7 +233,7 @@ The root wiring class for the Laminas component installer. When invoked, it retu
| `LogFactory` | Factory | `Monolog\Logger` as `LoggerInterface` | Pushes handlers and processors in order |
| `MezzioErrorHandlerDelegator` | Delegator | `Laminas\Stratigility\Middleware\ErrorHandler` | Conditionally attaches `MezzioErrorListener` when `log_errors = true` |

The delegator pattern means the error handler is extended non-destructively — if `log_errors` is `false`, the original handler is returned unmodified.
The delegator pattern means the error handler is extended non-destructively - if `log_errors` is `false`, the original handler is returned unmodified.

---

Expand Down Expand Up @@ -367,7 +367,7 @@ CREATE TABLE `log` (

### UUID Strategy

UUID v7 (time-ordered, monotonic) is used — generated per record using `ramsey/uuid` with the record's own `datetime`. This allows chronological sorting on the `uuid` column while maintaining global uniqueness.
UUID v7 (time-ordered, monotonic) is used - generated per record using `ramsey/uuid` with the record's own `datetime`. This allows chronological sorting on the `uuid` column while maintaining global uniqueness.

---

Expand Down Expand Up @@ -408,7 +408,7 @@ The component itself does not log its own operations. Internal errors propagate

- `ConfigProvider` is the entry point for `laminas-component-installer`.
- The `extra.laminas` key in `composer.json` identifies the component and its config-provider FQCN.
- The component registers itself as a **library** (not an application) — it ships defaults that applications can override.
- The component registers itself as a **library** (not an application) - it ships defaults that applications can override.

### 7.2 Mezzio Middleware Pipeline

Expand All @@ -435,9 +435,9 @@ The dispatcher calls `Psr3LogPsr14Listener::__invoke(LogEvent)`, which translate
Handler and processor registration order in `LogFactory`:
```
LaminasDbHandler | PhpDbHandler ← handler (persists to DB)
[RamseyUuidProcessor] ← processor #1 (UUID v7) — guarded by process_uuid flag (0.1.0+)
[RamseyUuidProcessor] ← processor #1 (UUID v7) - guarded by process_uuid flag (0.1.0+)
PsrLogMessageProcessor ← processor #2 (interpolate placeholders)
[LaminasI18nProcessor] ← processor #3 (translate, optional) — guarded by process_translation flag
[LaminasI18nProcessor] ← processor #3 (translate, optional) - guarded by process_translation flag
```
Monolog processes records in **LIFO** order for processors and passes through handler stack in registration order.

Expand Down Expand Up @@ -479,7 +479,7 @@ The `PhpDbHandler` is the current development focus (branch `add-phpdb-handler`)
### 8.2 Adding a New Processor

1. Create `src/Processor/MyProcessor.php` implementing `Monolog\Processor\ProcessorInterface`.
2. `__invoke(LogRecord $record): LogRecord` — enrich `$record->extra` and return `$record->with(extra: ...)`.
2. `__invoke(LogRecord $record): LogRecord` - enrich `$record->extra` and return `$record->with(extra: ...)`.
3. If container dependencies are needed, create `src/Processor/MyProcessorFactory.php`.
4. Register as `invokables` (no deps) or `factories` (with deps) in `ConfigProvider`.
5. Add `$logger->pushProcessor(...)` in `LogFactory`.
Expand Down Expand Up @@ -522,7 +522,7 @@ The `EventDispatcherInterface` is resolved from the container (provided by `phly
```
test/
├── unit/
│ └── TestAsset/ (shared test doubles — currently empty)
│ └── TestAsset/ (shared test doubles - currently empty)
└── integration/
├── Extension/
│ ├── ListenerExtension.php (PHPUnit bootstrap extension)
Expand Down Expand Up @@ -589,7 +589,7 @@ The `mysql.sql` fixture is auto-executed by MySQL's `docker-entrypoint-initdb.d`

### Runtime Dependency: DB Adapter

This component requires a `Laminas\Db\Adapter\AdapterInterface` (for `LaminasDbHandler`) or `PhpDb\Adapter\AdapterInterface` (for `PhpDbHandler`) to be registered in the application container. These are not shipped with this component — the host application is responsible for configuring the DB adapter.
This component requires a `Laminas\Db\Adapter\AdapterInterface` (for `LaminasDbHandler`) or `PhpDb\Adapter\AdapterInterface` (for `PhpDbHandler`) to be registered in the application container. These are not shipped with this component - the host application is responsible for configuring the DB adapter.

---

Expand All @@ -599,15 +599,15 @@ This component requires a `Laminas\Db\Adapter\AdapterInterface` (for `LaminasDbH

1. Push `PhpDbHandler` onto the logger in `LogFactory` (optionally remove `LaminasDbHandler`).
2. Update the DB schema: rename `userIdentifier` → `user_identifier` and add a `context JSON` column.
3. Ensure the host application registers `PhpDb\Adapter\AdapterInterface::class` in the container via `php-db/phpdb`'s own config provider — **not** using laminas-db configuration structure; the two config structures are incompatible.
3. Ensure the host application registers `PhpDb\Adapter\AdapterInterface::class` in the container via `php-db/phpdb`'s own config provider - **not** using laminas-db configuration structure; the two config structures are incompatible.
4. Both adapters can coexist in the container under separate FQCN service IDs.

### 11.2 Adding a Non-DB Handler

1. Registering it as a service in the DI container.
2. Pushing it onto the logger in `LogFactory`.

The handler stack in Monolog is ordered — add higher-priority handlers last (they are tried first).
The handler stack in Monolog is ordered - add higher-priority handlers last (they are tried first).

### 11.4 Scoping Processors per Channel

Expand Down Expand Up @@ -685,7 +685,7 @@ test (phpunit unit suite)
| Branch | Purpose |
|---|---|
| `0.0.x` | Default / stable release branch |
| `add-phpdb-handler` | Active development — adds `PhpDbHandler` (merges to 0.0.x) |
| `add-phpdb-handler` | Active development - adds `PhpDbHandler` (merges to 0.0.x) |
| `0.1.x` _(planned)_ | Target for all 0.1.0 refactoring work |

### Known Technical Debt (0.0.x)
Expand All @@ -695,13 +695,13 @@ Items marked ✅ are addressed by the [0.1.0 refactoring plan](../plan/refactor-
| Location | Issue | 0.1.0 |
|---|---|---|
| `LaminasDbHandler` | `Sql` re-created per `write()` call | ✅ TASK-025 |
| `LaminasDbHandler` | Column named `userIdentifier` (camelCase) — inconsistent with `PhpDbHandler` | ✅ TASK-026 |
| `LaminasDbHandler` | Column named `userIdentifier` (camelCase) - inconsistent with `PhpDbHandler` | ✅ TASK-026 |
| `MonologMiddleware` | Hard-coded `UserInterface` attribute key | ✅ TASK-027 |
| `LogEvent::getChannel()` | Instantiates `new ConfigProvider()` at runtime | ✅ TASK-007 |
| `LogFactory` | `process_uuid` / `process_translation` flags not honoured | ✅ TASK-028 |
| `phpunit.xml.dist` | Schema targets PHPUnit 11.4 but requirement is ^13.0 | ✅ TASK-029 |
| `phpcs.xml` | Legacy PHP_CodeSniffer config — tool is not used; style enforced by `php-cs-fixer` | ✅ TASK-031 |
| `psalm.xml.dist` / `psalm-baseline.xml` | Psalm config files — `vimeo/psalm` replaced by PHPStan | ✅ TASK-032 |
| `phpcs.xml` | Legacy PHP_CodeSniffer config - tool is not used; style enforced by `php-cs-fixer` | ✅ TASK-031 |
| `psalm.xml.dist` / `psalm-baseline.xml` | Psalm config files - `vimeo/psalm` replaced by PHPStan | ✅ TASK-032 |
| `composer.json` scripts | `cs-check`/`cs-fix` pointed to phpcs/phpcbf; `static-analysis` pointed to `psalm` | ✅ TASK-033 |
| `ConfigProvider` | Config key `ConfigProvider::class` should reflect what it configures | ✅ TASK-001 |
| `Runtime::Mvc` | MVC case defined but never wired; Laminas MVC being retired | ✅ TASK-009 |
Expand All @@ -713,11 +713,11 @@ Items marked ✅ are addressed by the [0.1.0 refactoring plan](../plan/refactor-
### 0.0.x (Current)

- Core PSR-3 / Monolog integration for Mezzio
- `LaminasDbHandler` — persist log records via `laminas-db`
- `PhpDbHandler` — persist log records via `php-db/phpdb` _(branch: `add-phpdb-handler`)_
- `MezzioErrorHandlerDelegator` — automatic error logging
- `MonologMiddleware` — user identity enrichment
- `Psr3LogLaminasListener` — Laminas EventManager → PSR-3 bridge
- `LaminasDbHandler` - persist log records via `laminas-db`
- `PhpDbHandler` - persist log records via `php-db/phpdb` _(branch: `add-phpdb-handler`)_
- `MezzioErrorHandlerDelegator` - automatic error logging
- `MonologMiddleware` - user identity enrichment
- `Psr3LogLaminasListener` - Laminas EventManager → PSR-3 bridge

### 0.1.0 (Planned)

Expand Down
2 changes: 1 addition & 1 deletion docs/error-logging.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,4 @@ The error is logged at `ERROR` level with the exception, request, and response a

- The `ErrorHandler` continues to generate its own error response as normal.
- No exception is swallowed.
- The delegator pattern means the `ErrorHandler` service ID in the container still resolves to the original `ErrorHandler` instance — only a listener is attached to it.
- The delegator pattern means the `ErrorHandler` service ID in the container still resolves to the original `ErrorHandler` instance - only a listener is attached to it.
2 changes: 1 addition & 1 deletion docs/handlers.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ Uses `laminas/laminas-db` to write records.
By default the handler reads `extra.email` for the user column. Override via config:

```php
// Not yet exposed as a top-level config key — override by extending
// Not yet exposed as a top-level config key - override by extending
// LaminasDbHandlerFactory and passing a different $extraAuthIdentifier
// to the LaminasDbHandler constructor.
```
Expand Down
2 changes: 1 addition & 1 deletion docs/middleware.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use Webware\Log\Http\Middleware\MonologMiddleware;
$app->pipe(MonologMiddleware::class);
```

**Config-driven pipeline** — uncomment the `middleware_pipeline` section in `ConfigProvider`:
**Config-driven pipeline**: uncomment the `middleware_pipeline` section in `ConfigProvider`:

```php
// src/ConfigProvider.php (or your own config file)
Expand Down
Loading
Loading