Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
4 / 4
CRAP
100.00% covered (success)
100.00%
1 / 1
SqlIdentifierValidator
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
4 / 4
6
100.00% covered (success)
100.00%
1 / 1
 isValid
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 assertIdentifier
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 quote
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 quoteColumn
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3declare(strict_types=1);
4
5/** @license For full copyright and license information, please see the LICENSE.md file. */
6
7namespace App\Core\Database\Security;
8
9defined('AMMONLY_APP') || exit('Direct script access is forbidden.');
10
11use InvalidArgumentException;
12
13/**
14 * Enterprise SQL Identifier Security Validator.
15 *
16 * Enforces strict whitelist validation and safe escaping for dynamic SQL table,
17 * view, and column identifiers to prevent SQL injection vulnerabilities.
18 *
19 * @package App\Core\Database\Security
20 */
21final readonly class SqlIdentifierValidator
22{
23    private const string PATTERN = '/^[a-zA-Z0-9_]{1,64}$/';
24
25    /**
26     * Checks whether an identifier matches safe alphanumeric SQL naming conventions.
27     *
28     * @param string $identifier Table or column name to test.
29     * @return bool True if valid, false otherwise.
30     */
31    public static function isValid(string $identifier): bool
32    {
33        return preg_match(self::PATTERN, $identifier) === 1;
34    }
35
36    /**
37     * Asserts that an identifier is strictly valid, throwing an exception if invalid.
38     *
39     * @param string $identifier Table or column name.
40     * @return string Validated identifier.
41     * @throws InvalidArgumentException If identifier contains illegal characters.
42     */
43    public static function assertIdentifier(string $identifier): string
44    {
45        if (!self::isValid($identifier)) {
46            throw new InvalidArgumentException(
47                sprintf('Invalid SQL identifier "%s". Must match [a-zA-Z0-9_]{1,64}.', $identifier)
48            );
49        }
50
51        return $identifier;
52    }
53
54    /**
55     * Validates and encloses an identifier with backticks.
56     *
57     * @param string $identifier Table or column name.
58     * @return string Quoted identifier.
59     * @throws InvalidArgumentException If identifier contains illegal characters.
60     */
61    public static function quote(string $identifier): string
62    {
63        return '`' . self::assertIdentifier($identifier) . '`';
64    }
65
66    /**
67     * Validates and quotes a column identifier, supporting optional table alias (e.g. `table.column`).
68     *
69     * @param string $column Column name or alias.column.
70     * @return string Quoted column expression.
71     * @throws InvalidArgumentException If any part of the identifier is invalid.
72     */
73    public static function quoteColumn(string $column): string
74    {
75        $parts = explode('.', $column, 2);
76        if (count($parts) === 2) {
77            return self::quote($parts[0]) . '.' . self::quote($parts[1]);
78        }
79
80        return self::quote($column);
81    }
82}