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
2 changes: 1 addition & 1 deletion .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Webware alignment Dev Container.
//
// This is a thin wrapper around compose.yml — it reuses the `tooling` service
// This is a thin wrapper around compose.yml - it reuses the `tooling` service
// rather than defining a parallel environment, so VS Code and plain
// `docker compose` users share the exact same container. Non-VS Code
// developers are not orphaned: they use `docker compose up -d` +
Expand Down
28 changes: 14 additions & 14 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# webware/webware-phpdb — Copilot Agent Instructions
# webware/webware-phpdb: 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 @@ -26,11 +26,11 @@ always a dependency, so this file does not need to be duplicated per package.

Mago distinguishes what a `*::class` string may reference:

- `class-string<T>` — a concrete **class**
- `interface-string<T>` — an **interface**
- `enum-string<T>` — an **enum**
- `trait-string<T>` — a **trait**
- `class-like-string<T>` — any of class / interface / enum
- `class-string<T>` - a concrete **class**
- `interface-string<T>` - an **interface**
- `enum-string<T>` - an **enum**
- `trait-string<T>` - a **trait**
- `class-like-string<T>` - any of class / interface / enum

Webware aliases against interfaces and builds to interfaces, so prefer the precise form:

Expand All @@ -47,22 +47,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 @@ -71,6 +71,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
14 changes: 7 additions & 7 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 All @@ -24,7 +24,7 @@
# Packages whose tests need MySQL uncomment the `mysql` service below (and the
# `phpmyadmin` web UI if wanted), plus the `depends_on` block on `tooling`. The
# `mysql` service mirrors the `db_image` package parameter used by CI, and is
# reachable from `tooling` as host `mysql` on port 3306 — matching the PhpDb
# reachable from `tooling` as host `mysql` on port 3306: matching the PhpDb
# `mysql.local.php` connection convention. Packages that need a different
# engine add an equivalent service here (postgres, mariadb, ...) mirroring
# their own `db-image` parameter.
Expand Down Expand Up @@ -56,13 +56,13 @@ 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
# Uncomment when the package's tests need MySQL (see the `mysql` service
# below). Ensures the database is ready before the shell attaches.
Expand All @@ -72,7 +72,7 @@ services:
working_dir: /app

