From 6e487c421e35639e81f87b44ffe3f02ed7ae880f Mon Sep 17 00:00:00 2001 From: Alexandre Gomes Gaigalas Date: Thu, 19 Mar 2026 16:44:13 -0300 Subject: [PATCH] Add opt-in filtered column persist support to AbstractMapper Introduce filterColumns() helper that restricts persisted columns to those listed in Filtered::$filters (plus the PK). Concrete mappers call it during flush to enable partial writes; InMemoryMapper skips it, preserving full-column persist for backends without partial support. --- src/AbstractMapper.php | 24 ++++++++ tests/AbstractMapperTest.php | 105 +++++++++++++++++++++++++++++++++++ tests/InMemoryMapper.php | 13 ++++- 3 files changed, 139 insertions(+), 3 deletions(-) diff --git a/src/AbstractMapper.php b/src/AbstractMapper.php index 4658b6c..eb19a92 100644 --- a/src/AbstractMapper.php +++ b/src/AbstractMapper.php @@ -8,6 +8,9 @@ use Respect\Data\Collections\Filtered; use SplObjectStorage; +use function array_flip; +use function array_intersect_key; + abstract class AbstractMapper { /** @var SplObjectStorage */ @@ -105,6 +108,27 @@ public function registerCollection(string $alias, Collection $collection): void abstract protected function defaultHydrator(Collection $collection): Hydrator; + /** + * @param array $columns + * + * @return array + */ + protected function filterColumns(array $columns, Collection $collection): array + { + if ( + !$collection instanceof Filtered + || !$collection->filters + || $collection->identifierOnly + || $collection->name === null + ) { + return $columns; + } + + $pk = $this->style->identifier($collection->name); + + return array_intersect_key($columns, array_flip([...$collection->filters, $pk])); + } + protected function resolveHydrator(Collection $collection): Hydrator { return $collection->hydrator ?? $this->defaultHydrator($collection); diff --git a/tests/AbstractMapperTest.php b/tests/AbstractMapperTest.php index b182ea9..bca34d4 100644 --- a/tests/AbstractMapperTest.php +++ b/tests/AbstractMapperTest.php @@ -369,4 +369,109 @@ public function filteredWithoutNextFallsBackToNormalPersist(): void $fetched = $mapper->post->fetch(); $this->assertEquals('Direct', $fetched->title); } + + #[Test] + public function filteredUpdatePersistsOnlyFilteredColumns(): void + { + $mapper = new InMemoryMapper(); + $mapper->seed('post', [ + ['id' => 1, 'title' => 'Original', 'text' => 'Body'], + ]); + + $mapper->postTitles = Filtered::post('title'); + $post = $mapper->postTitles()->fetch(); + $this->assertIsObject($post); + + $mapper->entityFactory->set($post, 'title', 'Changed'); + $mapper->postTitles()->persist($post); + $mapper->flush(); + + $fetched = $mapper->post->fetch(); + $this->assertEquals('Changed', $mapper->entityFactory->get($fetched, 'title')); + $this->assertEquals('Body', $mapper->entityFactory->get($fetched, 'text')); + } + + #[Test] + public function filteredInsertPersistsOnlyFilteredColumns(): void + { + $mapper = new InMemoryMapper(); + $mapper->seed('post', []); + + $mapper->postTitles = Filtered::post('title'); + $post = new stdClass(); + $post->id = 1; + $post->title = 'Partial'; + $post->text = 'Should not persist'; + $mapper->postTitles()->persist($post); + $mapper->flush(); + + $fetched = $mapper->post->fetch(); + $this->assertEquals('Partial', $mapper->entityFactory->get($fetched, 'title')); + $this->assertNull($mapper->entityFactory->get($fetched, 'text')); + } + + #[Test] + public function filterColumnsPassesThroughForPlainCollection(): void + { + $mapper = new InMemoryMapper(); + $mapper->seed('post', [ + ['id' => 1, 'title' => 'Original', 'text' => 'Body'], + ]); + + $post = $mapper->post->fetch(); + $this->assertIsObject($post); + + $mapper->entityFactory->set($post, 'title', 'Changed'); + $mapper->entityFactory->set($post, 'text', 'New Body'); + $mapper->post->persist($post); + $mapper->flush(); + + $fetched = $mapper->post->fetch(); + $this->assertEquals('Changed', $mapper->entityFactory->get($fetched, 'title')); + $this->assertEquals('New Body', $mapper->entityFactory->get($fetched, 'text')); + } + + #[Test] + public function filterColumnsPassesThroughForEmptyFilters(): void + { + $mapper = new InMemoryMapper(); + $mapper->seed('post', [ + ['id' => 1, 'title' => 'Original', 'text' => 'Body'], + ]); + + $mapper->allPosts = Filtered::post(); + $post = $mapper->allPosts()->fetch(); + $this->assertIsObject($post); + + $mapper->entityFactory->set($post, 'title', 'Changed'); + $mapper->entityFactory->set($post, 'text', 'New Body'); + $mapper->allPosts()->persist($post); + $mapper->flush(); + + $fetched = $mapper->post->fetch(); + $this->assertEquals('Changed', $mapper->entityFactory->get($fetched, 'title')); + $this->assertEquals('New Body', $mapper->entityFactory->get($fetched, 'text')); + } + + #[Test] + public function filterColumnsPassesThroughForIdentifierOnly(): void + { + $mapper = new InMemoryMapper(); + $mapper->seed('post', [ + ['id' => 1, 'title' => 'Original', 'text' => 'Body'], + ]); + + $mapper->postIds = Filtered::post(Filtered::IDENTIFIER_ONLY); + $post = $mapper->postIds()->fetch(); + $this->assertIsObject($post); + + $mapper->entityFactory->set($post, 'title', 'Changed'); + $mapper->entityFactory->set($post, 'text', 'New Body'); + $mapper->postIds()->persist($post); + $mapper->flush(); + + $fetched = $mapper->post->fetch(); + $this->assertEquals('Changed', $mapper->entityFactory->get($fetched, 'title')); + $this->assertEquals('New Body', $mapper->entityFactory->get($fetched, 'text')); + } } diff --git a/tests/InMemoryMapper.php b/tests/InMemoryMapper.php index 57c2165..ae90ef8 100644 --- a/tests/InMemoryMapper.php +++ b/tests/InMemoryMapper.php @@ -8,6 +8,7 @@ use Respect\Data\Hydrators\Nested; use function array_filter; +use function array_merge; use function array_values; use function is_array; use function reset; @@ -56,7 +57,10 @@ public function flush(): void $collection = $this->tracked[$entity]; $tableName = (string) $collection->name; $pk = $this->style->identifier($tableName); - $row = $this->entityFactory->extractProperties($entity); + $row = $this->filterColumns( + $this->entityFactory->extractProperties($entity), + $collection, + ); if (!isset($row[$pk])) { ++$this->lastInsertId; @@ -76,11 +80,14 @@ public function flush(): void $tableName = (string) $collection->name; $pk = $this->style->identifier($tableName); $pkValue = $this->entityFactory->get($entity, $pk); - $row = $this->entityFactory->extractProperties($entity); + $row = $this->filterColumns( + $this->entityFactory->extractProperties($entity), + $collection, + ); foreach ($this->tables[$tableName] as $index => $existing) { if (isset($existing[$pk]) && $existing[$pk] == $pkValue) { - $this->tables[$tableName][$index] = $row; + $this->tables[$tableName][$index] = array_merge($existing, $row); break; }