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
5 changes: 3 additions & 2 deletions .claude/rules/php-library-code-style.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ paths:

# Code style

Semantic rules for all PHP files in libraries. Formatting rules covered by `PSR-12` are enforced
by `phpcs.xml`. Four formatting rules outside `PSR-12` (single-line signatures within 120
Semantic rules for all PHP files in libraries. Formatting and structural rules are enforced by
`phpcs.xml` (the `PSR12` ruleset plus curated Squiz, Generic, PSR2, and `SlevomatCodingStandard`
sniffs). Four formatting rules the ruleset does not cover (single-line signatures within 120
characters, no vertical alignment in parameter lists, vertical alignment of `=>` in multi-line
match arms and array literals, no trailing comma in multi-line lists) are documented at the end
of this file under "Formatting overrides". Complexity rules live in `php-library-modeling.md`.
Expand Down
27 changes: 18 additions & 9 deletions .claude/rules/php-library-tooling.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,18 +36,26 @@ revise before outputting.
2. `composer.json` exposes exactly five scripts: `configure`, `configure-and-update`, `review`,
`test-file`, `tests`. No other public scripts.
3. `composer.json` fixed fields use the canonical values from the skill asset (`license`, `type`,
`minimum-stability`, `prefer-stable`, `authors`, `config`, `require.php`). The five universal
`minimum-stability`, `prefer-stable`, `authors`, `config`, `require.php`). The six universal
dev dependencies (`ergebnis/composer-normalize`, `infection/infection`, `phpstan/phpstan`,
`phpunit/phpunit`, `squizlabs/php_codesniffer`) are present. `require-dev` may add libraries
the tests need on top of those five. The asset's caret ranges are the canonical floor, and
the repo `composer.json` matches the asset. To bump, update the asset first, then the repo.
`phpunit/phpunit`, `slevomat/coding-standard`, `squizlabs/php_codesniffer`) are present, and
`config.allow-plugins` enables `dealerdirect/phpcodesniffer-composer-installer` so the Slevomat
sniffs register with phpcs. `require-dev` may add libraries the tests need on top of those six.
The asset's caret ranges are the canonical floor, and the repo `composer.json` matches the
asset. To bump, update the asset first, then the repo.
4. `composer.json` `description` is a single short sentence. Multi-sentence prose belongs in the
README Overview, not in Composer metadata.
5. `composer.json` includes a `keywords` array that contains `"tiny-blocks"`. Its position in
the array is not constrained. The remaining entries are topic tokens derived from the
library's purpose (`psr-7`, `http-client`, `event-sourcing`, etc.).
6. `phpcs.xml` references only the `PSR12` ruleset. No additional sniffs. Formatting rules outside
PSR-12 live in `php-library-code-style.md` under "Formatting overrides".
6. `phpcs.xml` references the `PSR12` ruleset plus curated Squiz, Generic, PSR2, and
`SlevomatCodingStandard` sniffs that mechanically enforce the library's structural and
type-hint conventions (class member order, alphabetically sorted and used-only imports, strict
types, parameter, property, and return type hints, and complexity ceilings). The
`SlevomatCodingStandard.Classes.ClassStructure` groups collapse all methods into one group, so
the `php-ordering-conformance` hook owns the name-length ordering within them. The four
formatting rules the ruleset does not cover live in `php-library-code-style.md` under
"Formatting overrides".
7. `phpunit.xml` sets all five `failOn*` flags to `true` (`failOnDeprecation`, `failOnNotice`,
`failOnPhpunitDeprecation`, `failOnRisky`, `failOnWarning`).
8. `phpunit.xml` sets `executionOrder="random"` and `beStrictAboutOutputDuringTests="true"`.
Expand Down Expand Up @@ -84,9 +92,10 @@ revise before outputting.
The committed config files split into two naming conventions on purpose. The split is documented
here so it reads as intentional, not accidental.

