Duplicate String Literals
ID |
php.duplicate_string_literals |
Severity |
high |
Remediation Complexity |
medium |
Remediation Risk |
low |
Remediation Effort |
medium |
Resource |
Code Smell |
Language |
Php |
Tags |
code-style, duplication |
Description
Reports a string literal that appears the same three or more times in one file (both thresholds are configurable). One finding per literal, at the first occurrence: the repetition is one fact about the file, and every copy is fixed by the same edit. The message names the literal, so the finding can be acted on without opening the file.
The scope is the file rather than the class, because PHP files hold free functions, procedural includes and templates as readily as classes.
Single- and double-quoted literals of the same text are counted together — the quoting style is a choice a rule should not police, and the two spellings of one value are exactly the pair that has to stay in step. Escapes are compared as written rather than decoded.
The following are left out, structurally:
-
A literal that interpolates. A double-quoted string carrying
$nameor{$expr}is not a constant candidate at all, since its value depends on the scope it sits in. -
Heredoc, nowdoc and backtick literals. A heredoc is a block of text rather than a magic value.
-
Literals inside an array item list or a
matcharm — data in a table, not a magic value repeated through logic. A route map, a MIME-type table or a locale list recurs because several rows genuinely share a word, keys included, and "extract it to a constant" is not advice that can be followed there. The item list is the test rather than the array expression, because a property, static or parameter default array is parsed as a constant initializer that spells the table inline. -
A literal that is the whole key of an array subscript,
$args['limit']. That is how PHP spells field access: the sibling languages hold an identifier in the same position and see no string at all, so counting it reports a shape no sibling reports, on code that has no constant to extract. -
An argument of a translation function,
('…')orxlt('…'). The extraction this rule asks for breaks the toolchain: xgettext, WP-CLIi18n make-potand Symfony’s extractor scan for a literal argument, so(self::PRIVACY)drops the string from the catalogue altogether. -
A call argument whose literal is a bare identifier,
get_option('siteurl')or→addTag('kernel.event_listener'). The literal is a name handed to a keyed API — an enum, a constant, a DI token or a column — which every other language spells as a symbol. Prose and format strings in the same position are kept: they are the genuine repetition this rule is about. -
The definition of a constant — a
const, a class constant, anenumcase, or adefine()call. That literal is the extraction this rule asks for. -
Literals inside a PHP attribute — configuration DSL, not executable code.
Test files are skipped whole: a test repeats a fixture name across cases on purpose, and the repetition is what makes each case readable on its own.
Rationale
A repeated literal is an invitation to an inconsistency bug. One copy gets renamed and the others do not, and nothing in the copies tells a reader they were meant to stay in step — so the mismatch is found at runtime, in the one code path that was not exercised. The copies also hide how widely the value is used: a grep is the only way to learn that a carrier code appears in a comparison, a log line and a lookup, and a grep is not something a reviewer does by reflex.
Naming the value fixes both. The constant records the intent that the occurrences share one value, the rename happens in one place, and the compiler — not the test suite — catches a use that was missed.
<?php
class ShipmentGateway
{
public function dispatch(array $shipment): string
{
if ($shipment['carrier'] === 'royal-express') { // FLAW - repeated three times below
$this->log[] = 'royal-express';
}
return $this->quote('royal-express', $shipment);
}
public function carriers(): array
{
return [ // OK - a data table
'royal-express' => 'Royal Express',
'sea-freight' => 'Sea Freight',
];
}
public function settings(array $args): array
{
return [
$args['limit_response_size'], // OK - a subscript key is field access
get_option('shipment_carrier'), // OK - a name handed to a keyed API
__('Could not save the shipment.'), // OK - xgettext needs the literal
];
}
}
Remediation
Extract the value into a class constant, or a namespaced const when it is shared beyond one class,
and use the constant everywhere. Where the literal is one of a closed set of values — a status, a
carrier, a channel — an enum states that better than a set of constants, and gives the type
checker something to work with at every call site.
<?php
class ShipmentGateway
{
private const CARRIER_ROYAL_EXPRESS = 'royal-express';
public function dispatch(array $shipment): string
{
if ($shipment['carrier'] === self::CARRIER_ROYAL_EXPRESS) {
$this->log[] = self::CARRIER_ROYAL_EXPRESS;
}
return $this->quote(self::CARRIER_ROYAL_EXPRESS, $shipment);
}
}
Configuration
| Property | Default | Meaning |
|---|---|---|
|
|
Repetitions in one file before the literal is flagged. |
|
|
Minimum literal length. Short strings ( |
|
|
Drop a literal that is the whole key of an array subscript, |
|
|
Drop a call argument whose literal is a bare identifier. Prose and format strings still count. |
|
the |
Translation functions whose argument has to stay a literal for the catalogue extractor to find it. |