Skip to content

Commit 2414bf9

Browse files
authored
Release/6.0.1 (#58)
* docs: Update ordering rules for class members and chained calls. * refactor: Reorder class members and chained calls by name length.
1 parent ea6f268 commit 2414bf9

41 files changed

Lines changed: 2863 additions & 2862 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/rules/php-library-code-style.md

Lines changed: 35 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -44,27 +44,45 @@ Verify every item before producing any PHP code. If any item fails, revise befor
4444
5. Classes follow the rules in "Inheritance and constructors". `final readonly` is the default,
4545
with documented exceptions for extension points and for parents that are not `readonly`.
4646
6. Members are ordered constants first, then constructor, then static methods, then instance
47-
methods. Within each group, order by body size ascending (number of lines between `{` and `}`).
48-
Constants and enum cases, which have no body, are ordered by name length ascending. This
49-
ordering may be overridden only when the alternative carries explicit documentation value:
50-
grouping by domain class with section markers (HTTP status codes by 1xx/2xx/3xx/etc),
51-
mirroring the order of an implemented interface, or similar evident structure. The override
52-
must be obvious at first reading.
47+
methods. Within each group, order by **member name length ascending** (count the name only,
48+
without parentheses, arguments, or return type). Constants, enum cases, and methods share
49+
the same name-length-ascending rule, applied within their respective groups. This mirrors
50+
the rule that governs constructor parameters and named arguments (rule 7). When two names
51+
have equal length, order them alphabetically. This ordering may be overridden only when the
52+
alternative carries explicit documentation value: grouping by domain class with section
53+
markers (HTTP status codes by 1xx/2xx/3xx/etc), mirroring the order of an implemented
54+
interface, or similar evident structure. The override must be obvious at first reading.
5355

5456
**At call sites** (chained method calls in production code, tests, or documentation
55-
examples), consecutive method invocations on the same receiver are ordered by the **visible
56-
width** of each call expression ascending. The body is not visible at the call site, so the
57-
visible width is the practical proxy for body size. Boolean toggles such as `->secure()` and
58-
`->httpOnly()` come before parameterized `with*` builders for the same reason. When two
59-
calls have equal width, order them alphabetically by method name.
57+
examples), consecutive method invocations on the same receiver are ordered by **method name
58+
length ascending**, the same rule that governs member declarations. Boolean toggles such as
59+
`->secure()` and `->httpOnly()` come before parameterized `with*` builders because their
60+
names are shorter, not because the expression is narrower. When two method names have equal
61+
length, order them alphabetically.
6062