- **Committed live, no `.dist`:** `phpcs.xml` and `phpunit.xml`. The ruleset (`PSR12` only) and
the test configuration are stable across the whole ecosystem and identical in every library.
There is no per-clone local-override story, so the live file is committed directly.
- **Committed live, no `.dist`:** `phpcs.xml` and `phpunit.xml`. The ruleset (`PSR12` plus the
curated Squiz, Generic, PSR2, and `SlevomatCodingStandard` sniffs) and the test configuration
are stable across the whole ecosystem and identical in every library. There is no per-clone
local-override story, so the live file is committed directly.
- **Committed as `.dist`:** `phpstan.neon.dist` and `infection.json.dist`. These are the two
tools a contributor may legitimately want to tune locally (a temporary `ignoreErrors` entry, a
narrower mutator set while iterating). The `.dist` baseline is committed. A contributor drops a
Expand Down
5 changes: 3 additions & 2 deletions .claude/skills/tiny-blocks-create/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ description: Scaffold a new PHP library for the tiny-blocks ecosystem, or restor
This skill is the single source of truth for the boilerplate every tiny-blocks PHP library
shares: the config files, the CI workflow, and the repository templates. The canonical bodies
live in `assets/` as drop-in files. Copy them and substitute the placeholders rather than
regenerating them from memory. The assets already encode the ecosystem's decisions (PSR-12 only,
`level: max`, MSI 100, Docker-wrapped Makefile, the `.dist` naming split, the export-ignore set).
regenerating them from memory. The assets already encode the ecosystem's decisions (the `PSR12`
plus `SlevomatCodingStandard` ruleset, `level: max`, MSI 100, Docker-wrapped Makefile, the
`.dist` naming split, the export-ignore set).

The semantic conventions (how to name classes, how to structure `src/`, how to write tests) are
**not** in this skill. They live in `.claude/rules/`. This skill produces the skeleton. The rules
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

*.php text diff=php

# Mantém os scripts de tooling do Claude fora da estatística de linguagem do GitHub
/.claude/**/*.py linguist-vendored

# Dev-only, excluded from the Packagist tarball
/.github export-ignore
/tests export-ignore
Expand Down
4 changes: 4 additions & 0 deletions .claude/skills/tiny-blocks-create/assets/config/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,7 @@ infection.log
Thumbs.db
.DS_Store
Desktop.ini

# Cache dos hooks Python do .claude
__pycache__/
*.pyc
2 changes: 2 additions & 0 deletions .claude/skills/tiny-blocks-create/assets/config/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"infection/infection": "^0.33",
"phpstan/phpstan": "^2.2",
"phpunit/phpunit": "^13.1",
"slevomat/coding-standard": "^8.30",
"squizlabs/php_codesniffer": "^4.0"
},
"minimum-stability": "stable",
Expand All @@ -43,6 +44,7 @@
},
"config": {
"allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true,
"ergebnis/composer-normalize": true,
"infection/extension-installer": true
},
Expand Down
88 changes: 87 additions & 1 deletion .claude/skills/tiny-blocks-create/assets/config/phpcs.xml
Original file line number Diff line number Diff line change
@@ -1,7 +1,93 @@
<?xml version="1.0"?>
<ruleset name="tiny-blocks">
<description>Code style for the tiny-blocks library.</description>
<rule ref="PSR12"/>

<file>src</file>
<file>tests</file>

<rule ref="PSR12"/>

<rule ref="Squiz.Arrays.ArrayBracketSpacing"/>
<rule ref="Squiz.ControlStructures.ControlSignature"/>
<rule ref="Squiz.ControlStructures.ElseIfDeclaration"/>
<rule ref="Squiz.Formatting.OperatorBracket"/>
<rule ref="Squiz.Functions.FunctionDeclarationArgumentSpacing">
<properties>
<property name="equalsSpacing" value="1"/>
</properties>
</rule>
<rule ref="Squiz.Scope.MethodScope"/>
<rule ref="Squiz.WhiteSpace.ControlStructureSpacing"/>
<rule ref="Squiz.WhiteSpace.FunctionSpacing">
<properties>
<property name="spacing" value="1"/>
<property name="spacingBeforeFirst" value="0"/>
<property name="spacingAfterLast" value="0"/>
</properties>
</rule>
<rule ref="Squiz.WhiteSpace.SuperfluousWhitespace"/>