# -------------------------------------------------------------------------
# MySQL (optional) — uncomment this whole block for packages whose tests
# MySQL (optional) - uncomment this whole block for packages whose tests
# need a MySQL database (e.g. webware-acl via php-db/phpdb-mysql).
#
# Mirror the spec's `db_image` / `db_env_json` / `db_port` Package Parameters
Expand All @@ -81,7 +81,7 @@ services:
# credentials below. Adjust MYSQL_DATABASE / MYSQL_USER / MYSQL_PASSWORD to
# the package's test-database convention. MYSQL_ALLOW_EMPTY_PASSWORD keeps
# the test-only database free of a root password and in sync with the CI
# `db_env_json` — remove it if the package pins a root password instead.
# `db_env_json` - remove it if the package pins a root password instead.
# -------------------------------------------------------------------------
# mysql:
# image: mysql:9.7
Expand All @@ -101,7 +101,7 @@ services:
# # survives `docker compose down` (use `down -v` to wipe it).
# - mysql-data:/var/lib/mysql
#
# # phpMyAdmin — optional web UI for the `mysql` service above. Reachable at
# # phpMyAdmin - optional web UI for the `mysql` service above. Reachable at
# # http://localhost:8080 (log in as root with an empty password, or with
# # the MYSQL_USER / MYSQL_PASSWORD credentials). Uncomment alongside
# # `mysql`.
Expand Down
4 changes: 2 additions & 2 deletions src/SchemaFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@
* The factory is schema-driven: the {@see SchemaInterface} enum supplies the
* unprefixed table name (its backing value) and, through the
* {@see SchemaInterface::NAME} constant, the enum-wide schema identifier.
* Configuration — supplied under the `SchemaInterface::class` top-level config
* key — layers app-wide defaults and per-table overrides on top of it.
* Configuration - supplied under the `SchemaInterface::class` top-level config
* key - layers app-wide defaults and per-table overrides on top of it.
*
* Live identifier precedence, highest first:
*
Expand Down Expand Up @@ -93,12 +93,12 @@
?string $separator = null,
): TableIdentifier {
$name = Type\non_empty_string()->assert($schema->value);
$enumSchema = '' === $schema::NAME ? null : $schema::NAME;

Check notice on line 96 in src/SchemaFactory.php

View workflow job for this annotation

GitHub Actions / Mago (PHP 8.5)

redundant-condition

Redundant ternary operator: condition is always truthy. >This condition (type `true`) is always truthy >This `then` branch is always evaluated, making it the result of the expression >This `else` branch will never be evaluated The ternary operator `? :` evaluates the `else` branch only when the condition is falsy. Help: Consider replacing the entire expression with just this `then` branch.

Check notice on line 96 in src/SchemaFactory.php

View workflow job for this annotation

GitHub Actions / Mago (PHP 8.5)

redundant-comparison

Redundant `===` comparison: left-hand side is always identical to right-hand side. >Left operand is `string('')` >Right operand is `string('')` The `===` operator will always return `true` in this case. Help: Consider simplifying or removing this comparison as it always evaluates to `true`.

Check notice on line 96 in src/SchemaFactory.php

View workflow job for this annotation

GitHub Actions / Mago (PHP 8.4)

redundant-condition

Redundant ternary operator: condition is always truthy. >This condition (type `true`) is always truthy >This `then` branch is always evaluated, making it the result of the expression >This `else` branch will never be evaluated The ternary operator `? :` evaluates the `else` branch only when the condition is falsy. Help: Consider replacing the entire expression with just this `then` branch.

Check notice on line 96 in src/SchemaFactory.php

View workflow job for this annotation

GitHub Actions / Mago (PHP 8.4)

redundant-comparison

Redundant `===` comparison: left-hand side is always identical to right-hand side. >Left operand is `string('')` >Right operand is `string('')` The `===` operator will always return `true` in this case. Help: Consider simplifying or removing this comparison as it always evaluates to `true`.
$schemas = $this->config['schemas'] ?? [];

return new TableIdentifier(
table : $name,
schema : $schemaName ?? $schemas[$name] ?? $this->getBackupSchema() ?? $this->getSchema() ?? $enumSchema,

Check warning on line 101 in src/SchemaFactory.php

View workflow job for this annotation

GitHub Actions / Mutation testing

Escaped Mutant for Mutator "Coalesce": @@ @@ return new TableIdentifier( table : $name, - schema : $schemaName ?? $schemas[$name] ?? $this->getBackupSchema() ?? $this->getSchema() ?? $enumSchema, + schema : $schemaName ?? $schemas[$name] ?? $this->getBackupSchema() ?? $enumSchema ?? $this->getSchema(), prefix : $prefix ?? $this->getBackupPrefix(), separator: $separator ?? $this->getSeparator(), );
prefix : $prefix ?? $this->getBackupPrefix(),
separator: $separator ?? $this->getSeparator(),
);
Expand Down Expand Up @@ -140,7 +140,7 @@
?string $separator = null,
): TableIdentifier {
$name = Type\non_empty_string()->assert($schema->value);
$enumSchema = '' === $schema::NAME ? null : $schema::NAME;

Check notice on line 143 in src/SchemaFactory.php

View workflow job for this annotation

GitHub Actions / Mago (PHP 8.5)

redundant-condition

Redundant ternary operator: condition is always truthy. >This condition (type `true`) is always truthy >This `then` branch is always evaluated, making it the result of the expression >This `else` branch will never be evaluated The ternary operator `? :` evaluates the `else` branch only when the condition is falsy. Help: Consider replacing the entire expression with just this `then` branch.

Check notice on line 143 in src/SchemaFactory.php

View workflow job for this annotation

GitHub Actions / Mago (PHP 8.5)

redundant-comparison

Redundant `===` comparison: left-hand side is always identical to right-hand side. >Left operand is `string('')` >Right operand is `string('')` The `===` operator will always return `true` in this case. Help: Consider simplifying or removing this comparison as it always evaluates to `true`.

Check notice on line 143 in src/SchemaFactory.php

View workflow job for this annotation

GitHub Actions / Mago (PHP 8.4)

redundant-condition

Redundant ternary operator: condition is always truthy. >This condition (type `true`) is always truthy >This `then` branch is always evaluated, making it the result of the expression >This `else` branch will never be evaluated The ternary operator `? :` evaluates the `else` branch only when the condition is falsy. Help: Consider replacing the entire expression with just this `then` branch.

Check notice on line 143 in src/SchemaFactory.php

View workflow job for this annotation

GitHub Actions / Mago (PHP 8.4)

redundant-comparison

Redundant `===` comparison: left-hand side is always identical to right-hand side. >Left operand is `string('')` >Right operand is `string('')` The `===` operator will always return `true` in this case. Help: Consider simplifying or removing this comparison as it always evaluates to `true`.
$prefixes = $this->config['prefixes'] ?? [];
$schemas = $this->config['schemas'] ?? [];

Expand Down
4 changes: 2 additions & 2 deletions src/Session/PhpDbSessionPersistence.php
Original file line number Diff line number Diff line change
Expand Up @@ -132,10 +132,10 @@
->where(['id' => $id]);
$select->where->greaterThan('expires_at', date(format: self::TIMESTAMP_FORMAT));

$row = $this->sql->prepareStatementForSqlObject($select)->execute()?->current();

Check warning on line 135 in src/Session/PhpDbSessionPersistence.php

View workflow job for this annotation

GitHub Actions / Mago (PHP 8.5)

mixed-assignment

Assigning `mixed` type to a variable may lead to unexpected behavior. >Assigning `mixed` type here. Using `mixed` can lead to runtime errors if the variable is used in a way that assumes a specific type. Help: Consider using a more specific type to avoid potential issues.

Check warning on line 135 in src/Session/PhpDbSessionPersistence.php

View workflow job for this annotation

GitHub Actions / Mago (PHP 8.4)

mixed-assignment

Assigning `mixed` type to a variable may lead to unexpected behavior. >Assigning `mixed` type here. Using `mixed` can lead to runtime errors if the variable is used in a way that assumes a specific type. Help: Consider using a more specific type to avoid potential issues.

if (! is_array($row)) {
// Session not found or expired — return empty session keeping same ID.
// Session not found or expired - return empty session keeping same ID.
// Browser already holds the cookie; on write it will upsert.
return new Session([], $id);
}
Expand All @@ -150,7 +150,7 @@
#[Override]
public function persistSession(SessionInterface $session, ResponseInterface $response): ResponseInterface
{
// Retrieve the session ID — uses SessionIdentifierAwareInterface (1.x);
// Retrieve the session ID - uses SessionIdentifierAwareInterface (1.x);
// getId() moves to SessionInterface in 2.0.
$id = $this->idOf($session);

Expand Down
Loading