diff --git a/docs/audit/PHASE10_AUDIT.md b/docs/audit/PHASE10_AUDIT.md new file mode 100644 index 0000000..37e4939 --- /dev/null +++ b/docs/audit/PHASE10_AUDIT.md @@ -0,0 +1,39 @@ +# PHASE 10 AUDIT: Error, Logging & Observability Boundaries + +## Scope +**Focus Areas:** +- `src/Exceptions/` +- `src/Generic/` (Repositories) +- `src/Generic/Support/` (Ops & Builders) +- `src/Base/` +- `docs/adr/ADR-012.md` +- `docs/adr/ADR-013.md` + +## ADR Compliance Matrix + +| ADR | Requirement | Status | Verification Notes | +| :--- | :--- | :--- | :--- | +| **ADR-012** | **Root Exception Mandatory** | ✅ **PASS** | `RepositoryException` is the root of all exceptions. | +| **ADR-012** | **Exception Taxonomy** | ✅ **PASS** | Specific exceptions (`QueryExecutionException`, `InvalidFilterException`, etc.) are implemented and used. | +| **ADR-012** | **No Driver Leaks** | ✅ **PASS** | All repositories wrap driver exceptions in `try-catch` blocks using `\Throwable`. | +| **ADR-013** | **No Mandatory Logging** | ✅ **PASS** | `BaseRepository` uses optional `NullLogger`. | +| **ADR-013** | **Ops Zero Logging** | ✅ **PASS** | No logging found in `*Ops` classes. | +| **ADR-013** | **No Side Effects** | ✅ **PASS** | Logging failures (if any) do not appear to crash flow. | +| **ADR-012** | **Deterministic Messages** | ✅ **PASS** | Exception messages are static and do not leak internal driver details. | + +## Findings + +### Post-Remediation Status +All initial findings have been addressed. +- **Taxonomy:** Full exception hierarchy from ADR-012 is implemented. +- **Leaks:** Repositories now catch `\Throwable` (or `\Exception` where appropriate and covered) to prevent leaks from drivers and helper classes. +- **Messages:** Error messages are sanitized (e.g., "Find operation failed.") while preserving the original exception chain. +- **Redis Safety:** `RedisSafetyException` and `UnsafeOperationException` are properly utilized. + +## Verdict +**STATUS: PASS** + +The codebase now strictly adheres to Phase 10 requirements regarding Error Taxonomy and Logging Boundaries. + +## Lock Recommendation +None. Proceed to Phase 11. diff --git a/src/Exceptions/DriverOperationException.php b/src/Exceptions/DriverOperationException.php new file mode 100644 index 0000000..f32761c --- /dev/null +++ b/src/Exceptions/DriverOperationException.php @@ -0,0 +1,20 @@ + + * @since 2025-12-19 + * @see https://www.maatify.dev + * @link https://github.com/Maatify/data-repository + * @note Distributed in the hope that it will be useful - WITHOUT WARRANTY. + */ + +namespace Maatify\DataRepository\Exceptions; + +class DriverOperationException extends RepositoryException +{ +} diff --git a/src/Exceptions/HydrationException.php b/src/Exceptions/HydrationException.php new file mode 100644 index 0000000..04dd1fc --- /dev/null +++ b/src/Exceptions/HydrationException.php @@ -0,0 +1,20 @@ + + * @since 2025-12-19 + * @see https://www.maatify.dev + * @link https://github.com/Maatify/data-repository + * @note Distributed in the hope that it will be useful - WITHOUT WARRANTY. + */ + +namespace Maatify\DataRepository\Exceptions; + +class HydrationException extends RepositoryException +{ +} diff --git a/src/Exceptions/HydrationValidationException.php b/src/Exceptions/HydrationValidationException.php new file mode 100644 index 0000000..8711f44 --- /dev/null +++ b/src/Exceptions/HydrationValidationException.php @@ -0,0 +1,20 @@ + + * @since 2025-12-19 + * @see https://www.maatify.dev + * @link https://github.com/Maatify/data-repository + * @note Distributed in the hope that it will be useful - WITHOUT WARRANTY. + */ + +namespace Maatify\DataRepository\Exceptions; + +class HydrationValidationException extends HydrationException +{ +} diff --git a/src/Exceptions/InvalidAdapterException.php b/src/Exceptions/InvalidAdapterException.php new file mode 100644 index 0000000..b994a45 --- /dev/null +++ b/src/Exceptions/InvalidAdapterException.php @@ -0,0 +1,20 @@ + + * @since 2025-12-19 + * @see https://www.maatify.dev + * @link https://github.com/Maatify/data-repository + * @note Distributed in the hope that it will be useful - WITHOUT WARRANTY. + */ + +namespace Maatify\DataRepository\Exceptions; + +class InvalidAdapterException extends RepositoryConfigurationException +{ +} diff --git a/src/Exceptions/InvalidFilterException.php b/src/Exceptions/InvalidFilterException.php new file mode 100644 index 0000000..93a871e --- /dev/null +++ b/src/Exceptions/InvalidFilterException.php @@ -0,0 +1,20 @@ + + * @since 2025-12-19 + * @see https://www.maatify.dev + * @link https://github.com/Maatify/data-repository + * @note Distributed in the hope that it will be useful - WITHOUT WARRANTY. + */ + +namespace Maatify\DataRepository\Exceptions; + +class InvalidFilterException extends RepositoryException +{ +} diff --git a/src/Exceptions/InvalidPaginationException.php b/src/Exceptions/InvalidPaginationException.php new file mode 100644 index 0000000..9b5d776 --- /dev/null +++ b/src/Exceptions/InvalidPaginationException.php @@ -0,0 +1,20 @@ + + * @since 2025-12-19 + * @see https://www.maatify.dev + * @link https://github.com/Maatify/data-repository + * @note Distributed in the hope that it will be useful - WITHOUT WARRANTY. + */ + +namespace Maatify\DataRepository\Exceptions; + +class InvalidPaginationException extends RepositoryException +{ +} diff --git a/src/Exceptions/QueryExecutionException.php b/src/Exceptions/QueryExecutionException.php new file mode 100644 index 0000000..6bcc780 --- /dev/null +++ b/src/Exceptions/QueryExecutionException.php @@ -0,0 +1,20 @@ + + * @since 2025-12-19 + * @see https://www.maatify.dev + * @link https://github.com/Maatify/data-repository + * @note Distributed in the hope that it will be useful - WITHOUT WARRANTY. + */ + +namespace Maatify\DataRepository\Exceptions; + +class QueryExecutionException extends RepositoryException +{ +} diff --git a/src/Exceptions/RedisSafetyLimitExceededException.php b/src/Exceptions/RedisSafetyLimitExceededException.php new file mode 100644 index 0000000..5660263 --- /dev/null +++ b/src/Exceptions/RedisSafetyLimitExceededException.php @@ -0,0 +1,20 @@ + + * @since 2025-12-19 + * @see https://www.maatify.dev + * @link https://github.com/Maatify/data-repository + * @note Distributed in the hope that it will be useful - WITHOUT WARRANTY. + */ + +namespace Maatify\DataRepository\Exceptions; + +class RedisSafetyLimitExceededException extends RedisSafetyException +{ +} diff --git a/src/Exceptions/RepositoryConfigurationException.php b/src/Exceptions/RepositoryConfigurationException.php new file mode 100644 index 0000000..6fd2f4e --- /dev/null +++ b/src/Exceptions/RepositoryConfigurationException.php @@ -0,0 +1,20 @@ + + * @since 2025-12-19 + * @see https://www.maatify.dev + * @link https://github.com/Maatify/data-repository + * @note Distributed in the hope that it will be useful - WITHOUT WARRANTY. + */ + +namespace Maatify\DataRepository\Exceptions; + +class RepositoryConfigurationException extends RepositoryException +{ +} diff --git a/src/Exceptions/UnsafeOperationException.php b/src/Exceptions/UnsafeOperationException.php new file mode 100644 index 0000000..bc175fa --- /dev/null +++ b/src/Exceptions/UnsafeOperationException.php @@ -0,0 +1,20 @@ + + * @since 2025-12-19 + * @see https://www.maatify.dev + * @link https://github.com/Maatify/data-repository + * @note Distributed in the hope that it will be useful - WITHOUT WARRANTY. + */ + +namespace Maatify\DataRepository\Exceptions; + +class UnsafeOperationException extends RepositoryException +{ +} diff --git a/src/Generic/GenericMongoRepository.php b/src/Generic/GenericMongoRepository.php index d5edf83..0d53dee 100644 --- a/src/Generic/GenericMongoRepository.php +++ b/src/Generic/GenericMongoRepository.php @@ -15,7 +15,12 @@ namespace Maatify\DataRepository\Generic; +use InvalidArgumentException; use Maatify\DataRepository\Base\BaseMongoRepository; +use Maatify\DataRepository\Exceptions\DriverOperationException; +use Maatify\DataRepository\Exceptions\InvalidFilterException; +use Maatify\DataRepository\Exceptions\InvalidPaginationException; +use Maatify\DataRepository\Exceptions\QueryExecutionException; use Maatify\DataRepository\Exceptions\RepositoryException; use Maatify\DataRepository\Generic\Support\FilterUtils; use Maatify\DataRepository\Generic\Support\LimitOffsetValidator; @@ -52,8 +57,8 @@ public function find(int|string $id): ?array $result = $this->getCollectionObj()->findOne($filter); return $this->getMongoOps()->toArray($result); - } catch (\Exception $e) { - throw new RepositoryException('Find failed: ' . $e->getMessage(), 0, $e); + } catch (\Throwable $e) { + throw new QueryExecutionException('Find operation failed.', 0, $e); } } @@ -83,8 +88,10 @@ public function findBy(array $filters, ?array $orderBy = null, ?int $limit = nul $cursor = $this->getCollectionObj()->find($normalizedFilters, $options); return $this->getMongoOps()->cursorToArray($cursor); - } catch (\Exception $e) { - throw new RepositoryException('FindBy failed: ' . $e->getMessage(), 0, $e); + } catch (InvalidArgumentException $e) { + throw new InvalidFilterException('Invalid filter configuration.', 0, $e); + } catch (\Throwable $e) { + throw new QueryExecutionException('FindBy operation failed.', 0, $e); } } @@ -103,8 +110,10 @@ public function findOneBy(array $filters): ?array $result = $this->getCollectionObj()->findOne($normalizedFilters); return $this->getMongoOps()->toArray($result); - } catch (\Exception $e) { - throw new RepositoryException('FindOneBy failed: ' . $e->getMessage(), 0, $e); + } catch (InvalidArgumentException $e) { + throw new InvalidFilterException('Invalid filter configuration.', 0, $e); + } catch (\Throwable $e) { + throw new QueryExecutionException('FindOneBy operation failed.', 0, $e); } } @@ -128,8 +137,10 @@ public function count(array $filters = []): int $normalizedFilters = FilterUtils::buildMongoFilter($filters); return $this->getCollectionObj()->countDocuments($normalizedFilters); - } catch (\Exception $e) { - throw new RepositoryException('Count failed: ' . $e->getMessage(), 0, $e); + } catch (InvalidArgumentException $e) { + throw new InvalidFilterException('Invalid filter configuration.', 0, $e); + } catch (\Throwable $e) { + throw new QueryExecutionException('Count operation failed.', 0, $e); } } @@ -145,15 +156,15 @@ public function insert(array $data): int|string $normalizedId = $this->getMongoOps()->normalizeInsertedId($id); if ($normalizedId === '') { - throw new RepositoryException('Insert failed: received invalid ID type from driver.'); + throw new DriverOperationException('Insert failed: received invalid ID type from driver.'); } return $normalizedId; - } catch (\Exception $e) { + } catch (\Throwable $e) { if ($e instanceof RepositoryException) { throw $e; } - throw new RepositoryException('Insert failed: ' . $e->getMessage(), 0, $e); + throw new DriverOperationException('Insert operation failed.', 0, $e); } } @@ -167,8 +178,8 @@ public function update(int|string $id, array $data): bool $result = $this->getCollectionObj()->updateOne($filter, ['$set' => $data]); return $result->getMatchedCount() > 0; - } catch (\Exception $e) { - throw new RepositoryException('Update failed: ' . $e->getMessage(), 0, $e); + } catch (\Throwable $e) { + throw new DriverOperationException('Update operation failed.', 0, $e); } } @@ -182,8 +193,8 @@ public function delete(int|string $id): bool $result = $this->getCollectionObj()->deleteOne($filter); return $result->getDeletedCount() > 0; - } catch (\Exception $e) { - throw new RepositoryException('Delete failed: ' . $e->getMessage(), 0, $e); + } catch (\Throwable $e) { + throw new DriverOperationException('Delete operation failed.', 0, $e); } } @@ -275,16 +286,27 @@ public function paginateBy(array $filters, int $page = 1, int $perPage = 10, ?ar $perPage = 10; } - $total = $this->count($filters); - $offset = ($page - 1) * $perPage; + try { + $total = $this->count($filters); + $offset = ($page - 1) * $perPage; - LimitOffsetValidator::validate($perPage, $offset); + LimitOffsetValidator::validate($perPage, $offset); - $data = $this->findBy($filters, $orderBy, $perPage, $offset); + $data = $this->findBy($filters, $orderBy, $perPage, $offset); - $pagination = PaginationHelper::buildMeta($total, $page, $perPage); + $pagination = PaginationHelper::buildMeta($total, $page, $perPage); - return new PaginationResultDTO($data, $pagination); + return new PaginationResultDTO($data, $pagination); + } catch (RepositoryException $e) { + // Re-throw RepositoryException subclasses (like InvalidPaginationException from Validator) as-is + // to avoid misclassification. + throw $e; + } catch (InvalidArgumentException $e) { + throw new InvalidPaginationException('Invalid pagination parameters.', 0, $e); + } catch (\Throwable $e) { + // Wrap unexpected errors (Driver/Query) in QueryExecutionException + throw new QueryExecutionException('Pagination failed.', 0, $e); + } } } diff --git a/src/Generic/GenericMySQLRepository.php b/src/Generic/GenericMySQLRepository.php index 6481426..5713b9e 100644 --- a/src/Generic/GenericMySQLRepository.php +++ b/src/Generic/GenericMySQLRepository.php @@ -15,7 +15,11 @@ namespace Maatify\DataRepository\Generic; +use InvalidArgumentException; use Maatify\DataRepository\Base\BaseMySQLRepository; +use Maatify\DataRepository\Exceptions\InvalidFilterException; +use Maatify\DataRepository\Exceptions\InvalidPaginationException; +use Maatify\DataRepository\Exceptions\QueryExecutionException; use Maatify\DataRepository\Exceptions\RepositoryException; use Maatify\DataRepository\Generic\Support\FilterUtils; use Maatify\DataRepository\Generic\Support\LimitOffsetValidator; @@ -54,8 +58,8 @@ public function find(int|string $id): ?array $result = $stmt->fetch(PDO::FETCH_ASSOC); return $result === false ? null : $result; - } catch (\PDOException $e) { - throw new RepositoryException('Find failed: ' . $e->getMessage(), 0, $e); + } catch (\Throwable $e) { + throw new QueryExecutionException('Find operation failed.', 0, $e); } } @@ -68,30 +72,32 @@ public function find(int|string $id): ?array */ public function findBy(array $filters, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array { - [$where, $params] = $this->buildWhereClause($filters); + try { + [$where, $params] = $this->buildWhereClause($filters); - $sql = "SELECT * FROM `{$this->tableName}` {$where}"; + $sql = "SELECT * FROM `{$this->tableName}` {$where}"; - if (! empty($orderBy)) { - $sql .= ' ' . OrderUtils::buildSqlOrderBy($orderBy); - } + if (! empty($orderBy)) { + $sql .= ' ' . OrderUtils::buildSqlOrderBy($orderBy); + } - if ($limit !== null) { - $sql .= ' LIMIT ' . (int)$limit; - } + if ($limit !== null) { + $sql .= ' LIMIT ' . (int)$limit; + } - if ($offset !== null) { - $sql .= ' OFFSET ' . (int)$offset; - } + if ($offset !== null) { + $sql .= ' OFFSET ' . (int)$offset; + } - try { $stmt = $this->getPdo()->prepare($sql); $stmt->execute($params); /** @var array> $result */ $result = $stmt->fetchAll(PDO::FETCH_ASSOC); return $result; - } catch (\PDOException $e) { - throw new RepositoryException('FindBy failed: ' . $e->getMessage(), 0, $e); + } catch (InvalidArgumentException $e) { + throw new InvalidFilterException('Invalid filter configuration.', 0, $e); + } catch (\Throwable $e) { + throw new QueryExecutionException('FindBy operation failed.', 0, $e); } } @@ -132,8 +138,10 @@ public function count(array $filters = []): int $stmt->execute($params); return (int)$stmt->fetchColumn(); - } catch (\PDOException $e) { - throw new RepositoryException('Count failed: ' . $e->getMessage(), 0, $e); + } catch (InvalidArgumentException $e) { + throw new InvalidFilterException('Invalid filter configuration.', 0, $e); + } catch (\Throwable $e) { + throw new QueryExecutionException('Count operation failed.', 0, $e); } } @@ -157,8 +165,8 @@ public function insert(array $data): int|string $pdo->prepare($sql)->execute($data); return $this->getMysqlOps()->lastInsertId(); - } catch (\PDOException $e) { - throw new RepositoryException('Insert failed: ' . $e->getMessage(), 0, $e); + } catch (\Throwable $e) { + throw new QueryExecutionException('Insert operation failed.', 0, $e); } } @@ -189,8 +197,8 @@ public function update(int|string $id, array $data): bool $stmt = $this->getPdo()->prepare($sql); return $stmt->execute($data); - } catch (\PDOException $e) { - throw new RepositoryException('Update failed: ' . $e->getMessage(), 0, $e); + } catch (\Throwable $e) { + throw new QueryExecutionException('Update operation failed.', 0, $e); } } @@ -205,8 +213,8 @@ public function delete(int|string $id): bool $stmt->bindValue(':id', $id); return $stmt->execute(); - } catch (\PDOException $e) { - throw new RepositoryException('Delete failed: ' . $e->getMessage(), 0, $e); + } catch (\Throwable $e) { + throw new QueryExecutionException('Delete operation failed.', 0, $e); } } @@ -288,15 +296,23 @@ public function paginateBy(array $filters, int $page = 1, int $perPage = 10, ?ar $perPage = 10; } - $total = $this->count($filters); - $offset = ($page - 1) * $perPage; + try { + $total = $this->count($filters); + $offset = ($page - 1) * $perPage; - LimitOffsetValidator::validate($perPage, $offset); + LimitOffsetValidator::validate($perPage, $offset); - $data = $this->findBy($filters, $orderBy, $perPage, $offset); + $data = $this->findBy($filters, $orderBy, $perPage, $offset); - $pagination = PaginationHelper::buildMeta($total, $page, $perPage); + $pagination = PaginationHelper::buildMeta($total, $page, $perPage); - return new PaginationResultDTO($data, $pagination); + return new PaginationResultDTO($data, $pagination); + } catch (RepositoryException $e) { + throw $e; + } catch (InvalidArgumentException $e) { + throw new InvalidPaginationException('Invalid pagination parameters.', 0, $e); + } catch (\Throwable $e) { + throw new QueryExecutionException('Pagination failed.', 0, $e); + } } } diff --git a/src/Generic/GenericRedisRepository.php b/src/Generic/GenericRedisRepository.php index de5b827..3ff2fa5 100644 --- a/src/Generic/GenericRedisRepository.php +++ b/src/Generic/GenericRedisRepository.php @@ -15,8 +15,13 @@ namespace Maatify\DataRepository\Generic; +use InvalidArgumentException; use Maatify\DataRepository\Base\BaseRedisRepository; +use Maatify\DataRepository\Exceptions\DriverOperationException; +use Maatify\DataRepository\Exceptions\InvalidFilterException; +use Maatify\DataRepository\Exceptions\InvalidPaginationException; use Maatify\DataRepository\Exceptions\RepositoryException; +use Maatify\DataRepository\Exceptions\UnsafeOperationException; use Maatify\DataRepository\Generic\Support\LimitOffsetValidator; use Maatify\DataRepository\Generic\Support\RedisOps; use Maatify\DataRepository\Generic\Support\Safety\RedisSafetyConfig; @@ -62,8 +67,8 @@ public function find(int|string $id): ?array /** @var array $decoded */ return $decoded; - } catch (\Exception $e) { - throw new RepositoryException('Find failed: ' . $e->getMessage(), 0, $e); + } catch (\Throwable $e) { + throw new DriverOperationException('Find operation failed.', 0, $e); } } @@ -73,25 +78,25 @@ public function find(int|string $id): ?array public function insert(array $data): int|string { if (! isset($data['id'])) { - throw new RepositoryException("Generic Redis Insert requires 'id' in data payload."); + throw new UnsafeOperationException("Generic Redis Insert requires 'id' in data payload."); } $id = $data['id']; if (! is_int($id) && ! is_string($id)) { - throw new RepositoryException("Generic Redis Insert 'id' must be int|string."); + throw new UnsafeOperationException("Generic Redis Insert 'id' must be int|string."); } $key = $this->getKey($id); $payload = json_encode($data); if ($payload === false) { - throw new RepositoryException('Failed to JSON-encode data for Redis insert.'); + throw new UnsafeOperationException('Failed to JSON-encode data for Redis insert.'); } try { $this->getRedisOps()->set($key, $payload); - } catch (\Exception $e) { - throw new RepositoryException('Insert failed: ' . $e->getMessage(), 0, $e); + } catch (\Throwable $e) { + throw new DriverOperationException('Insert operation failed.', 0, $e); } return $id; @@ -112,13 +117,13 @@ public function update(int|string $id, array $data): bool $payload = json_encode($merged); if ($payload === false) { - throw new RepositoryException('Failed to JSON-encode data for Redis update.'); + throw new UnsafeOperationException('Failed to JSON-encode data for Redis update.'); } try { return $this->getRedisOps()->set($this->getKey($id), $payload); - } catch (\Exception $e) { - throw new RepositoryException('Update failed: ' . $e->getMessage(), 0, $e); + } catch (\Throwable $e) { + throw new DriverOperationException('Update operation failed.', 0, $e); } } @@ -129,8 +134,8 @@ public function delete(int|string $id): bool { try { return $this->getRedisOps()->del($this->getKey($id)) > 0; - } catch (\Exception $e) { - throw new RepositoryException('Delete failed: ' . $e->getMessage(), 0, $e); + } catch (\Throwable $e) { + throw new DriverOperationException('Delete operation failed.', 0, $e); } } @@ -143,30 +148,37 @@ public function delete(int|string $id): bool */ public function findBy(array $filters, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array { - // 1. Fetch all items (inefficient for large sets, but required for in-memory filtering) - $all = $this->findAll(); - - // 2. Filter - $filtered = []; - foreach ($all as $item) { - if ($this->matches($item, $filters)) { - $filtered[] = $item; + try { + // 1. Fetch all items (inefficient for large sets, but required for in-memory filtering) + $all = $this->findAll(); + + // 2. Filter + $filtered = []; + foreach ($all as $item) { + if ($this->matches($item, $filters)) { + $filtered[] = $item; + } } - } - // 3. Sort - if ($orderBy) { - $filtered = Support\OrderUtils::sortArray($filtered, $orderBy); - } + // 3. Sort + if ($orderBy) { + $filtered = Support\OrderUtils::sortArray($filtered, $orderBy); + } - // 4. Limit/Offset - if ($limit !== null || $offset !== null) { - $offset = $offset ?? 0; - $limit = $limit ?? count($filtered); // if limit is null, take all - $filtered = array_slice($filtered, $offset, $limit); - } + // 4. Limit/Offset + if ($limit !== null || $offset !== null) { + $offset = $offset ?? 0; + $limit = $limit ?? count($filtered); // if limit is null, take all + $filtered = array_slice($filtered, $offset, $limit); + } - return array_values($filtered); // Re-index array + return array_values($filtered); // Re-index array + } catch (RepositoryException $e) { + throw $e; + } catch (\Throwable $e) { + // Catch potential OrderUtils exceptions or array manipulation errors + throw new DriverOperationException('FindBy operation failed.', 0, $e); + } } /** @@ -185,30 +197,35 @@ public function findOneBy(array $filters): ?array /** * @return array> + * @throws RepositoryException */ public function findAll(): array { - /** @var array $keys */ - $keys = $this->getRedisOps()->keys($this->keyPrefix . '*'); + try { + /** @var array $keys */ + $keys = $this->getRedisOps()->keys($this->keyPrefix . '*'); + + /** @var array> $results */ + $results = []; + foreach ($keys as $key) { + $data = $this->getRedisOps()->get($key); + if ($data === null) { + continue; + } - /** @var array> $results */ - $results = []; - foreach ($keys as $key) { - $data = $this->getRedisOps()->get($key); - if ($data === null) { - continue; - } + $decoded = json_decode($data, true); + if (! is_array($decoded)) { + continue; + } - $decoded = json_decode($data, true); - if (! is_array($decoded)) { - continue; + /** @var array $decoded */ + $results[] = $decoded; } - /** @var array $decoded */ - $results[] = $decoded; + return $results; + } catch (\Throwable $e) { + throw new DriverOperationException('FindAll operation failed.', 0, $e); } - - return $results; } /** @@ -219,12 +236,17 @@ public function findAll(): array public function count(array $filters = []): int { if (! empty($filters)) { - throw new RepositoryException('Filtering count is not supported in Redis.'); + throw new UnsafeOperationException('Filtering count is not supported in Redis.'); } - /** @var array $keys */ - $keys = $this->getRedisOps()->keys($this->keyPrefix . '*'); - return count($keys); + try { + /** @var array $keys */ + $keys = $this->getRedisOps()->keys($this->keyPrefix . '*'); + + return count($keys); + } catch (\Throwable $e) { + throw new DriverOperationException('Count operation failed.', 0, $e); + } } private function getKey(int|string $id): string @@ -286,31 +308,39 @@ public function paginate(int $page = 1, int $perPage = 10, ?array $orderBy = nul $perPage = 10; } - /** @var array $keys */ - $keys = $this->getRedisOps()->keys($this->keyPrefix . '*'); - $total = count($keys); + try { + /** @var array $keys */ + $keys = $this->getRedisOps()->keys($this->keyPrefix . '*'); + $total = count($keys); - $offset = ($page - 1) * $perPage; + $offset = ($page - 1) * $perPage; - LimitOffsetValidator::validate($perPage, $offset); + LimitOffsetValidator::validate($perPage, $offset); - $pagedKeys = array_slice($keys, $offset, $perPage); + $pagedKeys = array_slice($keys, $offset, $perPage); - $data = []; - foreach ($pagedKeys as $key) { - $content = $this->getRedisOps()->get($key); - if ($content !== null) { - $decoded = json_decode($content, true); - if (is_array($decoded)) { - /** @var array $decoded */ - $data[] = $decoded; + $data = []; + foreach ($pagedKeys as $key) { + $content = $this->getRedisOps()->get($key); + if ($content !== null) { + $decoded = json_decode($content, true); + if (is_array($decoded)) { + /** @var array $decoded */ + $data[] = $decoded; + } } } - } - $pagination = PaginationHelper::buildMeta($total, $page, $perPage); + $pagination = PaginationHelper::buildMeta($total, $page, $perPage); - return new PaginationResultDTO($data, $pagination); + return new PaginationResultDTO($data, $pagination); + } catch (RepositoryException $e) { + throw $e; + } catch (InvalidArgumentException $e) { + throw new InvalidPaginationException('Invalid pagination parameters.', 0, $e); + } catch (\Throwable $e) { + throw new DriverOperationException('Paginate operation failed.', 0, $e); + } } /** @@ -331,20 +361,28 @@ public function paginateBy(array $filters, int $page = 1, int $perPage = 10, ?ar $perPage = 10; } - // Get all filtered items - $allFiltered = $this->findBy($filters, $orderBy); - $total = count($allFiltered); + try { + // Get all filtered items + $allFiltered = $this->findBy($filters, $orderBy); + $total = count($allFiltered); - // Slice for pagination - $offset = ($page - 1) * $perPage; + // Slice for pagination + $offset = ($page - 1) * $perPage; - LimitOffsetValidator::validate($perPage, $offset); + LimitOffsetValidator::validate($perPage, $offset); - $data = array_slice($allFiltered, $offset, $perPage); + $data = array_slice($allFiltered, $offset, $perPage); - $pagination = PaginationHelper::buildMeta($total, $page, $perPage); + $pagination = PaginationHelper::buildMeta($total, $page, $perPage); - return new PaginationResultDTO($data, $pagination); + return new PaginationResultDTO($data, $pagination); + } catch (RepositoryException $e) { + throw $e; + } catch (InvalidArgumentException $e) { + throw new InvalidPaginationException('Invalid pagination parameters.', 0, $e); + } catch (\Throwable $e) { + throw new DriverOperationException('PaginateBy operation failed.', 0, $e); + } } /** diff --git a/tests/Contracts/Repository/RepositoryInterfaceSignatureTest.php b/tests/Contracts/Repository/RepositoryInterfaceSignatureTest.php index d447f2c..03b51a4 100644 --- a/tests/Contracts/Repository/RepositoryInterfaceSignatureTest.php +++ b/tests/Contracts/Repository/RepositoryInterfaceSignatureTest.php @@ -21,10 +21,13 @@ use ReflectionMethod; use ReflectionParameter; use ReflectionType; +use ReflectionNamedType; use ReflectionUnionType; +use ReflectionIntersectionType; class RepositoryInterfaceSignatureTest extends TestCase { + /** @var ReflectionClass */ private ReflectionClass $ref; protected function setUp(): void @@ -135,9 +138,11 @@ public function testSetAdapterSignature(): void $this->assertSame(['adapter'], $this->paramNames($method)); $this->assertSame('static', (string) $method->getReturnType()); + $paramType = $method->getParameters()[0]->getType(); + $this->assertInstanceOf(ReflectionNamedType::class, $paramType); $this->assertSame( AdapterInterface::class, - $method->getParameters()[0]->getType()?->getName() + $paramType->getName() ); } @@ -157,16 +162,19 @@ private function paramNames(ReflectionMethod $method): array */ private function unionTypeNames(?ReflectionType $type): array { - if ($type instanceof ReflectionUnionType) { - $names = array_map( - static fn(ReflectionType $t): string => $t->getName(), - $type->getTypes() - ); - } else { - $names = [$type?->getName() ?? '']; + $names = []; + + if ($type instanceof ReflectionUnionType || $type instanceof ReflectionIntersectionType) { + foreach ($type->getTypes() as $t) { + if ($t instanceof ReflectionNamedType) { + $names[] = $t->getName(); + } + } + } elseif ($type instanceof ReflectionNamedType) { + $names[] = $type->getName(); } - sort($names); // ← FIX + sort($names); return $names; } diff --git a/tests/Generic/Coverage/GenericMongoCoverageTest.php b/tests/Generic/Coverage/GenericMongoCoverageTest.php index 02337d4..976363e 100644 --- a/tests/Generic/Coverage/GenericMongoCoverageTest.php +++ b/tests/Generic/Coverage/GenericMongoCoverageTest.php @@ -16,6 +16,8 @@ namespace Maatify\DataRepository\Tests\Generic\Coverage; use Maatify\Common\Contracts\Adapter\AdapterInterface; +use Maatify\DataRepository\Exceptions\DriverOperationException; +use Maatify\DataRepository\Exceptions\QueryExecutionException; use Maatify\DataRepository\Exceptions\RepositoryException; use Maatify\DataRepository\Generic\GenericMongoRepository; use MongoDB\Collection; @@ -52,8 +54,8 @@ public function testFind(): void } }; - $this->expectException(RepositoryException::class); - $this->expectExceptionMessage('Collection name not defined'); + $this->expectException(QueryExecutionException::class); + $this->expectExceptionMessage('Find operation failed.'); $repo->testFind(); } @@ -79,8 +81,8 @@ public function testFind(): void } }; - $this->expectException(RepositoryException::class); - $this->expectExceptionMessage('Failed to retrieve MongoDB Collection'); + $this->expectException(QueryExecutionException::class); + $this->expectExceptionMessage('Find operation failed.'); $repo->testFind(); } @@ -109,8 +111,7 @@ protected function getCollection(string $name): mixed } }; - $this->expectException(RepositoryException::class); - // It might be "Insert failed" or a specific message from MongoOps + $this->expectException(DriverOperationException::class); $this->expectExceptionMessage('Insert failed'); $repo->insert(['a' => 1]); @@ -148,56 +149,56 @@ protected function getCollection(string $name): mixed try { $repo->find('123'); $this->fail('Expected exception'); - } catch (RepositoryException $e) { - $this->assertStringContainsString('Find failed', $e->getMessage()); + } catch (QueryExecutionException $e) { + $this->assertSame('Find operation failed.', $e->getMessage()); } // FindBy try { $repo->findBy([]); $this->fail('Expected exception'); - } catch (RepositoryException $e) { - $this->assertStringContainsString('FindBy failed', $e->getMessage()); + } catch (QueryExecutionException $e) { + $this->assertSame('FindBy operation failed.', $e->getMessage()); } // FindOneBy try { $repo->findOneBy([]); $this->fail('Expected exception'); - } catch (RepositoryException $e) { - $this->assertStringContainsString('FindOneBy failed', $e->getMessage()); + } catch (QueryExecutionException $e) { + $this->assertSame('FindOneBy operation failed.', $e->getMessage()); } // Count try { $repo->count(); $this->fail('Expected exception'); - } catch (RepositoryException $e) { - $this->assertStringContainsString('Count failed', $e->getMessage()); + } catch (QueryExecutionException $e) { + $this->assertSame('Count operation failed.', $e->getMessage()); } // Insert try { $repo->insert(['a' => 1]); $this->fail('Expected exception'); - } catch (RepositoryException $e) { - $this->assertStringContainsString('Insert failed', $e->getMessage()); + } catch (DriverOperationException $e) { + $this->assertSame('Insert operation failed.', $e->getMessage()); } // Update try { $repo->update('123', ['a' => 1]); $this->fail('Expected exception'); - } catch (RepositoryException $e) { - $this->assertStringContainsString('Update failed', $e->getMessage()); + } catch (DriverOperationException $e) { + $this->assertSame('Update operation failed.', $e->getMessage()); } // Delete try { $repo->delete('123'); $this->fail('Expected exception'); - } catch (RepositoryException $e) { - $this->assertStringContainsString('Delete failed', $e->getMessage()); + } catch (DriverOperationException $e) { + $this->assertSame('Delete operation failed.', $e->getMessage()); } } } diff --git a/tests/Generic/Coverage/GenericMySQLCoverageTest.php b/tests/Generic/Coverage/GenericMySQLCoverageTest.php index 37144b3..de5b5f4 100644 --- a/tests/Generic/Coverage/GenericMySQLCoverageTest.php +++ b/tests/Generic/Coverage/GenericMySQLCoverageTest.php @@ -17,6 +17,7 @@ use Maatify\Common\Contracts\Adapter\AdapterInterface; use Maatify\DataRepository\Exceptions\RepositoryException; +use Maatify\DataRepository\Exceptions\QueryExecutionException; use Maatify\DataRepository\Generic\GenericMySQLRepository; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -64,8 +65,8 @@ public function testGetPdoThrowsExceptionWhenDriverIsNotPdo(): void ->method('getDriver') ->willReturn(new \stdClass()); // Not PDO - $this->expectException(RepositoryException::class); - $this->expectExceptionMessage('GenericMySQLRepository requires a PDO driver or compatible wrapper.'); + $this->expectException(QueryExecutionException::class); + $this->expectExceptionMessage('Find operation failed.'); $this->repository->find(1); } @@ -112,49 +113,49 @@ public function testCrudMethodsThrowRepositoryExceptionOnPdoFailure(): void // Test Find try { $this->repository->find(1); - $this->fail('Expected RepositoryException not thrown'); - } catch (RepositoryException $e) { - $this->assertStringContainsString('Find failed', $e->getMessage()); + $this->fail('Expected QueryExecutionException not thrown'); + } catch (QueryExecutionException $e) { + $this->assertSame('Find operation failed.', $e->getMessage()); } // Test FindBy try { $this->repository->findBy(['col' => 'val']); - $this->fail('Expected RepositoryException not thrown'); - } catch (RepositoryException $e) { - $this->assertStringContainsString('FindBy failed', $e->getMessage()); + $this->fail('Expected QueryExecutionException not thrown'); + } catch (QueryExecutionException $e) { + $this->assertSame('FindBy operation failed.', $e->getMessage()); } // Test Count try { $this->repository->count(); - $this->fail('Expected RepositoryException not thrown'); - } catch (RepositoryException $e) { - $this->assertStringContainsString('Count failed', $e->getMessage()); + $this->fail('Expected QueryExecutionException not thrown'); + } catch (QueryExecutionException $e) { + $this->assertSame('Count operation failed.', $e->getMessage()); } // Test Insert try { $this->repository->insert(['col' => 'val']); - $this->fail('Expected RepositoryException not thrown'); - } catch (RepositoryException $e) { - $this->assertStringContainsString('Insert failed', $e->getMessage()); + $this->fail('Expected QueryExecutionException not thrown'); + } catch (QueryExecutionException $e) { + $this->assertSame('Insert operation failed.', $e->getMessage()); } // Test Update try { $this->repository->update(1, ['col' => 'val']); - $this->fail('Expected RepositoryException not thrown'); - } catch (RepositoryException $e) { - $this->assertStringContainsString('Update failed', $e->getMessage()); + $this->fail('Expected QueryExecutionException not thrown'); + } catch (QueryExecutionException $e) { + $this->assertSame('Update operation failed.', $e->getMessage()); } // Test Delete try { $this->repository->delete(1); - $this->fail('Expected RepositoryException not thrown'); - } catch (RepositoryException $e) { - $this->assertStringContainsString('Delete failed', $e->getMessage()); + $this->fail('Expected QueryExecutionException not thrown'); + } catch (QueryExecutionException $e) { + $this->assertSame('Delete operation failed.', $e->getMessage()); } } } diff --git a/tests/Generic/Coverage/GenericRedisCoverageTest.php b/tests/Generic/Coverage/GenericRedisCoverageTest.php index 3d442e8..ccf3976 100644 --- a/tests/Generic/Coverage/GenericRedisCoverageTest.php +++ b/tests/Generic/Coverage/GenericRedisCoverageTest.php @@ -17,6 +17,7 @@ use Maatify\Common\Contracts\Adapter\AdapterInterface; use Maatify\DataRepository\Exceptions\RepositoryException; +use Maatify\DataRepository\Exceptions\UnsafeOperationException; use Maatify\DataRepository\Generic\GenericRedisRepository; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -56,21 +57,21 @@ public function testCountWithFiltersThrowsException(): void // Phase 19 implementation only touched findBy/paginateBy logic. // Let's check the code: GenericRedisRepository::count() still has check: // if (! empty($filters)) { throw new RepositoryException... } - $this->expectException(RepositoryException::class); + $this->expectException(UnsafeOperationException::class); $this->expectExceptionMessage('Filtering count is not supported'); $this->repository->count(['a' => 1]); } public function testInsertThrowsExceptionIfIdMissing(): void { - $this->expectException(RepositoryException::class); + $this->expectException(UnsafeOperationException::class); $this->expectExceptionMessage("Generic Redis Insert requires 'id'"); $this->repository->insert(['name' => 'test']); } public function testInsertThrowsExceptionIfIdInvalidType(): void { - $this->expectException(RepositoryException::class); + $this->expectException(UnsafeOperationException::class); $this->expectExceptionMessage("Generic Redis Insert 'id' must be int|string"); $this->repository->insert(['id' => 1.5]); } diff --git a/tests/Generic/Exceptions/GenericMongoRepositoryExceptionTest.php b/tests/Generic/Exceptions/GenericMongoRepositoryExceptionTest.php index 10abc6b..8c1146e 100644 --- a/tests/Generic/Exceptions/GenericMongoRepositoryExceptionTest.php +++ b/tests/Generic/Exceptions/GenericMongoRepositoryExceptionTest.php @@ -16,7 +16,8 @@ namespace Maatify\DataRepository\Tests\Generic\Exceptions; use Maatify\Common\Contracts\Adapter\AdapterInterface; -use Maatify\DataRepository\Exceptions\RepositoryException; +use Maatify\DataRepository\Exceptions\DriverOperationException; +use Maatify\DataRepository\Exceptions\QueryExecutionException; use Maatify\DataRepository\Generic\GenericMongoRepository; use MongoDB\Collection; use PHPUnit\Framework\MockObject\MockObject; @@ -97,8 +98,8 @@ public function testFindThrowsRepositoryException(): void { $this->collection->method('findOne')->willThrowException(new \Exception('Simulated Mongo Error')); - $this->expectException(RepositoryException::class); - $this->expectExceptionMessage('Find failed: Simulated Mongo Error'); + $this->expectException(QueryExecutionException::class); + $this->expectExceptionMessage('Find operation failed.'); $this->repository->find('507f1f77bcf86cd799439011'); } @@ -107,8 +108,8 @@ public function testFindByThrowsRepositoryException(): void { $this->collection->method('find')->willThrowException(new \Exception('Simulated Mongo Error')); - $this->expectException(RepositoryException::class); - $this->expectExceptionMessage('FindBy failed: Simulated Mongo Error'); + $this->expectException(QueryExecutionException::class); + $this->expectExceptionMessage('FindBy operation failed.'); $this->repository->findBy(['status' => 1]); } @@ -117,8 +118,8 @@ public function testFindOneByThrowsRepositoryException(): void { $this->collection->method('findOne')->willThrowException(new \Exception('Simulated Mongo Error')); - $this->expectException(RepositoryException::class); - $this->expectExceptionMessage('FindOneBy failed: Simulated Mongo Error'); + $this->expectException(QueryExecutionException::class); + $this->expectExceptionMessage('FindOneBy operation failed.'); $this->repository->findOneBy(['status' => 1]); } @@ -127,8 +128,8 @@ public function testCountThrowsRepositoryException(): void { $this->collection->method('countDocuments')->willThrowException(new \Exception('Simulated Mongo Error')); - $this->expectException(RepositoryException::class); - $this->expectExceptionMessage('Count failed: Simulated Mongo Error'); + $this->expectException(QueryExecutionException::class); + $this->expectExceptionMessage('Count operation failed.'); $this->repository->count(); } @@ -137,8 +138,8 @@ public function testInsertThrowsRepositoryException(): void { $this->collection->method('insertOne')->willThrowException(new \Exception('Simulated Mongo Error')); - $this->expectException(RepositoryException::class); - $this->expectExceptionMessage('Insert failed: Simulated Mongo Error'); + $this->expectException(DriverOperationException::class); + $this->expectExceptionMessage('Insert operation failed.'); $this->repository->insert(['col' => 'val']); } @@ -147,8 +148,8 @@ public function testUpdateThrowsRepositoryException(): void { $this->collection->method('updateOne')->willThrowException(new \Exception('Simulated Mongo Error')); - $this->expectException(RepositoryException::class); - $this->expectExceptionMessage('Update failed: Simulated Mongo Error'); + $this->expectException(DriverOperationException::class); + $this->expectExceptionMessage('Update operation failed.'); $this->repository->update('507f1f77bcf86cd799439011', ['col' => 'val']); } @@ -157,8 +158,8 @@ public function testDeleteThrowsRepositoryException(): void { $this->collection->method('deleteOne')->willThrowException(new \Exception('Simulated Mongo Error')); - $this->expectException(RepositoryException::class); - $this->expectExceptionMessage('Delete failed: Simulated Mongo Error'); + $this->expectException(DriverOperationException::class); + $this->expectExceptionMessage('Delete operation failed.'); $this->repository->delete('507f1f77bcf86cd799439011'); } diff --git a/tests/Generic/Exceptions/GenericMySQLRepositoryExceptionTest.php b/tests/Generic/Exceptions/GenericMySQLRepositoryExceptionTest.php index 8f05df4..73641cc 100644 --- a/tests/Generic/Exceptions/GenericMySQLRepositoryExceptionTest.php +++ b/tests/Generic/Exceptions/GenericMySQLRepositoryExceptionTest.php @@ -16,7 +16,7 @@ namespace Maatify\DataRepository\Tests\Generic\Exceptions; use Maatify\Common\Contracts\Adapter\AdapterInterface; -use Maatify\DataRepository\Exceptions\RepositoryException; +use Maatify\DataRepository\Exceptions\QueryExecutionException; use Maatify\DataRepository\Generic\GenericMySQLRepository; use PDO; use PDOException; @@ -73,8 +73,8 @@ public function testFindThrowsRepositoryException(): void { $this->pdo->method('prepare')->willThrowException(new PDOException('Simulated PDO Error')); - $this->expectException(RepositoryException::class); - $this->expectExceptionMessage('Find failed: Simulated PDO Error'); + $this->expectException(QueryExecutionException::class); + $this->expectExceptionMessage('Find operation failed.'); $this->repository->find(1); } @@ -83,8 +83,8 @@ public function testFindByThrowsRepositoryException(): void { $this->pdo->method('prepare')->willThrowException(new PDOException('Simulated PDO Error')); - $this->expectException(RepositoryException::class); - $this->expectExceptionMessage('FindBy failed: Simulated PDO Error'); + $this->expectException(QueryExecutionException::class); + $this->expectExceptionMessage('FindBy operation failed.'); $this->repository->findBy(['id' => 1]); } @@ -93,8 +93,8 @@ public function testCountThrowsRepositoryException(): void { $this->pdo->method('prepare')->willThrowException(new PDOException('Simulated PDO Error')); - $this->expectException(RepositoryException::class); - $this->expectExceptionMessage('Count failed: Simulated PDO Error'); + $this->expectException(QueryExecutionException::class); + $this->expectExceptionMessage('Count operation failed.'); $this->repository->count(); } @@ -103,8 +103,8 @@ public function testInsertThrowsRepositoryException(): void { $this->pdo->method('prepare')->willThrowException(new PDOException('Simulated PDO Error')); - $this->expectException(RepositoryException::class); - $this->expectExceptionMessage('Insert failed: Simulated PDO Error'); + $this->expectException(QueryExecutionException::class); + $this->expectExceptionMessage('Insert operation failed.'); $this->repository->insert(['col' => 'val']); } @@ -113,8 +113,8 @@ public function testUpdateThrowsRepositoryException(): void { $this->pdo->method('prepare')->willThrowException(new PDOException('Simulated PDO Error')); - $this->expectException(RepositoryException::class); - $this->expectExceptionMessage('Update failed: Simulated PDO Error'); + $this->expectException(QueryExecutionException::class); + $this->expectExceptionMessage('Update operation failed.'); $this->repository->update(1, ['col' => 'val']); } @@ -123,8 +123,8 @@ public function testDeleteThrowsRepositoryException(): void { $this->pdo->method('prepare')->willThrowException(new PDOException('Simulated PDO Error')); - $this->expectException(RepositoryException::class); - $this->expectExceptionMessage('Delete failed: Simulated PDO Error'); + $this->expectException(QueryExecutionException::class); + $this->expectExceptionMessage('Delete operation failed.'); $this->repository->delete(1); } diff --git a/tests/Generic/Fake/GenericMySQLRepositoryFakeTest.php b/tests/Generic/Fake/GenericMySQLRepositoryFakeTest.php index 324fa7c..765ac0d 100644 --- a/tests/Generic/Fake/GenericMySQLRepositoryFakeTest.php +++ b/tests/Generic/Fake/GenericMySQLRepositoryFakeTest.php @@ -18,7 +18,7 @@ use Maatify\Common\Contracts\Adapter\AdapterInterface; use Maatify\DataRepository\Generic\GenericMySQLRepository; use PHPUnit\Framework\TestCase; -use InvalidArgumentException; +use Maatify\DataRepository\Exceptions\InvalidFilterException; class GenericMySQLRepositoryFakeTest extends TestCase { @@ -128,7 +128,7 @@ public function testCountAndUpdateEmptyPayload(): void public function testFindByThrowsOnInvalidColumns(): void { - $this->expectException(InvalidArgumentException::class); + $this->expectException(InvalidFilterException::class); $this->repo->findBy(['role' => 'admin', 'invalid column' => 'x']); } diff --git a/tests/Generic/NoSQL/MongoCastingRegressionTest.php b/tests/Generic/NoSQL/MongoCastingRegressionTest.php index 61b4388..be87ffd 100644 --- a/tests/Generic/NoSQL/MongoCastingRegressionTest.php +++ b/tests/Generic/NoSQL/MongoCastingRegressionTest.php @@ -28,8 +28,11 @@ */ class MongoCastingRegressionTest extends TestCase { - private object $collectionMock; - private AdapterInterface $adapterMock; + /** @var MockObject&\MongoDB\Collection */ + private $collectionMock; + /** @var MockObject&AdapterInterface */ + private $adapterMock; + /** @var GenericMongoRepository */ private GenericMongoRepository $repo; protected function setUp(): void @@ -39,13 +42,14 @@ protected function setUp(): void } // Mock Collection needed for instanceof check - /** @var MockObject&\MongoDB\Collection $collectionMock */ - $collectionMock = $this->createMock(\MongoDB\Collection::class); - $this->collectionMock = $collectionMock; + $this->collectionMock = $this->createMock(\MongoDB\Collection::class); // Mock Adapter $this->adapterMock = $this->createMock(AdapterInterface::class); $this->adapterMock->method('getDriver')->willReturn(new class ($this->collectionMock) { + /** + * @param \MongoDB\Collection&MockObject $collection + */ public function __construct(private object $collection) {} // Use variadic args to match any signature requirement for selectCollection public function selectCollection(mixed ...$args): object @@ -55,7 +59,15 @@ public function selectCollection(mixed ...$args): object }); // Instantiate Repo - $this->repo = new class ($this->adapterMock) extends GenericMongoRepository { + $this->repo = $this->createGenericMongoRepository($this->adapterMock); + } + + /** + * @return GenericMongoRepository + */ + private function createGenericMongoRepository(AdapterInterface $adapter): GenericMongoRepository + { + return new class ($adapter) extends GenericMongoRepository { protected string $collectionName = 'test_col'; protected function validateAdapter(): void {} // bypass }; diff --git a/tests/Legacy/Redis/GenericRedisRepositoryFakeTest.php b/tests/Legacy/Redis/GenericRedisRepositoryFakeTest.php index 5199ea2..ea5add8 100644 --- a/tests/Legacy/Redis/GenericRedisRepositoryFakeTest.php +++ b/tests/Legacy/Redis/GenericRedisRepositoryFakeTest.php @@ -257,24 +257,15 @@ public function __construct() } }; - // create a proxy that forces ReflectionException - $ops = new class ($driver) extends RedisOps { - public function keys(string $pattern): array - { - throw new \ReflectionException('forced'); - } - }; + // actual RedisOps implementation returns [] if reflection fails + // We cannot force ReflectionException on ReflectionObject constructor easily in a test + // without hacking standard library. + // Instead we rely on the fact that if a property doesn't exist, it returns empty. - try { - $result = $ops->keys('*'); - $this->fail('Exception should have been thrown inside overridden keys()'); - } catch (\ReflectionException) { - // actual RedisOps implementation returns [] if reflection fails - $driver2 = new class () {}; - $ops2 = new \Maatify\DataRepository\Generic\Support\RedisOps($driver2); + $driver2 = new class () {}; + $ops2 = new \Maatify\DataRepository\Generic\Support\RedisOps($driver2); - $this->assertSame([], $ops2->keys('*')); - } + $this->assertSame([], $ops2->keys('*')); }