<rule ref="Generic.Arrays.DisallowLongArraySyntax"/>
<rule ref="Generic.CodeAnalysis.EmptyStatement"/>
<rule ref="Generic.ControlStructures.InlineControlStructure"/>
<rule ref="Generic.Formatting.DisallowMultipleStatements"/>
<rule ref="Generic.Metrics.CyclomaticComplexity">
<properties>
<property name="complexity" value="10"/>
<property name="absoluteComplexity" value="20"/>
</properties>
</rule>
<rule ref="Generic.Metrics.NestingLevel"/>
<rule ref="Generic.NamingConventions.ConstructorName"/>
<rule ref="Generic.PHP.DeprecatedFunctions"/>
<rule ref="Generic.PHP.LowerCaseKeyword"/>
<rule ref="Generic.Strings.UnnecessaryStringConcat"/>

<rule ref="PSR2.Classes.PropertyDeclaration"/>
<rule ref="PSR2.Files.EndFileNewline"/>
<rule ref="PSR2.Methods.MethodDeclaration"/>

<rule ref="SlevomatCodingStandard.Classes.ClassStructure">
<properties>
<property name="groups" type="array">
<element value="uses"/>
<element value="public constants"/>
<element value="protected constants"/>
<element value="private constants"/>
<element value="enum cases"/>
<element value="public properties, public static properties"/>
<element value="protected properties, protected static properties"/>
<element value="private properties, private static properties"/>
<element value="constructor, destructor"/>
<element value="all public methods, all protected methods, all private methods, static constructors"/>
<element value="magic methods"/>
</property>
</properties>
</rule>
<rule ref="SlevomatCodingStandard.Classes.ModernClassNameReference"/>
<rule ref="SlevomatCodingStandard.Complexity.Cognitive"/>
<rule ref="SlevomatCodingStandard.ControlStructures.RequireNullCoalesceOperator"/>
<rule ref="SlevomatCodingStandard.Namespaces.AlphabeticallySortedUses"/>
<rule ref="SlevomatCodingStandard.Namespaces.ReferenceUsedNamesOnly"/>
<rule ref="SlevomatCodingStandard.Namespaces.UnusedUses">
<properties>
<property name="searchAnnotations" value="true"/>
</properties>
</rule>
<rule ref="SlevomatCodingStandard.Namespaces.UseFromSameNamespace"/>
<rule ref="SlevomatCodingStandard.TypeHints.DeclareStrictTypes">
<properties>
<property name="spacesCountAroundEqualsSign" value="0"/>
</properties>
</rule>
<rule ref="SlevomatCodingStandard.TypeHints.ParameterTypeHint">
<exclude name="SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingTraversableTypeHintSpecification"/>
</rule>
<rule ref="SlevomatCodingStandard.TypeHints.PropertyTypeHint">
<exclude name="SlevomatCodingStandard.TypeHints.PropertyTypeHint.MissingTraversableTypeHintSpecification"/>
</rule>
<rule ref="SlevomatCodingStandard.TypeHints.ReturnTypeHint">
<exclude name="SlevomatCodingStandard.TypeHints.ReturnTypeHint.MissingTraversableTypeHintSpecification"/>
</rule>
<rule ref="SlevomatCodingStandard.Variables.UnusedVariable"/>
</ruleset>
3 changes: 3 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

*.php text diff=php

# Mantém os scripts de tooling do Claude fora da estatística de linguagem do GitHub
/.claude/**/*.py linguist-vendored

# Dev-only, excluded from the Packagist tarball
/.github export-ignore
/tests export-ignore
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,7 @@ infection.log
Thumbs.db
.DS_Store
Desktop.ini

# Cache dos hooks Python do .claude
__pycache__/
*.pyc
16 changes: 9 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,13 @@ $schema = Schema::create()