6163
**Terminal methods that change the receiver type** stay at the end of the chain regardless
62-
of width. A `build()` that returns the built value, a `commit()` that finalizes a unit of
63-
work, a `send()` that flushes a request, are terminal: the chain ends with them. The
64+
of name length. A `build()` that returns the built value, a `commit()` that finalizes a unit
65+
of work, a `send()` that flushes a request, are terminal: the chain ends with them. The
6466
ordering rule applies only to consecutive calls on the same receiver type; calls that
6567
transition to a different type are not reorderable. The same applies in reverse to the
6668
factory or accessor that starts the chain (`Cookie::create(...)`, `$repository`) — it stays
6769
at its position.
70+
71+
**PHPUnit test classes** follow a dedicated sub-grouping inside the instance-methods group
72+
that overrides the name-length-ascending rule:
73+
74+
1. **Lifecycle hooks** first, in PHPUnit execution order:
75+
`setUpBeforeClass``setUp``tearDown``tearDownAfterClass`. Only those actually
76+
defined appear; never introduce an empty hook to satisfy the rule.
77+
2. **Test methods** (prefix `test`) next, ordered by name length ascending (alphabetical
78+
tiebreak).
79+
3. **Data providers** last, ordered by name length ascending (alphabetical tiebreak).
80+
81+
A method is a data provider if and only if its name appears as the string argument of a
82+
`#[DataProvider('<name>')]` attribute or a `@dataProvider <name>` docblock annotation on a
83+
test method in the same class. The naming convention (`*DataProvider`) is informational
84+
only; the reference is the authoritative signal. A method named `*DataProvider` that no
85+
test references is dead code under rule 17, not a data provider.
6886
7. Constructor parameters are ordered by parameter name length ascending (count the name only,
6987
without `$` or type), except when parameters have an implicit semantic order (for example,
7088
`$start/$end`, `$from/$to`, `$startAt/$endAt`), which takes precedence. Parameters with default
@@ -225,10 +243,7 @@ are `canceled` (not `cancelled`), `organization` (not `organisation`), `initiali
225243

226244
### When required
227245

228-
- Every method of an interface, **including interfaces declared inside `src/Internal/`**.
229-
Interfaces define contracts. The contract is documentation by definition, regardless of
230-
namespace. The `Internal/` boundary applies to implementations, not to the contracts that
231-
internal collaborators expose to each other.
246+
- Every method of an interface.
232247
- Every public method of a concrete class outside `src/Internal/`. Public classes are at the
233248
public API boundary by definition. Consumers call every public method directly, and the
234249
PHPDoc is the contract for each call. Trivial getters and `with*` methods are not exempt.
@@ -244,10 +259,7 @@ are `canceled` (not `cancelled`), `organization` (not `organisation`), `initiali
244259
interface. The interface carries the docblock.
245260
- Anything inside `src/Internal/`. Internal types are implementation detail and must not carry
246261
PHPDoc. The namespace itself is the boundary. See `php-library-architecture.md` for the
247-
architectural meaning of `Internal/`. **Exception**: interfaces and their methods. An
248-
interface declared inside `src/Internal/` still defines a contract, and the contract is
249-
documented per `### When required` regardless of namespace. The prohibition covers concrete
250-
classes, traits, enums, and anonymous classes inside `Internal/`, never interfaces.
262+
architectural meaning of `Internal/`.
251263
- Anywhere inside `tests/`. Test methods name the scenario via the `testXxxWhenYyyGivenThenZzz`
252264
naming convention, and the `@Given`/`@When`/`@Then`/`@And` annotation blocks defined in
253265
`php-library-testing.md` describe the steps. PHPDoc documentation (summary plus
@@ -270,10 +282,7 @@ The PHPDoc prohibitions above take priority over the typed-array case. When PHPS
270282

271283
- On a **constructor parameter** → suppress via `ignoreErrors` in `phpstan.neon.dist`. Do not
272284
add PHPDoc.
273-
- On anything inside **`src/Internal/`** (concrete classes, traits, enums) → suppress via
274-
`ignoreErrors`. Do not add PHPDoc. Interfaces inside `src/Internal/` are the exception:
275-
they carry PHPDoc per `### When required`, and the PHPStan errors they raise are resolved
276-
through the PHPDoc, never through `ignoreErrors`.
285+
- On anything inside **`src/Internal/`** → suppress via `ignoreErrors`. Do not add PHPDoc.
277286
- On anything inside **`tests/`** → suppress via `ignoreErrors`. Do not add PHPDoc.
278287
- On a **public method of a public (non-Internal) class** → add full PHPDoc with summary,
279288
`@param` descriptions, and the typed-array information. The bare-tag form remains
@@ -338,8 +347,7 @@ public function __construct(public array $entries)
338347
}
339348
```
340349

341-
**Prohibited.** PHPDoc on a **concrete class** inside `src/Internal/` (the prohibition does
342-
not extend to interfaces; see "Correct" below for an Internal/ interface):
350+
**Prohibited.** PHPDoc on anything inside `src/Internal/`:
343351

344352
```php
345353
namespace TinyBlocks\Http\Internal\Client;
@@ -353,26 +361,6 @@ final readonly class Url
353361
}
354362
```
355363

356-
**Correct.** Interface declared **inside `src/Internal/`** still carries PHPDoc on every
357-
method. The Internal/ prohibition covers concrete classes; interfaces are exempt because they
358-
are the contract:
359-
360-
```php
361-
namespace TinyBlocks\Http\Internal\Client;
362-
363-
interface RequestResolver
364-
{
365-
/**
366-
* Resolves the given URL against the configured base URL.
367-
*
368-
* @param string $url The path or absolute URL to resolve.
369-
* @return string The absolute URL to dispatch.
370-
* @throws MalformedPath If the URL violates RFC 3986.
371-
*/
372-
public function resolve(string $url): string;
373-
}
374-
```
375-
376364
**Correct.** Generic array type with summary and `@param` description:
377365

378366
```php

