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
62 changes: 50 additions & 12 deletions .claude/rules/php-library-modeling.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,39 @@ Breaking changes inside `Internal/` are not semver-breaking for the library.
1. Every class, property, method, and exception name reflects the **concept** the library represents. A math library
uses `Precision`, `RoundingMode`; a money library uses `Currency`, `Amount`; a collection library uses
`Collectible`, `Order`.
2. Never use generic technical names as class suffixes, prefixes, or method names: `Manager`, `Helper`, `Processor`,
`Handler`, `Service`, `Data`, `Info`, `Utils`, `Item`, `Record`, `Entity`, `Exception`, `process`, `handle`,
`execute`, `mark`, `enforce`, `manage`, `ensure`, `validate`, `check`, `verify`, `assert`, `transform`, `parse`,
`compute`, `sanitize`, or `normalize`.
3. Name classes after what they represent: `Money`, `Color`, `Pipeline` — not after what they do technically.
4. Name methods after the operation in its vocabulary: `add()`, `convertTo()`, `splitAt()`.
2. Name classes after what they represent: `Money`, `Color`, `Pipeline` — not after what they do technically.
3. Name methods after the operation in the library's vocabulary: `add()`, `convertTo()`, `splitAt()`.

### Always banned

These names carry zero semantic content. Never use them anywhere, as class suffixes, prefixes, or method names:

- `Data`, `Info`, `Utils`, `Item`, `Record`, `Entity`.
- `Exception` as a class suffix (e.g., `FooException` — use `Foo` when it already extends a native exception).

### Anemic verbs (banned by default)

These verbs hide what is actually happening behind a generic action. Banned unless the verb **is** the operation
that constitutes the library's reason to exist (e.g., a JSON parser may have `parse()`; a hashing library may
have `compute()`):

- `ensure`, `validate`, `check`, `verify`, `assert`, `mark`, `enforce`, `sanitize`, `normalize`, `compute`,
`transform`, `parse`.

When in doubt, prefer the domain operation name. `Password::hash()` beats `Password::compute()`; `Email::parse()`
is fine in a parser library but suspicious elsewhere (use `Email::from()` instead).

### Architectural roles (allowed with justification)

These names describe a role the library offers as a building block. Acceptable when the class **is** that role
(e.g., `EventHandler` in an events library, `CacheManager` in a cache library, `Upcaster` in an event-sourcing
library). Not acceptable on domain objects inside the library (value objects, enums, contract interfaces):

- `Manager`, `Handler`, `Processor`, `Service`, and their verb forms `process`, `handle`, `execute`.

The test: if the consumer instantiates or extends this class to integrate with the library, the role name is
legitimate. If the class models a concept the consumer manipulates (a money amount, a country code, a color),
the role name is wrong.

## Value objects

Expand All @@ -60,20 +87,23 @@ Breaking changes inside `Internal/` are not semver-breaking for the library.

1. Every failure throws a **dedicated exception class** named after the invariant it guards — never
`throw new DomainException('...')`, `throw new InvalidArgumentException('...')`,
`throw new RuntimeException('...')`, or any other generic native exception with a string message. If the
invariant is worth throwing for, it is worth a named class.
`throw new RuntimeException('...')`, or any other generic native exception thrown directly. If the invariant
is worth throwing for, it is worth a named class.
2. Dedicated exception classes **extend** the appropriate native PHP exception (`DomainException`,
`InvalidArgumentException`, `OverflowException`, etc.) — the native class is the parent, never the thing that
is thrown. Consumers that catch the broad standard types continue to work; consumers that need precise handling
can catch the specific classes.
3. Exceptions are pure: no transport-specific fields (`code`, formatted `message`). Formatting to any transport
happens at the consumer's boundary, not inside the library.
3. Exceptions are pure: no transport-specific fields (`code` populated with HTTP status, formatted `message` meant
for end-user display). Formatting to any transport happens at the consumer's boundary, not inside the library.
4. Exceptions signal invariant violations only, not control flow.
5. Name the class after the invariant violated, never after the technical type:
- `PrecisionOutOfRange` — not `InvalidPrecisionException`.
- `CurrencyMismatch` — not `BadCurrencyException`.
- `ContainerWaitTimeout` — not `TimeoutException`.
6. No exception-formatting constructor, no custom message argument — the class name is the message.
6. A descriptive `message` argument is allowed and encouraged when it carries **debugging context** — the violating
value, the boundary that was crossed, the state the library was in. The class name identifies the invariant;
the message describes the specific violation for stack traces and test assertions. Do not build messages meant
for end-user display or transport rendering. Keep them short, factual, and in American English.
7. Public exceptions live in `src/Exceptions/`. Internal exceptions live in `src/Internal/Exceptions/`.

