1
0
Files
fs 0e86925464 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>
2026-04-13 23:20:42 +02:00

199 lines
5.7 KiB
PHP

<?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;
}
}