.claude/rules/php-library-testing.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ Verify every item before producing any test code. If any item fails, revise befo
3636
4. No intermediate variables used only once. Chain method calls when the intermediate state is
3737
not referenced elsewhere (e.g., `Money::of(...)->add(...)` instead of
3838
`$money = Money::of(...)` followed by `$money->add(...)`).
39-
5. No private or helper methods in test classes. The only non-test methods allowed are data
39+
5. No private or helper methods in test classes. The only non-test methods allowed are PHPUnit
40+
lifecycle hooks (`setUp`, `setUpBeforeClass`, `tearDown`, `tearDownAfterClass`) and data
4041
providers. Setup logic complex enough to extract belongs in a dedicated fixture class.
4142
6. Test only the public API. Never assert on private state or `Internal/` classes directly.
4243
7. Test the behavior that **raises** an exception, never the exception itself. Exception classes
@@ -69,6 +70,8 @@ Verify every item before producing any test code. If any item fails, revise befo
6970
15. Never use `@codeCoverageIgnore`, attributes, or configuration that exclude code from
7071
coverage. Never suppress mutants via `infection.json.dist` or any other mechanism. See
7172
"Coverage and mutation discipline".
73+
16. Member ordering in test classes follows `php-library-code-style.md` rule 6 (PHPUnit
74+
test-class sub-grouping).
7275

7376
## Structure: Given/When/Then (BDD)
7477

src/Attribute.php

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -67,28 +67,28 @@ public function toString(): string
6767
}
6868

6969
/**
70-
* Returns the Attribute as an integer.
70+
* Returns the Attribute as a boolean.
7171
*
72-
* @return int The wrapped value coerced to an integer, or <code>0</code> when it is not scalar.
72+
* @return bool The wrapped value coerced to a boolean, or <code>false</code> when it is not scalar.
7373
*/
74-
public function toInteger(): int
74+
public function toBoolean(): bool
7575
{
7676
return match (true) {
77-
is_scalar($this->value) => (int)$this->value,
78-
default => 0
77+
is_scalar($this->value) => (bool)$this->value,
78+
default => false
7979
};
8080
}
8181

8282
/**
83-
* Returns the Attribute as a boolean.
83+
* Returns the Attribute as an integer.
8484
*
85-
* @return bool The wrapped value coerced to a boolean, or <code>false</code> when it is not scalar.
85+
* @return int The wrapped value coerced to an integer, or <code>0</code> when it is not scalar.
8686
*/
87-
public function toBoolean(): bool
87+
public function toInteger(): int
8888
{
8989
return match (true) {
90-
is_scalar($this->value) => (bool)$this->value,
91-
default => false
90+
is_scalar($this->value) => (int)$this->value,
91+
default => 0
9292
};
9393
}
9494
}

src/Body.php

Lines changed: 39 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -36,41 +36,6 @@ public static function fromArray(array $data): Body
3636
return new Body(data: $data);
3737
}
3838

