Skip to content

Commit c509d6b

Browse files
committed
refactor: eliminate code duplication in QueryBuilder and related classes
- Extract common functionality into reusable traits: - CommonDependenciesTrait: shared dependency injection - ExternalReferenceProcessingTrait: external table reference detection - IdentifierQuotingTrait: database-specific identifier quoting - RawValueResolutionTrait: RawValue handling and resolution - TableManagementTrait: table name and prefix management - Move query interfaces to query/interfaces/ directory for better organization - Update all classes to use traits instead of duplicated code - Remove JsonIntegrationTrait (over-abstraction, logic inlined back) - Fix PHPStan type hints to use concrete QueryBuilder class in callable types - Add missing properties and imports to resolve PHP 8.4 compatibility issues All tests pass, no breaking changes to public API, maintains backward compatibility.
1 parent 2931d8a commit c509d6b

26 files changed

Lines changed: 321 additions & 395 deletions

src/query/BatchProcessor.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
use PDO;
1010
use PDOException;
1111
use tommyknocker\pdodb\connection\ConnectionInterface;
12+
use tommyknocker\pdodb\query\interfaces\BatchProcessorInterface;
13+
use tommyknocker\pdodb\query\interfaces\ExecutionEngineInterface;
14+
use tommyknocker\pdodb\query\interfaces\ParameterManagerInterface;
1215