# GET /v1/orders?filter=status==paid;total=ge=100&sort=-created_at,id&page[number]=3&page[size]=20
/** @var ServerRequestInterface $request */
$criteria = Criteria::fromQuery(request: $request, schema: $schema);
$criteria = Criteria::fromQuery(schema: $schema, request: $request);
```

When `Criteria::fromQuery` receives no schema, an empty contract applies: the default page-size bounds, no filterable
or sortable field, and no default sort. Any incoming filter or sort is then rejected.
For an endpoint that declares no contract, `Criteria::fromQueryWithDefaultSchema` is the request-only entry point. It
applies the default schema, an empty contract: the default page-size bounds, no filterable or sortable field, and no
default sort. Any incoming filter or sort is then rejected. Both `Offset\Criteria` and `Cursor\Criteria` expose it, so
either pagination parses a request without building a schema.

| Setting | Default | Meaning |
|------------------|---------|-------------------------------------------------|
Expand Down Expand Up @@ -178,7 +180,7 @@ use TinyBlocks\HttpQuery\Schema;

# GET /v1/orders?page[number]=3&page[size]=20
/** @var ServerRequestInterface $request */
$criteria = Criteria::fromQuery(request: $request, schema: Schema::create());
$criteria = Criteria::fromQuery(schema: Schema::create(), request: $request);

/** @var iterable<mixed> $items */
$page = $criteria->page(total: 480, items: $items);
Expand All @@ -202,7 +204,7 @@ use TinyBlocks\HttpQuery\Schema;

# GET /v1/orders?page[number]=2&page[size]=20
/** @var ServerRequestInterface $request */
$criteria = Criteria::fromQuery(request: $request, schema: Schema::create());
$criteria = Criteria::fromQuery(schema: Schema::create(), request: $request);

/** @var iterable<mixed> $items */
$slice = $criteria->slice(items: $items);
Expand Down Expand Up @@ -231,7 +233,7 @@ $schema = Schema::create()->sortable(fields: ['created_at', 'id']);

# GET /v1/orders?sort=-created_at,id&page[cursor]=BS3RvKY4LqEjYD19mQ0mCpJ&page[size]=20
/** @var ServerRequestInterface $request */
$keyset = Criteria::fromQuery(request: $request, schema: $schema)->keyset();
$keyset = Criteria::fromQuery(schema: $schema, request: $request)->keyset();

$keyset->limit()->toInteger(); # The page size, 20.
$keyset->orders(); # The list<Order> the seek is ordered by.
Expand Down Expand Up @@ -387,7 +389,7 @@ $schema = Schema::create()->sortable(fields: ['created_at', 'id']);

# GET /v1/orders?filter=status==paid&sort=-created_at,id&page[number]=3&page[size]=20
/** @var ServerRequestInterface $request */
$criteria = Criteria::fromQuery(request: $request, schema: $schema);
$criteria = Criteria::fromQuery(schema: $schema, request: $request);

/** @var iterable<mixed> $items */
$response = $criteria->page(total: 480, items: $items)->toResponse(baseUri: '/v1/orders');
Expand Down
2 changes: 2 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"nyholm/psr7": "^1.8",
"phpstan/phpstan": "^2.2",
"phpunit/phpunit": "^13.1",
"slevomat/coding-standard": "^8.30",
"squizlabs/php_codesniffer": "^4.0"
},
"minimum-stability": "stable",
Expand All @@ -51,6 +52,7 @@
},
"config": {
"allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true,
"ergebnis/composer-normalize": true,
"infection/extension-installer": true
},
Expand Down
88 changes: 87 additions & 1 deletion phpcs.xml
Original file line number Diff line number Diff line change
@@ -1,7 +1,93 @@
<?xml version="1.0"?>
<ruleset name="tiny-blocks">
<description>Code style for the tiny-blocks library.</description>
<rule ref="PSR12"/>

<file>src</file>
<file>tests</file>

<rule ref="PSR12"/>

<rule ref="Squiz.Arrays.ArrayBracketSpacing"/>
<rule ref="Squiz.ControlStructures.ControlSignature"/>
<rule ref="Squiz.ControlStructures.ElseIfDeclaration"/>
<rule ref="Squiz.Formatting.OperatorBracket"/>
<rule ref="Squiz.Functions.FunctionDeclarationArgumentSpacing">
<properties>
<property name="equalsSpacing" value="1"/>
</properties>
</rule>
<rule ref="Squiz.Scope.MethodScope"/>
<rule ref="Squiz.WhiteSpace.ControlStructureSpacing"/>
<rule ref="Squiz.WhiteSpace.FunctionSpacing">
<properties>
<property name="spacing" value="1"/>
<property name="spacingBeforeFirst" value="0"/>
<property name="spacingAfterLast" value="0"/>
</properties>
</rule>
<rule ref="Squiz.WhiteSpace.SuperfluousWhitespace"/>

<rule ref="Generic.Arrays.DisallowLongArraySyntax"/>
<rule ref="Generic.CodeAnalysis.EmptyStatement"/>
<rule ref="Generic.ControlStructures.InlineControlStructure"/>
<rule ref="Generic.Formatting.DisallowMultipleStatements"/>
<rule ref="Generic.Metrics.CyclomaticComplexity">
<properties>
<property name="complexity" value="10"/>
<property name="absoluteComplexity" value="20"/>
</properties>
</rule>
<rule ref="Generic.Metrics.NestingLevel"/>
<rule ref="Generic.NamingConventions.ConstructorName"/>
<rule ref="Generic.PHP.DeprecatedFunctions"/>
<rule ref="Generic.PHP.LowerCaseKeyword"/>
<rule ref="Generic.Strings.UnnecessaryStringConcat"/>

<rule ref="PSR2.Classes.PropertyDeclaration"/>
<rule ref="PSR2.Files.EndFileNewline"/>
<rule ref="PSR2.Methods.MethodDeclaration"/>

<rule ref="SlevomatCodingStandard.Classes.ClassStructure">
<properties>
<property name="groups" type="array">
<element value="uses"/>
<element value="public constants"/>
<element value="protected constants"/>
<element value="private constants"/>
<element value="enum cases"/>
<element value="public properties, public static properties"/>
<element value="protected properties, protected static properties"/>
<element value="private properties, private static properties"/>
<element value="constructor, destructor"/>
<element value="all public methods, all protected methods, all private methods, static constructors"/>
<element value="magic methods"/>
</property>
</properties>
</rule>
<rule ref="SlevomatCodingStandard.Classes.ModernClassNameReference"/>
<rule ref="SlevomatCodingStandard.Complexity.Cognitive"/>
<rule ref="SlevomatCodingStandard.ControlStructures.RequireNullCoalesceOperator"/>
<rule ref="SlevomatCodingStandard.Namespaces.AlphabeticallySortedUses"/>
<rule ref="SlevomatCodingStandard.Namespaces.ReferenceUsedNamesOnly"/>
<rule ref="SlevomatCodingStandard.Namespaces.UnusedUses">
<properties>
<property name="searchAnnotations" value="true"/>
</properties>
</rule>
<rule ref="SlevomatCodingStandard.Namespaces.UseFromSameNamespace"/>
<rule ref="SlevomatCodingStandard.TypeHints.DeclareStrictTypes">
<properties>
<property name="spacesCountAroundEqualsSign" value="0"/>
</properties>
</rule>
<rule ref="SlevomatCodingStandard.TypeHints.ParameterTypeHint">
<exclude name="SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingTraversableTypeHintSpecification"/>
</rule>
<rule ref="SlevomatCodingStandard.TypeHints.PropertyTypeHint">
<exclude name="SlevomatCodingStandard.TypeHints.PropertyTypeHint.MissingTraversableTypeHintSpecification"/>
</rule>
<rule ref="SlevomatCodingStandard.TypeHints.ReturnTypeHint">
<exclude name="SlevomatCodingStandard.TypeHints.ReturnTypeHint.MissingTraversableTypeHintSpecification"/>
</rule>
<rule ref="SlevomatCodingStandard.Variables.UnusedVariable"/>
</ruleset>
Loading