39-
/**
40-
* Creates a Body from a PSR-7 server request, decoding the JSON payload up to 64 levels deep.
41-
*
42-
* When the raw body is empty, falls back to the parsed body and degrades to an empty Body
43-
* when the parsed body is not an array. JSON decoding uses <code>JSON_THROW_ON_ERROR</code>;
44-
* any decoding failure degrades to an empty Body rather than propagating the exception.
45-
*
46-
* @param ServerRequestInterface $request The incoming PSR-7 server request.
47-
* @return Body A Body carrying the decoded payload, or an empty Body when decoding fails or
48-
* the payload is not an array.
49-
*/
50-
public static function fromServerRequest(ServerRequestInterface $request): Body
51-
{
52-
$streamFactory = StreamFactory::fromStream(stream: $request->getBody());
53-
54-
if (!$streamFactory->isEmptyContent()) {
55-
try {
56-
$decoded = json_decode(
57-
$streamFactory->content(),
58-
true,
59-
Body::MAX_JSON_DEPTH,
60-
JSON_THROW_ON_ERROR
61-
);
62-
} catch (JsonException) {
63-
return new Body(data: []);
64-
}
65-
66-
return new Body(data: is_array($decoded) ? $decoded : []);
67-
}
68-
69-
$parsedBody = $request->getParsedBody();
70-
71-
return new Body(data: is_array($parsedBody) ? $parsedBody : []);
72-
}
73-
7439
/**
7540
* Creates a Body from a PSR-7 response, decoding the JSON payload and degrading to empty on failure.
7641
*
@@ -106,13 +71,38 @@ public static function fromResponse(ResponseInterface $response): Body
10671
}
10772

10873
/**
109-
* Returns the Body as an associative array.
74+
* Creates a Body from a PSR-7 server request, decoding the JSON payload up to 64 levels deep.
11075
*
111-
* @return array<string, mixed> The decoded body data.
76+
* When the raw body is empty, falls back to the parsed body and degrades to an empty Body
77+
* when the parsed body is not an array. JSON decoding uses <code>JSON_THROW_ON_ERROR</code>;
78+
* any decoding failure degrades to an empty Body rather than propagating the exception.
79+
*
80+
* @param ServerRequestInterface $request The incoming PSR-7 server request.
81+
* @return Body A Body carrying the decoded payload, or an empty Body when decoding fails or
82+
* the payload is not an array.
11283
*/
113-
public function toArray(): array
84+
public static function fromServerRequest(ServerRequestInterface $request): Body
11485
{
115-
return $this->data;
86+
$streamFactory = StreamFactory::fromStream(stream: $request->getBody());
87+
88+
if (!$streamFactory->isEmptyContent()) {
89+
try {
90+
$decoded = json_decode(
91+
$streamFactory->content(),
92+
true,
93+
Body::MAX_JSON_DEPTH,
94+
JSON_THROW_ON_ERROR
95+
);
96+
} catch (JsonException) {
97+
return new Body(data: []);
98+
}
99+
100+
return new Body(data: is_array($decoded) ? $decoded : []);
101+
}
102+
103+
$parsedBody = $request->getParsedBody();
104+
105+
return new Body(data: is_array($parsedBody) ? $parsedBody : []);
116106
}
117107

118108
/**
@@ -127,4 +117,14 @@ public function get(string $key): Attribute
127117

128118
return Attribute::from(value: $attributeValue);
129119
}
120+
121+
/**
122+
* Returns the Body as an associative array.
123+
*
124+
* @return array<string, mixed> The decoded body data.
125+
*/
126+
public function toArray(): array
127+
{
128+
return $this->data;
129+
}
130130
}

src/Charset.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@ enum Charset: string
1414
case BIG5 = 'big5';
1515
case ASCII = 'ascii';
1616
case UTF_8 = 'utf-8';
17-
case UTF_16 = 'utf-16';
1817
case EUC_KR = 'euc-kr';
1918
case GB2312 = 'gb2312';
2019
case KOI8_R = 'koi8-r';
20+
case UTF_16 = 'utf-16';
2121
case SHIFT_JIS = 'shift_jis';
2222
case ISO_8859_1 = 'iso-8859-1';
2323
case WINDOWS_1252 = 'windows-1252';

0 commit comments

Comments
 (0)