Skip to content
Draft
39 changes: 39 additions & 0 deletions docs/audit/PHASE10_AUDIT.md
Original file line number Diff line number Diff line change
@@ -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.
20 changes: 20 additions & 0 deletions src/Exceptions/DriverOperationException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

/**
* @copyright ©2025 Maatify.dev
* @Library maatify/data-repository
* @Project maatify:data-repository
* @author Mohamed Abdulalim (megyptm) <mohamed@maatify.dev>
* @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
{
}
20 changes: 20 additions & 0 deletions src/Exceptions/HydrationException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

/**
* @copyright ©2025 Maatify.dev
* @Library maatify/data-repository
* @Project maatify:data-repository
* @author Mohamed Abdulalim (megyptm) <mohamed@maatify.dev>
* @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
{
}
20 changes: 20 additions & 0 deletions src/Exceptions/HydrationValidationException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

/**
* @copyright ©2025 Maatify.dev
* @Library maatify/data-repository
* @Project maatify:data-repository
* @author Mohamed Abdulalim (megyptm) <mohamed@maatify.dev>
* @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
{
}
20 changes: 20 additions & 0 deletions src/Exceptions/InvalidAdapterException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

/**
* @copyright ©2025 Maatify.dev
* @Library maatify/data-repository
* @Project maatify:data-repository
* @author Mohamed Abdulalim (megyptm) <mohamed@maatify.dev>
* @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
{
}
20 changes: 20 additions & 0 deletions src/Exceptions/InvalidFilterException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

/**
* @copyright ©2025 Maatify.dev
* @Library maatify/data-repository
* @Project maatify:data-repository
* @author Mohamed Abdulalim (megyptm) <mohamed@maatify.dev>
* @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
{
}
20 changes: 20 additions & 0 deletions src/Exceptions/InvalidPaginationException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

/**
* @copyright ©2025 Maatify.dev
* @Library maatify/data-repository
* @Project maatify:data-repository
* @author Mohamed Abdulalim (megyptm) <mohamed@maatify.dev>
* @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
{
}
20 changes: 20 additions & 0 deletions src/Exceptions/QueryExecutionException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

/**
* @copyright ©2025 Maatify.dev
* @Library maatify/data-repository
* @Project maatify:data-repository
* @author Mohamed Abdulalim (megyptm) <mohamed@maatify.dev>
* @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
{
}
20 changes: 20 additions & 0 deletions src/Exceptions/RedisSafetyLimitExceededException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

/**
* @copyright ©2025 Maatify.dev
* @Library maatify/data-repository
* @Project maatify:data-repository
* @author Mohamed Abdulalim (megyptm) <mohamed@maatify.dev>
* @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
{
}
20 changes: 20 additions & 0 deletions src/Exceptions/RepositoryConfigurationException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

/**
* @copyright ©2025 Maatify.dev
* @Library maatify/data-repository
* @Project maatify:data-repository
* @author Mohamed Abdulalim (megyptm) <mohamed@maatify.dev>
* @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
{
}
20 changes: 20 additions & 0 deletions src/Exceptions/UnsafeOperationException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

/**
* @copyright ©2025 Maatify.dev
* @Library maatify/data-repository
* @Project maatify:data-repository
* @author Mohamed Abdulalim (megyptm) <mohamed@maatify.dev>
* @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
{
}
64 changes: 43 additions & 21 deletions src/Generic/GenericMongoRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
}

Expand Down Expand Up @@ -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);
}
}

Expand All @@ -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);
}
}

Expand All @@ -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);
}
}

Expand All @@ -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);
}
}

Expand All @@ -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);
}
}

Expand All @@ -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);
}
}

Expand Down Expand Up @@ -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);
}
}

}
Loading