**Prohibited** — throwing a native exception directly:
Expand All @@ -84,7 +114,7 @@ if ($value < 0) {
}
```

**Correct** — dedicated class extending the native exception:
**Correct** — dedicated class, no message (class name is sufficient):

```php
// src/Exceptions/PrecisionOutOfRange.php
Expand All @@ -98,6 +128,14 @@ if ($value < 0) {
}
```

**Correct** — dedicated class with debugging context:

```php
if ($value < 0 || $value > 16) {
throw new PrecisionOutOfRange(sprintf('Precision must be between 0 and 16, got %d.', $value));
}
```

## Enums

1. Are PHP backed enums.
Expand Down
46 changes: 19 additions & 27 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,30 +1,22 @@
# Auto detect text files and perform LF normalization
* text=auto eol=lf

# ─── Diff drivers ────────────────────────────────────────────
*.php diff=php
*.md diff=markdown
*.php text diff=php

# ─── Force LF ────────────────────────────────────────────────
*.sh text eol=lf
Makefile text eol=lf

# ─── Generated (skip diff and GitHub stats) ──────────────────
composer.lock -diff linguist-generated

# ─── Export ignore (excluded from dist archive) ──────────────
/tests export-ignore
/vendor export-ignore
/rules export-ignore

/.github export-ignore
/.gitignore export-ignore
/.gitattributes export-ignore

/CLAUDE.md export-ignore
/LICENSE export-ignore
/Makefile export-ignore
/README.md export-ignore
/phpunit.xml export-ignore
/phpstan.neon.dist export-ignore
/infection.json.dist export-ignore
# Dev-only — excluded from the Packagist tarball
/.github export-ignore
/tests export-ignore
/.claude export-ignore
/.editorconfig export-ignore
/.gitattributes export-ignore
/.gitignore export-ignore
/phpunit.xml export-ignore
/phpunit.xml.dist export-ignore
/phpstan.neon export-ignore
/phpstan.neon.dist export-ignore
/phpcs.xml export-ignore
/phpcs.xml.dist export-ignore
/infection.json export-ignore
/infection.json.dist export-ignore
/Makefile export-ignore
/CONTRIBUTING.md export-ignore
/CHANGES.md export-ignore
Comment thread
gustavofreze marked this conversation as resolved.
23 changes: 18 additions & 5 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,20 @@
.idea
# Agent/IDE
.claude/
.idea/
.vscode/
.cursor/

vendor
report
# Composer
/vendor/
composer.lock

*.lock
.phpunit.*
# PHPUnit / coverage
.phpunit.cache/
.phpunit.result.cache
report/
coverage/
build/

# OS
.DS_Store
Thumbs.db
6 changes: 3 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@
"require-dev": {
"ergebnis/composer-normalize": "^2.51",
"infection/infection": "^0.32",
"laminas/laminas-httphandlerrunner": "^2.12",
"laminas/laminas-httphandlerrunner": "^2.13",
"phpstan/phpstan": "^2.1",
"phpunit/phpunit": "^13.1",
"slim/psr7": "^1.7",
"slim/slim": "^4.14",
"slim/psr7": "^1.8",
"slim/slim": "^4.15",
"squizlabs/php_codesniffer": "^4.0"
},
"minimum-stability": "stable",
Expand Down
Loading