diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml deleted file mode 100644 index 5bf8ce5..0000000 --- a/.github/workflows/claude-code-review.yml +++ /dev/null @@ -1,78 +0,0 @@ -name: Claude Code Review - -on: - pull_request: - types: [opened, synchronize] - # Optional: Only run on specific file changes - # paths: - # - "src/**/*.ts" - # - "src/**/*.tsx" - # - "src/**/*.js" - # - "src/**/*.jsx" - -jobs: - claude-review: - # Optional: Filter by PR author - # if: | - # github.event.pull_request.user.login == 'external-contributor' || - # github.event.pull_request.user.login == 'new-developer' || - # github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' - - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: read - issues: read - id-token: write - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 1 - - - name: Run Claude Code Review - id: claude-review - uses: anthropics/claude-code-action@beta - with: - claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - - # Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4) - # model: "claude-opus-4-20250514" - - # Direct prompt for automated review (no @claude mention needed) - direct_prompt: | - Please review this pull request and provide feedback on: - - Code quality and best practices - - Potential bugs or issues - - Performance considerations - - Security concerns - - Test coverage - - Be constructive and helpful in your feedback. - - # Optional: Use sticky comments to make Claude reuse the same comment on subsequent pushes to the same PR - # use_sticky_comment: true - - # Optional: Customize review based on file types - # direct_prompt: | - # Review this PR focusing on: - # - For TypeScript files: Type safety and proper interface usage - # - For API endpoints: Security, input validation, and error handling - # - For React components: Performance, accessibility, and best practices - # - For tests: Coverage, edge cases, and test quality - - # Optional: Different prompts for different authors - # direct_prompt: | - # ${{ github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' && - # 'Welcome! Please review this PR from a first-time contributor. Be encouraging and provide detailed explanations for any suggestions.' || - # 'Please provide a thorough code review focusing on our coding standards and best practices.' }} - - # Optional: Add specific tools for running tests or linting - # allowed_tools: "Bash(npm run test),Bash(npm run lint),Bash(npm run typecheck)" - - # Optional: Skip review for certain conditions - # if: | - # !contains(github.event.pull_request.title, '[skip-review]') && - # !contains(github.event.pull_request.title, '[WIP]') - diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml index f17e42e..38dadbc 100644 --- a/.github/workflows/phpstan.yml +++ b/.github/workflows/phpstan.yml @@ -68,4 +68,4 @@ jobs: key: "phpstan-result-cache-${{ github.run_id }}" - name: Run PHPUnit tests - run: vendor/bin/phpunit --configuration=phpunit.xml.dist --testdox --colors=always + run: php -d zend.assertions=1 vendor/bin/phpunit --configuration=phpunit.xml.dist --testdox --colors=always diff --git a/src/Collection/ArrayList.php b/src/Collection/ArrayList.php index 499b163..8ef59dd 100644 --- a/src/Collection/ArrayList.php +++ b/src/Collection/ArrayList.php @@ -253,7 +253,7 @@ final public function firstOrFail(?Closure $closure = null) #[Override] final public function sole(?Closure $closure = null) { - $items = $closure === null ? new static($this->elements) : $this->filter($closure); + $items = $closure === null ? new static($this->elements) : $this->filterStrict($closure); $count = $items->count(); if ($count === 0) { @@ -337,17 +337,47 @@ final public function mapStrict(Closure $closure): static return new static(array_map($closure, $this->elements, $keys)); } + /** + * @template TFlatMapValue + * @param Closure(TValue,int): iterable $closure + * @return self + */ + #[Override] + final public function flatMap(Closure $closure): self + { + $result = []; + + foreach ($this->elements as $index => $item) { + $mapped = $closure($item, $index); + + foreach ($mapped as $subItem) { + $result[] = $subItem; + } + } + + return new self($result); + } + + /** + * @param Closure(TValue,int): bool $closure + * @return self + */ #[Override] - final public function filter(Closure $closure): static + final public function filter(Closure $closure): self { + return new self(array_filter($this->elements, $closure, ARRAY_FILTER_USE_BOTH)); + } + #[Override] + final public function filterStrict(Closure $closure): static + { return new static(array_filter($this->elements, $closure, ARRAY_FILTER_USE_BOTH)); } #[Override] final public function reject(Closure $closure): static { - return $this->filter(static fn ($value, $key) => !$closure($value, $key)); + return $this->filterStrict(static fn ($value, $key) => !$closure($value, $key)); } #[Override] diff --git a/src/Collection/List/IArrayList.php b/src/Collection/List/IArrayList.php index 6ed538a..28d8fea 100644 --- a/src/Collection/List/IArrayList.php +++ b/src/Collection/List/IArrayList.php @@ -125,6 +125,14 @@ public function merge(self $other): self; */ public function map(Closure $closure): self; + /** + * 各要素に関数を適用し、結果を平坦化したコレクションを返す + * @template TFlatMapValue + * @param Closure(TValue,int): iterable $closure + * @return self + */ + public function flatMap(Closure $closure): self; + /** * @param Closure(TValue,int): TValue $closure * @return static @@ -134,9 +142,17 @@ public function mapStrict(Closure $closure): static; /** * 与えられた真理判定に合格するすべての要素のコレクションを作成する。 * @param Closure(TValue,int): bool $closure + * @return self + */ + public function filter(Closure $closure): self; + + /** + * 与えられた真理判定に合格するすべての要素のコレクションを作成する。 + * (strict version - 正確な型を保持) + * @param Closure(TValue,int): bool $closure * @return static */ - public function filter(Closure $closure): static; + public function filterStrict(Closure $closure): static; /** * 与えられた真理判定に合格しないすべての要素のコレクションを作成する。 diff --git a/src/ValueObjectList.php b/src/ValueObjectList.php index 1014f90..c40f67f 100644 --- a/src/ValueObjectList.php +++ b/src/ValueObjectList.php @@ -27,7 +27,7 @@ public function has(IValueObject $element): bool */ public function remove(IValueObject $element): static { - return $this->filter(static fn (IValueObject $e) => !$e->equals($element)); + return $this->filterStrict(static fn (IValueObject $e) => !$e->equals($element)); } /** @@ -45,6 +45,6 @@ public function put(IValueObject $element): static */ public function diff(self $other): static { - return $this->filter(static fn (IValueObject $e) => !$other->has($e)); + return $this->filterStrict(static fn (IValueObject $e) => !$other->has($e)); } } diff --git a/tests/Unit/Collection/ArrayListTest.php b/tests/Unit/Collection/ArrayListTest.php index 7cb2301..8da63fc 100644 --- a/tests/Unit/Collection/ArrayListTest.php +++ b/tests/Unit/Collection/ArrayListTest.php @@ -467,4 +467,76 @@ public function sort関数で要素をソートしたコレクションが取得 // 元のコレクションは変更されない(イミュータブル) $this->assertEquals([3, 1, 4, 2, 5], $collection->toArray()); } + + #[Test] + public function filter関数でselfを返すことができる(): void + { + $collection = ArrayList::from([1, 2, 3, 4, 5]); + + $filtered = $collection->filter(static fn ($value) => $value % 2 === 0); + + // 戻り値がArrayListインスタンス(self)であることを確認 + $this->assertInstanceOf(ArrayList::class, $filtered); + $this->assertEquals([1 => 2, 3 => 4], $filtered->toArray()); + + // 元のコレクションは変更されない(イミュータブル) + $this->assertEquals([1, 2, 3, 4, 5], $collection->toArray()); + } + + #[Test] + public function filterStrict関数でstaticを返すことができる(): void + { + $collection = ArrayList::from([1, 2, 3, 4, 5]); + + $filtered = $collection->filterStrict(static fn ($value) => $value % 2 === 0); + + // 戻り値が正確な型(static)であることを確認 + $this->assertInstanceOf(ArrayList::class, $filtered); + $this->assertEquals([1 => 2, 3 => 4], $filtered->toArray()); + + // 元のコレクションは変更されない(イミュータブル) + $this->assertEquals([1, 2, 3, 4, 5], $collection->toArray()); + } + + #[Test] + public function flatMap関数で各要素を変換して平坦化できる(): void + { + // 基本的な変換(各数値を2倍にして配列に包む) + $collection = ArrayList::from([1, 2, 3]); + $mapped = $collection->flatMap(static fn ($value) => [$value * 2]); + + $this->assertInstanceOf(ArrayList::class, $mapped); + $this->assertEquals([2, 4, 6], $mapped->toArray()); + + // 各要素を複数の要素に展開 + $collection2 = ArrayList::from([1, 2, 3]); + $expanded = $collection2->flatMap(static fn ($value) => [$value, $value * 10]); + + $this->assertEquals([1, 10, 2, 20, 3, 30], $expanded->toArray()); + + // 空の配列を返す場合 + $collection3 = ArrayList::from([1, 2, 3]); + $filtered = $collection3->flatMap(static fn ($value) => $value % 2 === 0 ? [$value] : []); + + $this->assertEquals([2], $filtered->toArray()); + + // 2次元配列の平坦化(従来のflattenと同等の動作) + $collection4 = ArrayList::from([[1, 2], [3, 4], [5, 6]]); + $flattened = $collection4->flatMap(static fn ($array) => $array); + + $this->assertEquals([1, 2, 3, 4, 5, 6], $flattened->toArray()); + // 元のコレクションは変更されない(イミュータブル) + $this->assertEquals([1, 2, 3], $collection->toArray()); + + // objectの2次元配列の平坦化(従来のflattenと同等の動作) + $collection5 = ArrayList::from([ + ArrayList::from([1, 2]), + ArrayList::from([3, 4]), + ArrayList::from([5, 6]), + ]); + $flattenedObjects = $collection5->flatMap(static fn ($array) => $array); + $this->assertEquals([1, 2, 3, 4, 5, 6], $flattenedObjects->toArray()); + // 元のコレクションは変更されない(イミュータブル) + $this->assertEquals([ArrayList::from([1, 2]), ArrayList::from([3, 4]), ArrayList::from([5, 6])], $collection5->toArray()); + } }