1316
class BatchProcessor implements BatchProcessorInterface
1417
{

src/query/ConditionBuilder.php

Lines changed: 14 additions & 142 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,23 @@
77
use InvalidArgumentException;
88
use PDOException;
99
use tommyknocker\pdodb\connection\ConnectionInterface;
10-
use tommyknocker\pdodb\dialects\DialectInterface;
1110
use tommyknocker\pdodb\helpers\RawValue;
11+
use tommyknocker\pdodb\query\interfaces\ConditionBuilderInterface;
12+
use tommyknocker\pdodb\query\interfaces\ExecutionEngineInterface;
13+
use tommyknocker\pdodb\query\interfaces\ParameterManagerInterface;
14+
use tommyknocker\pdodb\query\traits\CommonDependenciesTrait;
15+
use tommyknocker\pdodb\query\traits\ExternalReferenceProcessingTrait;
16+
use tommyknocker\pdodb\query\traits\IdentifierQuotingTrait;
17+
use tommyknocker\pdodb\query\traits\RawValueResolutionTrait;
18+
use tommyknocker\pdodb\query\traits\TableManagementTrait;
1219

1320
class ConditionBuilder implements ConditionBuilderInterface
1421
{
15-
protected ConnectionInterface $connection;
16-
protected DialectInterface $dialect;
17-
protected ParameterManagerInterface $parameterManager;
18-
protected ExecutionEngineInterface $executionEngine;
19-
protected RawValueResolver $rawValueResolver;
22+
use CommonDependenciesTrait;
23+
use RawValueResolutionTrait;
24+
use TableManagementTrait;
25+
use IdentifierQuotingTrait;
26+
use ExternalReferenceProcessingTrait;
2027

2128
/** @var array<int, string|array<string, mixed>> */
2229
protected array $where = [];
@@ -27,9 +34,6 @@ class ConditionBuilder implements ConditionBuilderInterface
2734
/** @var string|null table name */
2835
protected ?string $table = null;
2936

30-
/** @var string|null Table prefix */
31-
protected ?string $prefix = null;
32-
3337
/** @var array<int, string> ORDER BY expressions */
3438
protected array $order = [];
3539

@@ -42,37 +46,7 @@ public function __construct(
4246
ExecutionEngineInterface $executionEngine,
4347
RawValueResolver $rawValueResolver
4448
) {
45-
$this->connection = $connection;
46-
$this->dialect = $connection->getDialect();
47-
$this->parameterManager = $parameterManager;
48-
$this->executionEngine = $executionEngine;
49-
$this->rawValueResolver = $rawValueResolver;
50-
}
51-
52-
/**
53-
* Set table name.
54-
*
55-
* @param string $table
56-
*
57-
* @return self
58-
*/
59-
public function setTable(string $table): self
60-
{
61-
$this->table = $table;
62-
return $this;
63-
}
64-
65-
/**
66-
* Set table prefix.
67-
*
68-
* @param string|null $prefix
69-
*
70-
* @return self
71-
*/
72-
public function setPrefix(?string $prefix): self
73-
{
74-
$this->prefix = $prefix;
75-
return $this;
49+
$this->initializeCommonDependencies($connection, $parameterManager, $executionEngine, $rawValueResolver);
7650
}
7751

7852
/**
@@ -547,37 +521,6 @@ protected function buildSelectSql(): string
547521
return trim($sql);
548522
}
549523

550-
/**
551-
* Quote qualified identifier.
552-
*
553-
* @param string $name
554-
*
555-
* @return string
556-
*/
557-
protected function quoteQualifiedIdentifier(string $name): string
558-
{
559-
// If looks like an expression (contains spaces, parentheses, commas or quotes)
560-
// treat as raw expression but DO NOT accept suspicious unquoted parts silently.
561-
if (preg_match('/[`\["\'\s\(\),]/', $name)) {
562-
// allow already-quoted or complex expressions to pass through,
563-
// but still protect obvious injection attempts by checking for dangerous tokens
564-
if (preg_match('/;|--|\bDROP\b|\bDELETE\b|\bINSERT\b|\bUPDATE\b|\bSELECT\b|\bUNION\b/i', $name)) {
565-
throw new InvalidArgumentException('Unsafe SQL expression provided as identifier/expression.');
566-
}
567-
return $name;
568-
}
569-
570-
$parts = explode('.', $name);
571-
foreach ($parts as $p) {
572-
// require valid simple identifier parts
573-
if (!preg_match('/^[A-Za-z_][A-Za-z0-9_]*$/', $p)) {
574-
throw new InvalidArgumentException("Invalid identifier part: {$p}");
575-
}
576-
}
577-
$quoted = array_map(fn ($p) => $this->dialect->quoteIdentifier($p), $parts);
578-
return implode('.', $quoted);
579-
}
580-
581524
/**
582525
* Normalize operator (trim and uppercase).
583526
*
@@ -589,75 +532,4 @@ protected function normalizeOperator(string $operator): string
589532
{
590533
return strtoupper(trim($operator));
591534
}
592-
593-
/**
594-
* Resolve RawValue instances.
595-
*
596-
* @param string|RawValue $value
597-
*
598-
* @return string
599-
*/
600-
protected function resolveRawValue(string|RawValue $value): string
601-
{
602-
return $this->rawValueResolver->resolveRawValue($value);
603-
}
604-
605-
/**
606-
* Normalizes a table name by prefixing it with the database prefix if it is set.
607-
*
608-
* @param string|null $table
609-
*
610-
* @return string The normalized table name.
611-
*/
612-
protected function normalizeTable(?string $table = null): string
613-
{
614-
$table = $table ?: $this->table;
615-
return $this->dialect->quoteTable($this->prefix . $table);
616-
}
617-
618-
/**
619-
* Check if a string represents an external table reference.
620-
*
621-
* @param string $reference The reference to check (e.g., 'users.id')
622-
*
623-
* @return bool True if it's an external reference
624-
*/
625-
protected function isExternalReference(string $reference): bool
626-
{
627-
// Check if it matches table.column pattern
628-
if (!preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*\.[a-zA-Z_][a-zA-Z0-9_]*$/', $reference)) {
629-
return false;
630-
}
631-
632-
$table = explode('.', $reference)[0];
633-
return !$this->isTableInCurrentQuery($table);
634-
}
635-
636-
/**
637-
* Check if a table is referenced in the current query.
638-
*
639-
* @param string $tableName The table name to check
640-
*
641-
* @return bool True if table is in current query
642-
*/
643-
protected function isTableInCurrentQuery(string $tableName): bool
644-
{
645-
return $this->table === $tableName;
646-
}
647-
648-
/**
649-
* Automatically convert external references to RawValue.
650-
*
651-
* @param mixed $value The value to process
652-
*
653-
* @return mixed Processed value
654-
*/
655-
protected function processExternalReferences(mixed $value): mixed
656-
{
657-
if (is_string($value) && $this->isExternalReference($value)) {
658-
return new RawValue($value);
659-
}
660-
661-
return $value;
662-
}
663535
}

src/query/DmlQueryBuilder.php

Lines changed: 13 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,20 @@
88
use PDOException;
99
use RuntimeException;
1010
use tommyknocker\pdodb\connection\ConnectionInterface;
11-
use tommyknocker\pdodb\dialects\DialectInterface;
1211
use tommyknocker\pdodb\helpers\RawValue;
12+
use tommyknocker\pdodb\query\interfaces\ConditionBuilderInterface;
13+
use tommyknocker\pdodb\query\interfaces\DmlQueryBuilderInterface;
14+
use tommyknocker\pdodb\query\interfaces\ExecutionEngineInterface;
15+
use tommyknocker\pdodb\query\interfaces\ParameterManagerInterface;
16+
use tommyknocker\pdodb\query\traits\CommonDependenciesTrait;
17+
use tommyknocker\pdodb\query\traits\RawValueResolutionTrait;
18+
use tommyknocker\pdodb\query\traits\TableManagementTrait;
1319

1420
class DmlQueryBuilder implements DmlQueryBuilderInterface
1521
{
16-
protected ConnectionInterface $connection;
17-
protected DialectInterface $dialect;
18-
protected ParameterManagerInterface $parameterManager;
19-
protected ExecutionEngineInterface $executionEngine;
20-
protected ConditionBuilderInterface $conditionBuilder;
21-
protected RawValueResolver $rawValueResolver;
22+
use CommonDependenciesTrait;
23+
use RawValueResolutionTrait;
24+
use TableManagementTrait;
2225

2326
/** @var string|null table name */
2427
protected ?string $table = null {
@@ -39,28 +42,23 @@ class DmlQueryBuilder implements DmlQueryBuilderInterface
3942
/** @var array<string, string|int|float|bool|null|RawValue> */
4043
protected array $onDuplicate = [];
4144

42-
/** @var string|null Table prefix */
43-
protected ?string $prefix = null;
44-
4545
/** @var array<int|string, mixed> Query options (e.g., FOR UPDATE, IGNORE) */
4646
protected array $options = [];
4747

4848
/** @var int|null LIMIT value */
4949
protected ?int $limit = null;
5050

51+
protected ConditionBuilderInterface $conditionBuilder;
52+
5153
public function __construct(
5254
ConnectionInterface $connection,
5355
ParameterManagerInterface $parameterManager,
5456
ExecutionEngineInterface $executionEngine,
5557
ConditionBuilderInterface $conditionBuilder,
5658
RawValueResolver $rawValueResolver
5759
) {
58-
$this->connection = $connection;
59-
$this->dialect = $connection->getDialect();
60-
$this->parameterManager = $parameterManager;
61-
$this->executionEngine = $executionEngine;
60+
$this->initializeCommonDependencies($connection, $parameterManager, $executionEngine, $rawValueResolver);
6261
$this->conditionBuilder = $conditionBuilder;
63-
$this->rawValueResolver = $rawValueResolver;
6462
}
6563

6664
/**
@@ -471,29 +469,4 @@ protected function processValueForSql(mixed $value, string $columnName, string $
471469
$this->parameterManager->setParam($paramName, $value);
472470
return ['sql' => $paramName, 'params' => []];
473471
}
474-
475-
/**
476-
* Resolve RawValue instances.
477-
*
478-
* @param string|RawValue $value
479-
*
480-
* @return string
481-
*/
482-
protected function resolveRawValue(string|RawValue $value): string
483-
{
484-
return $this->rawValueResolver->resolveRawValue($value);
485-
}
486-
487-
/**
488-
* Normalizes a table name by prefixing it with the database prefix if it is set.
489-
*
490-
* @param string|null $table
491-
*
492-
* @return string The normalized table name.
493-
*/
494-
protected function normalizeTable(?string $table = null): string
495-
{
496-
$table = $table ?: $this->table;
497-
return $this->dialect->quoteTable($this->prefix . $table);
498-
}
499472
}

src/query/ExecutionEngine.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
use tommyknocker\pdodb\connection\ConnectionInterface;
1111
use tommyknocker\pdodb\exceptions\ExceptionFactory;
1212
use tommyknocker\pdodb\helpers\RawValue;
13+
use tommyknocker\pdodb\query\interfaces\ExecutionEngineInterface;
14+
use tommyknocker\pdodb\query\interfaces\ParameterManagerInterface;
1315

1416
class ExecutionEngine implements ExecutionEngineInterface
1517
{

src/query/FileLoader.php

Lines changed: 7 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -6,47 +6,25 @@
66

77
use PDOException;
88
use tommyknocker\pdodb\connection\ConnectionInterface;
9+
use tommyknocker\pdodb\dialects\DialectInterface;
910
use tommyknocker\pdodb\exceptions\ExceptionFactory;
11+
use tommyknocker\pdodb\query\interfaces\FileLoaderInterface;
12+
use tommyknocker\pdodb\query\traits\TableManagementTrait;
1013

1114
class FileLoader implements FileLoaderInterface
1215
{
16+
use TableManagementTrait;
17+
1318
protected ConnectionInterface $connection;
19+
protected DialectInterface $dialect;
1420

1521
/** @var string|null table name */
1622
protected ?string $table = null;
1723

18-
/** @var string|null Table prefix */
19-
protected ?string $prefix = null;
20-
2124
public function __construct(ConnectionInterface $connection)
2225
{
2326
$this->connection = $connection;
24-
}
25-
26-
/**
27-
* Set table name.
28-
*
29-
* @param string $table
30-
*
31-
* @return self
32-
*/
33-
public function setTable(string $table): self
34-
{
35-
$this->table = $table;
36-
return $this;
37-
}
38-
39-
/**
40-
* Set table prefix.
41-
*
42-
* @param string|null $prefix
43-
*
44-
* @return self
45-
*/
46-
public function setPrefix(?string $prefix): self
47-
{
48-
$this->prefix = $prefix;
49-
return $this;
27+
$this->dialect = $connection->getDialect();
5028
}
5129

5230
/**

0 commit comments

Comments
 (0)