refactor: rename lib/ to core/ for clearer core-module separation
Rename the top-level lib/ directory to core/ so the project root immediately communicates which code is core platform and which lives in modules/. PHP namespaces (MintyPHP\*) are unchanged — only the Composer PSR-4 path mapping moves from lib/ to core/. Module-internal lib/ directories (modules/*/lib/) are untouched. Updated across the full stack: - composer.json PSR-4 mapping - bootstrap entry points (web/index.php, bin/*, tests/bootstrap.php) - tooling configs (phpstan.neon, phpunit.xml, php-cs-fixer) - 26 architecture contract tests - enforcement-policy, guard-catalog, quality-gates - all documentation (CLAUDE.md, README, 25 docs/, .agents/skills/) - bin/qa-extended.sh search paths - .claude/settings.local.json permission paths Workflow: .agents/runs/CORE-LIB-RENAME-001/ (Analyst → Planner → Executor → Code Review (4 findings fixed) → Security Review → Acceptance Test → Finalizer) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
80
core/Repository/Support/DatabaseSessionRepository.php
Normal file
80
core/Repository/Support/DatabaseSessionRepository.php
Normal file
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Repository\Support;
|
||||
|
||||
use MintyPHP\DB;
|
||||
|
||||
/** Executes database session operations: advisory locks, transactions, and commit/rollback. */
|
||||
class DatabaseSessionRepository implements DatabaseSessionRepositoryInterface
|
||||
{
|
||||
public function acquireAdvisoryLock(string $lockName, int $timeoutSeconds = 0): bool
|
||||
{
|
||||
$lockName = trim($lockName);
|
||||
if ($lockName === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$timeoutSeconds = max(0, (int) $timeoutSeconds);
|
||||
$got = DB::selectValue(
|
||||
'select GET_LOCK(?, ?) as got_lock',
|
||||
$lockName,
|
||||
(string) $timeoutSeconds
|
||||
);
|
||||
|
||||
return (int) $got === 1;
|
||||
}
|
||||
|
||||
public function releaseAdvisoryLock(string $lockName): void
|
||||
{
|
||||
$lockName = trim($lockName);
|
||||
if ($lockName === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
DB::selectValue('select RELEASE_LOCK(?) as released_lock', $lockName);
|
||||
}
|
||||
|
||||
public function beginTransaction(): void
|
||||
{
|
||||
DB::handle()->begin_transaction();
|
||||
}
|
||||
|
||||
public function commitTransaction(): void
|
||||
{
|
||||
DB::handle()->commit();
|
||||
}
|
||||
|
||||
public function rollbackTransaction(): void
|
||||
{
|
||||
DB::handle()->rollback();
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a callback inside a DB transaction. Commits on success, rolls back on exception.
|
||||
*
|
||||
* Eliminates the need for manual begin/commit/rollback + rollbackQuietly() boilerplate
|
||||
* that is duplicated across multiple services. The callback receives no arguments;
|
||||
* use closures to capture dependencies.
|
||||
*
|
||||
* @template T
|
||||
* @param callable(): T $callback
|
||||
* @return T The value returned by the callback.
|
||||
* @throws \Throwable Re-throws the original exception after rollback.
|
||||
*/
|
||||
public function transaction(callable $callback): mixed
|
||||
{
|
||||
$this->beginTransaction();
|
||||
try {
|
||||
$result = $callback();
|
||||
$this->commitTransaction();
|
||||
return $result;
|
||||
} catch (\Throwable $e) {
|
||||
try {
|
||||
$this->rollbackTransaction();
|
||||
} catch (\Throwable) {
|
||||
// Swallow — the original exception is more important.
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Repository\Support;
|
||||
|
||||
/** Contract for database-level session control: advisory locks and transaction management. */
|
||||
interface DatabaseSessionRepositoryInterface
|
||||
{
|
||||
public function acquireAdvisoryLock(string $lockName, int $timeoutSeconds = 0): bool;
|
||||
|
||||
public function releaseAdvisoryLock(string $lockName): void;
|
||||
|
||||
public function beginTransaction(): void;
|
||||
|
||||
public function commitTransaction(): void;
|
||||
|
||||
public function rollbackTransaction(): void;
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param callable(): T $callback
|
||||
* @return T
|
||||
*/
|
||||
public function transaction(callable $callback): mixed;
|
||||
}
|
||||
198
core/Repository/Support/RepoQuery.php
Normal file
198
core/Repository/Support/RepoQuery.php
Normal file
@@ -0,0 +1,198 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Repository\Support;
|
||||
|
||||
/** Static helpers for safe SQL query building: LIKE escaping, limit/offset clamping, ID list normalization, and enum filtering. */
|
||||
class RepoQuery
|
||||
{
|
||||
private static function normalizeLikeValue(string $value): string
|
||||
{
|
||||
$value = trim($value);
|
||||
if ($value === '') {
|
||||
return '';
|
||||
}
|
||||
$value = str_replace('\\', '\\\\', $value);
|
||||
$value = str_replace('_', '\\_', $value);
|
||||
$value = str_replace('*', '%', $value);
|
||||
if (!str_starts_with($value, '%')) {
|
||||
$value = '%' . $value;
|
||||
}
|
||||
if (!str_ends_with($value, '%')) {
|
||||
$value = $value . '%';
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
public static function sanitizeLimitOffset(
|
||||
array $options,
|
||||
int $defaultLimit = 10,
|
||||
int $minLimit = 1,
|
||||
int $maxLimit = 100,
|
||||
int $defaultOffset = 0
|
||||
): array {
|
||||
$limit = (int) ($options['limit'] ?? $defaultLimit);
|
||||
if ($limit < $minLimit) {
|
||||
$limit = $defaultLimit;
|
||||
} elseif ($limit > $maxLimit) {
|
||||
$limit = $maxLimit;
|
||||
}
|
||||
|
||||
$offset = (int) ($options['offset'] ?? $defaultOffset);
|
||||
if ($offset < 0) {
|
||||
$offset = 0;
|
||||
}
|
||||
|
||||
return [$limit, $offset];
|
||||
}
|
||||
|
||||
public static function sanitizeOrder(
|
||||
array $options,
|
||||
array $allowedOrder,
|
||||
string $defaultOrder = 'id',
|
||||
string $defaultDir = 'desc'
|
||||
): array {
|
||||
$order = (string) ($options['order'] ?? $defaultOrder);
|
||||
$dir = strtolower((string) ($options['dir'] ?? $defaultDir));
|
||||
if (!in_array($order, $allowedOrder, true)) {
|
||||
$order = $defaultOrder;
|
||||
}
|
||||
if (!in_array($dir, ['asc', 'desc'], true)) {
|
||||
$dir = $defaultDir;
|
||||
}
|
||||
return [$order, $dir];
|
||||
}
|
||||
|
||||
public static function addLikeFilter(array &$where, array &$params, array $fields, string $value): void
|
||||
{
|
||||
$like = self::normalizeLikeValue($value);
|
||||
if ($like === '') {
|
||||
return;
|
||||
}
|
||||
$clauses = [];
|
||||
foreach ($fields as $field) {
|
||||
$clauses[] = $field . " like ? escape '\\\\'";
|
||||
$params[] = $like;
|
||||
}
|
||||
if ($clauses) {
|
||||
$where[] = '(' . implode(' or ', $clauses) . ')';
|
||||
}
|
||||
}
|
||||
|
||||
public static function addEnumFilter(array &$where, array &$params, $value, array $map): void
|
||||
{
|
||||
if ($value === null) {
|
||||
return;
|
||||
}
|
||||
$normalized = strtolower(trim((string) $value));
|
||||
if ($normalized === '' || $normalized === 'all') {
|
||||
return;
|
||||
}
|
||||
foreach ($map as $entry) {
|
||||
$aliases = $entry['aliases'] ?? [];
|
||||
$aliases = array_map('strtolower', $aliases);
|
||||
if (!in_array($normalized, $aliases, true)) {
|
||||
continue;
|
||||
}
|
||||
$sql = (string) ($entry['sql'] ?? '');
|
||||
if ($sql === '') {
|
||||
return;
|
||||
}
|
||||
$where[] = $sql;
|
||||
$paramsToAdd = $entry['params'] ?? [];
|
||||
foreach ($paramsToAdd as $param) {
|
||||
$params[] = $param;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public static function uuidV4(): string
|
||||
{
|
||||
$data = random_bytes(16);
|
||||
$data[6] = chr((ord($data[6]) & 0x0f) | 0x40);
|
||||
$data[8] = chr((ord($data[8]) & 0x3f) | 0x80);
|
||||
return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
|
||||
}
|
||||
|
||||
public static function addEqualsFilter(array &$where, array &$params, $value, string $sql, bool $allowEmpty = false): void
|
||||
{
|
||||
// Use for simple "= ?" filters; skip when empty to keep WHERE clean.
|
||||
if ($value === null) {
|
||||
return;
|
||||
}
|
||||
$normalized = trim((string) $value);
|
||||
if ($normalized === '' && !$allowEmpty) {
|
||||
return;
|
||||
}
|
||||
$where[] = $sql;
|
||||
$params[] = $normalized;
|
||||
}
|
||||
|
||||
public static function normalizeIdList($value): array
|
||||
{
|
||||
if (is_string($value)) {
|
||||
$value = array_filter(array_map('trim', explode(',', $value)));
|
||||
} elseif (!is_array($value)) {
|
||||
return [];
|
||||
}
|
||||
$flat = [];
|
||||
array_walk_recursive($value, static function ($item) use (&$flat): void {
|
||||
$flat[] = $item;
|
||||
});
|
||||
$ids = [];
|
||||
foreach ($flat as $item) {
|
||||
$id = (int) trim((string) $item);
|
||||
if ($id > 0) {
|
||||
$ids[] = $id;
|
||||
}
|
||||
}
|
||||
$ids = array_values(array_unique($ids));
|
||||
sort($ids, SORT_NUMERIC);
|
||||
return $ids;
|
||||
}
|
||||
|
||||
public static function normalizeStringList(mixed $value, int $max = 200, ?callable $sanitizer = null): array
|
||||
{
|
||||
if ($max <= 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (is_string($value)) {
|
||||
$value = array_filter(array_map('trim', explode(',', $value)));
|
||||
} elseif (!is_array($value)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$flat = [];
|
||||
array_walk_recursive($value, static function ($item) use (&$flat): void {
|
||||
$flat[] = $item;
|
||||
});
|
||||
|
||||
$items = [];
|
||||
$seen = [];
|
||||
foreach ($flat as $item) {
|
||||
$text = trim((string) $item);
|
||||
if ($text === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($sanitizer !== null) {
|
||||
$text = trim((string) $sanitizer($text));
|
||||
if ($text === '') {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($seen[$text])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$seen[$text] = true;
|
||||
$items[] = $text;
|
||||
if (count($items) >= $max) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $items;
|
||||
}
|
||||
}
|
||||
96
core/Repository/Support/RepositoryArrayHelper.php
Normal file
96
core/Repository/Support/RepositoryArrayHelper.php
Normal file
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Repository\Support;
|
||||
|
||||
final class RepositoryArrayHelper
|
||||
{
|
||||
/**
|
||||
* Unwrap a single DB row from its table-name wrapper.
|
||||
*
|
||||
* MintyPHP DB layer returns rows wrapped in table-name keys, e.g. ['tenants' => [...]].
|
||||
* This extracts the inner array for the given table key.
|
||||
*/
|
||||
public static function unwrap(?array $row, string $tableKey): ?array
|
||||
{
|
||||
if (!$row) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $row[$tableKey] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unwrap a list of DB rows from their table-name wrappers.
|
||||
*/
|
||||
public static function unwrapList(mixed $rows, string $tableKey): array
|
||||
{
|
||||
if (!is_array($rows)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$list = [];
|
||||
foreach ($rows as $row) {
|
||||
$data = $row[$tableKey] ?? null;
|
||||
if (is_array($data)) {
|
||||
$list[] = $data;
|
||||
}
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract integer IDs from DB rows, unwrapping the table-name key.
|
||||
*
|
||||
* Returns a deduplicated array of unique positive integer IDs.
|
||||
*/
|
||||
public static function extractIds(mixed $rows, string $tableKey, string $idField = 'id'): array
|
||||
{
|
||||
if (!is_array($rows)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$ids = [];
|
||||
foreach ($rows as $row) {
|
||||
$data = $row[$tableKey] ?? $row;
|
||||
if (is_array($data) && isset($data[$idField])) {
|
||||
$ids[] = (int) $data[$idField];
|
||||
}
|
||||
}
|
||||
|
||||
return array_values(array_unique($ids));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize an array of IDs to unique positive integers.
|
||||
*/
|
||||
public static function sanitizePositiveIds(array $ids): array
|
||||
{
|
||||
$ids = array_values(array_unique(array_map('intval', $ids)));
|
||||
|
||||
return array_values(array_filter($ids, static fn (int $id): bool => $id > 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* Flatten a MintyPHP DB row that can contain nested table-key arrays.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function flattenRow(mixed $row): array
|
||||
{
|
||||
if (!is_array($row)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$flat = [];
|
||||
foreach ($row as $key => $value) {
|
||||
if (is_array($value)) {
|
||||
$flat = array_merge($flat, $value);
|
||||
} elseif (is_string($key)) {
|
||||
$flat[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $flat;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user