diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 3e73900..11885b0 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -1,10 +1,12 @@ # Project -PHP microservices platform. Hexagonal architecture (ports & adapters), DDD, CQRS. +PHP library (tiny-blocks ecosystem). Self-contained package: immutable models, zero infrastructure +dependencies in core, small public surface area. Public API at `src/` root; implementation details +under `src/Internal/`. ## Rules -All coding standards, architecture, naming, testing, documentation, and OpenAPI conventions +All coding standards, architecture, naming, testing, and documentation conventions are defined in `rules/`. Read the applicable rule files before generating any code or documentation. ## Commands diff --git a/.claude/rules/php-domain.md b/.claude/rules/php-domain.md deleted file mode 100644 index f3b0eea..0000000 --- a/.claude/rules/php-domain.md +++ /dev/null @@ -1,96 +0,0 @@ ---- -description: Domain modeling rules for PHP libraries — folder structure, naming, value objects, exceptions, enums, and SOLID. -paths: - - "src/**/*.php" ---- - -# Domain modeling - -Libraries are self-contained packages. The core has no dependency on frameworks, databases, or I/O. -Refer to `rules/code-style.md` for the pre-output checklist applied to all PHP code. - -## Folder structure - -``` -src/ -├── .php # Primary contract for consumers -├── .php # Main implementation or extension point -├── .php # Public enum -├── Contracts/ # Interfaces for data returned to consumers -├── Internal/ # Implementation details (not part of public API) -│ ├── .php -│ └── Exceptions/ # Internal exception classes -├── / # Feature-specific subdirectory when needed -└── Exceptions/ # Public exception classes (when part of the API) -``` - -**Public API boundary:** Only interfaces, extension points, enums, and thin orchestration classes live at the -`src/` root. These classes define the contract consumers interact with and delegate all real work to collaborators -inside `src/Internal/`. If a class contains substantial logic (algorithms, state machines, I/O), it belongs in -`Internal/`, not at the root. - -The `Internal/` namespace signals classes that are implementation details. Consumers must not depend on them. -Never use `Entities/`, `ValueObjects/`, `Enums/`, or `Domain/` as folder names. - -## Nomenclature - -1. Every class, property, method, and exception name reflects the **domain 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: `Manager`, `Helper`, `Processor`, `Data`, `Info`, `Utils`, - `Item`, `Record`, `Entity`, `Exception`, `Ensure`, `Validate`, `Check`, `Verify`, - `Assert`, `Transform`, `Parse`, `Compute`, `Sanitize`, or `Normalize` as class suffixes or prefixes. -3. Name classes after what they represent: `Money`, `Color`, `Pipeline` — not after what they do technically. -4. Name methods after the operation in domain terms: `add()`, `convertTo()`, `splitAt()` — not `process()`, - `handle()`, `execute()`, `manage()`, `ensure()`, `validate()`, `check()`, `verify()`, `assert()`, - `transform()`, `parse()`, `compute()`, `sanitize()`, or `normalize()`. - -## Value objects - -1. Are immutable: no setters, no mutation after construction. Operations return new instances. -2. Compare by value, not by reference. -3. Validate invariants in the constructor and throw on invalid input. -4. Have no identity field. -5. Use static factory methods (e.g., `from`, `of`, `zero`) with a private constructor when multiple creation - paths exist. - -## Exceptions - -1. Extend native PHP exceptions (`DomainException`, `InvalidArgumentException`, `OverflowException`, etc.). -2. Are pure: no formatted `code`/`message` for HTTP responses. -3. Signal invariant violations only. -4. Name after the invariant violated, never after the technical type: - `PrecisionOutOfRange` — not `InvalidPrecisionException`. - `CurrencyMismatch` — not `BadCurrencyException`. - `ContainerWaitTimeout` — not `TimeoutException`. -5. Create the exception class directly with the invariant name and the appropriate native parent. The exception - is dedicated by definition when its name describes the specific invariant it guards. - -## Enums - -1. Are PHP backed enums. -2. Include domain-meaningful methods when needed (e.g., `Order::ASCENDING_KEY`). - -## Extension points - -1. When a class is designed to be extended by consumers (e.g., `Collection`, `ValueObject`), it uses `class` - instead of `final readonly class`. All other classes use `final readonly class`. -2. Extension point classes use a private constructor with static factory methods (`createFrom`, `createFromEmpty`) - as the only creation path. -3. Internal state is injected via the constructor and stored in a `private readonly` property. - -## Principles - -- **Immutability**: all models and value objects adopt immutability. Operations return new instances. -- **Zero dependencies**: the library's core has no dependency on frameworks, databases, or I/O. -- **Small surface area**: expose only what consumers need. Hide implementation in `Internal/`. - -## SOLID reference - -| Principle | Failure signal | -|---------------------------|---------------------------------------------| -| S — Single responsibility | Class does two unrelated things | -| O — Open/closed | Adding a feature requires editing internals | -| L — Liskov substitution | Subclass throws on parent method | -| I — Interface segregation | Interface has unused methods | -| D — Dependency inversion | Constructor uses `new ConcreteClass()` | diff --git a/.claude/rules/php-code-style.md b/.claude/rules/php-library-code-style.md similarity index 62% rename from .claude/rules/php-code-style.md rename to .claude/rules/php-library-code-style.md index 59323ba..7ec196e 100644 --- a/.claude/rules/php-code-style.md +++ b/.claude/rules/php-library-code-style.md @@ -1,14 +1,14 @@ --- -description: Pre-output checklist, naming, typing, comparisons, and PHPDoc rules for all PHP files in libraries. +description: Pre-output checklist, naming, typing, complexity, and PHPDoc rules for all PHP files in libraries. paths: - - "src/**/*.php" - - "tests/**/*.php" + - "src/**/*.php" + - "tests/**/*.php" --- # Code style Semantic code rules for all PHP files. Formatting rules (PSR-1, PSR-4, PSR-12, line length) are enforced by `phpcs.xml` -and are not repeated here. Refer to `rules/domain.md` for domain modeling rules. +and are not repeated here. Refer to `php-library-modeling.md` for library modeling rules. ## Pre-output checklist @@ -29,9 +29,10 @@ Verify every item before producing any PHP code. If any item fails, revise befor 8. No generic identifiers exist. Use domain-specific names instead: `$data` → `$payload`, `$value` → `$totalAmount`, `$item` → `$element`, `$info` → `$currencyDetails`, `$result` → `$conversionOutcome`. -9. No raw arrays exist where a typed collection or value object is available. Use `tiny-blocks/collection` - (`Collection`, `Collectible`) instead of raw `array` for any list of domain objects. Raw arrays are acceptable - only for primitive configuration data, variadic pass-through, or interop at system boundaries. +9. No raw arrays exist where a typed collection or value object is available. Use the `tiny-blocks/collection` + fluent API (`Collection`, `Collectible`) when data is `Collectible`. Use `createLazyFrom` when elements are + consumed once. Raw arrays are acceptable only for primitive configuration data, variadic pass-through, and + interop at system boundaries. See "Collection usage" below for the full rule and example. 10. No private methods exist except private constructors for factory patterns. Inline trivial logic at the call site or extract it to a collaborator or value object. 11. Members are ordered: constants first, then constructor, then static methods, then instance methods. Within each @@ -39,9 +40,16 @@ Verify every item before producing any PHP code. If any item fails, revise befor no body, are ordered by name length ascending. 12. Constructor parameters are ordered by parameter name length ascending (count the name only, without `$` or type), except when parameters have an implicit semantic order (e.g., `$start/$end`, `$from/$to`, `$startAt/$endAt`), - which takes precedence. The same rule applies to named arguments at call sites. + which takes precedence. Parameters with default values go last, regardless of name length. The same rule + applies to named arguments at call sites. Example: `$id` (2) → `$value` (5) → `$status` (6) → `$precision` (9). -13. No O(N²) or worse complexity exists. +13. Time and space complexity are first-class design concerns. + - No `O(N²)` or worse time complexity exists unless the problem inherently requires it and the cost is + documented in PHPDoc on the interface method. + - Space complexity is kept minimal: prefer lazy/streaming pipelines (`createLazyFrom`) over materializing + intermediate collections. + - Never re-iterate the same source; fuse stages when possible. + - Public interface methods document time and space complexity in Big O form (see "PHPDoc" section). 14. No logic is duplicated across two or more places (DRY). 15. No abstraction exists without real duplication or isolation need (KISS). 16. All identifiers, comments, and documentation are written in American English. @@ -59,8 +67,31 @@ Verify every item before producing any PHP code. If any item fails, revise befor 23. No vertical alignment of types in parameter lists or property declarations. Use a single space between type and variable name. Never pad with extra spaces to align columns: `public OrderId $id` — not `public OrderId $id`. -24. Opening brace `{` goes on the same line as the closing parenthesis `)` for constructors, methods, and - closures: `): ReturnType {` — not `): ReturnType\n {`. Parameters with default values go last. +24. Opening brace `{` follows PSR-12: on a **new line** for classes, interfaces, traits, enums, and methods + (including constructors); on the **same line** for closures and control structures (`if`, `for`, `foreach`, + `while`, `switch`, `match`, `try`). +25. Never pass an argument whose value equals the parameter's default. Omit the argument entirely. + Example — `toArray(KeyPreservation $keyPreservation = KeyPreservation::PRESERVE)`: + `$collection->toArray(keyPreservation: KeyPreservation::PRESERVE)` → `$collection->toArray()`. + Only pass the argument when the value differs from the default. +26. No trailing comma in any multi-line list. This applies to parameter lists (constructors, methods, + closures), argument lists at call sites, array literals, match arms, and any other comma-separated + multi-line structure. The last element never has a comma after it. PHP accepts trailing commas in + parameter lists, but this project prohibits them for visual consistency. + Example — correct: + ``` + new Precision( + value: 2, + rounding: RoundingMode::HALF_UP + ); + ``` + Example — prohibited: + ``` + new Precision( + value: 2, + rounding: RoundingMode::HALF_UP, + ); + ``` ## Casing conventions @@ -70,9 +101,7 @@ Verify every item before producing any PHP code. If any item fails, revise befor ## Naming - Names describe **what** in domain terms, not **how** technically: `$monthlyRevenue` instead of `$calculatedValue`. -- Generic technical verbs (`process`, `handle`, `execute`, `mark`, `enforce`, `manage`, `ensure`, `validate`, - `check`, `verify`, `assert`, `transform`, `parse`, `compute`, `sanitize`, `normalize`) **should be avoided**. - Prefer names that describe the domain operation. +- Generic technical verbs are avoided. See `php-library-modeling.md` — Nomenclature. - Booleans use predicate form: `isActive`, `hasPermission`, `wasProcessed`. - Collections are always plural: `$orders`, `$lines`. - Methods returning bool use prefixes: `is`, `has`, `can`, `was`, `should`. @@ -93,12 +122,19 @@ All identifiers, enum values, comments, and error codes use American English spe ## PHPDoc -- PHPDoc is restricted to interfaces only, documenting obligations and `@throws`. +- PHPDoc is restricted to interfaces only, documenting obligations, `@throws`, and complexity. - Never add PHPDoc to concrete classes. +- Document `@throws` for every exception the method may raise. +- Document time and space complexity in Big O form. When a method participates in a fused pipeline (e.g., collection + pipelines), express cost as a two-part form: call-site cost + fused-pass contribution. Include a legend defining + variables (e.g., `N` for input size, `K` for number of stages). ## Collection usage -When a property or parameter is `Collectible`, use its fluent API. Never break out to raw array functions. +When a property or parameter is `Collectible`, use its fluent API. Never break out to raw array functions such as +`array_map`, `array_filter`, `iterator_to_array`, or `foreach` + accumulation. The same applies to `filter()`, +`reduce()`, `each()`, and all other `Collectible` operations. Chain them fluently. Never materialize with +`iterator_to_array` to then pass into a raw `array_*` function. **Prohibited — `array_map` + `iterator_to_array` on a Collectible:** @@ -116,6 +152,3 @@ $names = $collection ->map(transformations: static fn(Element $element): string => $element->name()) ->toArray(keyPreservation: KeyPreservation::DISCARD); ``` - -The same applies to `filter()`, `reduce()`, `each()`, and all other `Collectible` operations. Chain them -fluently. Never materialize with `iterator_to_array` to then pass into a raw `array_*` function. diff --git a/.claude/rules/documentation.md b/.claude/rules/php-library-documentation.md similarity index 82% rename from .claude/rules/documentation.md rename to .claude/rules/php-library-documentation.md index 64587c9..d7ac6da 100644 --- a/.claude/rules/documentation.md +++ b/.claude/rules/php-library-documentation.md @@ -1,7 +1,7 @@ --- description: Standards for README files and all project documentation in PHP libraries. paths: - - "**/*.md" + - "**/*.md" --- # Documentation @@ -21,7 +21,8 @@ paths: frequently ask about. Each entry is a numbered question as heading (e.g., `### 01. Why does X happen?`) followed by a concise explanation. Only include entries that address real confusion points. 9. **License** and **Contributing** sections at the end. -10. Write strictly in American English. See `rules/code-style.md` American English section for spelling conventions. +10. Write strictly in American English. See `php-library-code-style.md` American English section for spelling + conventions. ## Structured data @@ -34,4 +35,6 @@ paths: 1. Keep language concise and scannable. 2. Never include placeholder content (`TODO`, `TBD`). 3. Code examples must be syntactically correct and self-contained. -4. Do not document `Internal/` classes or private API. Only document what consumers interact with. +4. Code examples include every `use` statement needed to compile. Each example stands alone — copyable into + a fresh file without modification. +5. Do not document `Internal/` classes or private API. Only document what consumers interact with. diff --git a/.claude/rules/php-library-modeling.md b/.claude/rules/php-library-modeling.md new file mode 100644 index 0000000..bedb733 --- /dev/null +++ b/.claude/rules/php-library-modeling.md @@ -0,0 +1,163 @@ +--- +description: Library modeling rules — folder structure, public API boundary, naming, value objects, exceptions, enums, extension points, and complexity. +paths: + - "src/**/*.php" +--- + +# Library modeling + +Libraries are self-contained packages. The core has no dependency on frameworks, databases, or I/O. Refer to +`php-library-code-style.md` for the pre-output checklist applied to all PHP code. + +## Folder structure + +``` +src/ +├── .php # Primary contract for consumers +├── .php # Main implementation or extension point +├── .php # Public enum +├── Contracts/ # Interfaces for data returned to consumers +├── Internal/ # Implementation details (not part of public API) +│ ├── .php +│ └── Exceptions/ # Internal exception classes +├── / # Feature-specific subdirectory when needed +└── Exceptions/ # Public exception classes (when part of the API) +``` + +Never use `Models/`, `Entities/`, `ValueObjects/`, `Enums/`, or `Domain/` as folder names. + +## Public API boundary + +Only interfaces, extension points, enums, and thin orchestration classes live at the `src/` root. These classes +define the contract consumers interact with and delegate all real work to collaborators inside `src/Internal/`. +If a class contains substantial logic (algorithms, state machines, I/O), it belongs in `Internal/`, not at the root. + +The `Internal/` namespace signals classes that are implementation details. Consumers must not depend on them. +Breaking changes inside `Internal/` are not semver-breaking for the library. + +## Nomenclature + +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. 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 + +1. Are immutable: no setters, no mutation after construction. Operations return new instances. +2. Compare by value, not by reference. +3. Validate invariants in the constructor and throw on invalid input. +4. Have no identity field. +5. Use static factory methods (e.g., `from`, `of`, `zero`) with a private constructor when multiple creation paths + exist. The factory name communicates the semantic intent. + +## Exceptions + +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 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` 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. 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: + +```php +if ($value < 0) { + throw new InvalidArgumentException('Precision cannot be negative.'); +} +``` + +**Correct** — dedicated class, no message (class name is sufficient): + +```php +// src/Exceptions/PrecisionOutOfRange.php +final class PrecisionOutOfRange extends InvalidArgumentException +{ +} + +// at the callsite +if ($value < 0) { + throw new PrecisionOutOfRange(); +} +``` + +**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. +2. Include methods when they carry vocabulary meaning (e.g., `Order::ASCENDING_KEY`, `RoundingMode::apply()`). +3. Live at the `src/` root when public. Enums used only by internals live in `src/Internal/`. + +## Extension points + +1. When a class is designed to be extended by consumers (e.g., `Collection`, `ValueObject`), it uses `class` instead + of `final readonly class`. All other classes use `final readonly class`. +2. Extension point classes use a private constructor with static factory methods (`createFrom`, `createFromEmpty`) + as the only creation path. +3. Internal state is injected via the constructor and stored in a `private readonly` property. + +## Time and space complexity + +1. Every public method has predictable, documented complexity. Document Big O in PHPDoc on the interface + (see `php-library-code-style.md`, "PHPDoc" section). +2. Algorithms run in `O(N)` or `O(N log N)` unless the problem inherently requires worse. `O(N²)` or worse must + be justified and documented. +3. Prefer lazy/streaming evaluation over materializing intermediate results. In pipeline-style libraries, fuse + stages so a single pass suffices. +4. Memory usage is bounded and proportional to the output, not to the sum of intermediate stages. +5. Validate complexity claims with benchmarks against a reference implementation when optimizing critical paths. + Parity testing against the reference library is the validation standard for optimization work. diff --git a/.claude/rules/php-testing.md b/.claude/rules/php-library-testing.md similarity index 82% rename from .claude/rules/php-testing.md rename to .claude/rules/php-library-testing.md index 7bd9e68..610b928 100644 --- a/.claude/rules/php-testing.md +++ b/.claude/rules/php-library-testing.md @@ -1,12 +1,13 @@ --- description: BDD Given/When/Then structure, PHPUnit conventions, test organization, and fixture rules for PHP libraries. paths: - - "tests/**/*.php" + - "tests/**/*.php" --- # Testing conventions -Framework: **PHPUnit**. Refer to `rules/code-style.md` for the code style checklist, which also applies to test files. +Framework: **PHPUnit**. Refer to `php-library-code-style.md` for the code style checklist, which also applies to +test files. ## Structure: Given/When/Then (BDD) @@ -62,15 +63,14 @@ Use `@And` for complementary preconditions or actions within the same scenario, 5. Never include conditional logic inside tests. 6. Include one logical concept per `@Then` block. 7. Maintain strict independence between tests. No inherited state. -8. For exception tests, place `@Then` (expectException) before `@When`. -9. Use domain-specific model classes in `tests/Models/` for test fixtures that represent domain concepts +8. Use domain-specific model classes in `tests/Models/` for test fixtures that represent domain concepts (e.g., `Amount`, `Invoice`, `Order`). -10. Use mock classes in `tests/Mocks/` (or `tests/Unit/Mocks/`) for test doubles of system boundaries - (e.g., `ClientMock`, `ExecutionCompletedMock`). -11. Exercise invariants and edge cases through the library's public entry point. Create a dedicated test class +9. Use mock classes in `tests/Mocks/` (or `tests/Unit/Mocks/`) for test doubles of system boundaries + (e.g., `ClientMock`, `ExecutionCompletedMock`). +10. Exercise invariants and edge cases through the library's public entry point. Create a dedicated test class for an internal model only when the condition cannot be reached through the public API. -12. Never use `/** @test */` annotation. Test methods are discovered by the `test` prefix in the method name. -13. Never use named arguments on PHPUnit assertions (`assertEquals`, `assertSame`, `assertTrue`, +11. Never use `/** @test */` annotation. Test methods are discovered by the `test` prefix in the method name. +12. Never use named arguments on PHPUnit assertions (`assertEquals`, `assertSame`, `assertTrue`, `expectException`, etc.). Pass arguments positionally. ## Test setup and fixtures @@ -104,11 +104,7 @@ tests/ └── bootstrap.php # Test bootstrap when needed ``` -- `tests/` or `tests/Unit/`: pure unit tests exercising the library's public API. -- `tests/Integration/`: tests requiring real external resources (e.g., Docker containers, databases). - Only present when the library interacts with infrastructure. -- `tests/Models/`: domain-specific fixture classes reused across test files. -- `tests/Mocks/` or `tests/Unit/Mocks/`: test doubles for system boundaries. +`tests/Integration/` is only present when the library interacts with infrastructure. ## Coverage and mutation testing diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..73e3c9a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,18 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{yml,yaml}] +indent_size = 2 + +[Makefile] +indent_style = tab + +[*.md] +trim_trailing_whitespace = false diff --git a/Makefile b/Makefile index f6c305b..07acc3b 100644 --- a/Makefile +++ b/Makefile @@ -17,6 +17,7 @@ YELLOW := \033[0;33m .PHONY: configure configure: ## Configure development environment @${DOCKER_RUN} composer update --optimize-autoloader + @${DOCKER_RUN} composer normalize .PHONY: test test: ## Run all tests with coverage diff --git a/README.md b/README.md index 5ddd501..50e88ce 100644 --- a/README.md +++ b/README.md @@ -18,14 +18,11 @@ ## Overview -The `Collection` library provides a flexible and efficient API to manipulate, iterate, and manage collections in a -structured and type-safe manner. - -It leverages [PHP's Generators](https://www.php.net/manual/en/language.generators.overview.php) for optimized memory -usage and lazy evaluation, ensuring that large datasets are handled efficiently without loading all -elements into memory at once. - -The library supports adding, removing, filtering, sorting, and transforming elements. +Models a type-safe, fluent collection API for PHP, unifying arrays, iterators, +and [generators](https://www.php.net/manual/en/language.generators.overview.php) behind a single +Collectible contract. Supports both eager and lazy evaluation pipelines, with chainable operations for mapping, +filtering, grouping, reducing, and joining. Designed for predictable memory and CPU profiles in data-intensive +workloads. ## Installation diff --git a/composer.json b/composer.json index d9caeb7..2e9db9e 100644 --- a/composer.json +++ b/composer.json @@ -1,38 +1,33 @@ { "name": "tiny-blocks/collection", - "type": "library", + "description": "Models a type-safe, fluent collection API for PHP with eager and lazy pipelines over arrays, iterators, and generators.", "license": "MIT", - "homepage": "https://github.com/tiny-blocks/collection", - "description": "Provides a flexible and efficient API to manipulate, iterate, and manage collections in a structured and type-safe manner.", - "prefer-stable": true, - "minimum-stability": "stable", - "keywords": [ - "psr", - "json", - "array", - "yield", - "iterator", - "iterators", - "generator", - "collection", - "tiny-blocks" - ], + "type": "library", "authors": [ { "name": "Gustavo Freze de Araujo Santos", "homepage": "https://github.com/gustavofreze" } ], + "homepage": "https://github.com/tiny-blocks/collection", "support": { "issues": "https://github.com/tiny-blocks/collection/issues", "source": "https://github.com/tiny-blocks/collection" }, - "config": { - "sort-packages": true, - "allow-plugins": { - "infection/extension-installer": true - } + "require": { + "php": "^8.5", + "tiny-blocks/mapper": "^2.0" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.51", + "infection/infection": "^0.32", + "phpstan/phpstan": "^2.1", + "phpunit/phpunit": "^13.1", + "squizlabs/php_codesniffer": "^4.0", + "tiny-blocks/currency": "^2.3" }, + "minimum-stability": "stable", + "prefer-stable": true, "autoload": { "psr-4": { "TinyBlocks\\Collection\\": "src/" @@ -43,28 +38,24 @@ "Test\\TinyBlocks\\Collection\\": "tests/" } }, - "require": { - "php": "^8.5", - "tiny-blocks/mapper": "^2.0" - }, - "require-dev": { - "phpunit/phpunit": "^13.1", - "phpstan/phpstan": "^2.1", - "infection/infection": "^0.32", - "tiny-blocks/currency": "^2.3", - "squizlabs/php_codesniffer": "^4.0" + "config": { + "allow-plugins": { + "ergebnis/composer-normalize": true, + "infection/extension-installer": true + }, + "sort-packages": true }, "scripts": { - "test": "php -d memory_limit=2G ./vendor/bin/phpunit --configuration phpunit.xml tests", + "mutation-test": "php ./vendor/bin/infection --threads=max --logger-html=report/coverage/mutation-report.html --coverage=report/coverage", "phpcs": "php ./vendor/bin/phpcs --standard=PSR12 --extensions=php ./src", "phpstan": "php ./vendor/bin/phpstan analyse -c phpstan.neon.dist --quiet --no-progress", - "test-file": "php ./vendor/bin/phpunit --configuration phpunit.xml --no-coverage --filter", - "mutation-test": "php ./vendor/bin/infection --threads=max --logger-html=report/coverage/mutation-report.html --coverage=report/coverage", - "test-no-coverage": "php ./vendor/bin/phpunit --configuration phpunit.xml --no-coverage tests", "review": [ "@phpcs", "@phpstan" ], + "test": "php -d memory_limit=2G ./vendor/bin/phpunit --configuration phpunit.xml tests", + "test-file": "php ./vendor/bin/phpunit --configuration phpunit.xml --no-coverage --filter", + "test-no-coverage": "php ./vendor/bin/phpunit --configuration phpunit.xml --no-coverage tests", "tests": [ "@test", "@mutation-test" diff --git a/src/Internal/EagerPipeline.php b/src/Internal/EagerPipeline.php index da9a945..e1e2882 100644 --- a/src/Internal/EagerPipeline.php +++ b/src/Internal/EagerPipeline.php @@ -23,7 +23,8 @@ public static function from(iterable $source): EagerPipeline public static function fromClosure(Closure $factory): EagerPipeline { - $elements = iterator_to_array($factory()); + $result = $factory(); + $elements = is_array($result) ? $result : iterator_to_array($result); return new EagerPipeline(materialization: Materialization::from(source: $elements, stages: [])); } diff --git a/src/Internal/Operations/Transforming/Filter.php b/src/Internal/Operations/Transforming/Filter.php index 7e07862..1734a72 100644 --- a/src/Internal/Operations/Transforming/Filter.php +++ b/src/Internal/Operations/Transforming/Filter.php @@ -14,7 +14,7 @@ private function __construct(?Closure ...$predicates) { - $filtered = array_filter($predicates); + $filtered = array_filter($predicates, static fn(?Closure $predicate): bool => !is_null($predicate)); $this->compiledPredicate = $filtered === [] ? static fn(mixed $value, mixed $key): bool => (bool)$value