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 $name or {$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 match arm — 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, ('…​') or xlt('…​'). The extraction this rule asks for breaks the toolchain: xgettext, WP-CLI i18n make-pot and 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, an enum case, or a define() 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

minOccurrences

3

Repetitions in one file before the literal is flagged.

minLength

5

Minimum literal length. Short strings ('', ',', 'id') are rarely worth constantising.

excludeSubscriptKeys

true

Drop a literal that is the whole key of an array subscript, $args['limit'].

excludeIdentifierArguments

true

Drop a call argument whose literal is a bare identifier. Prose and format strings still count.

i18nFunctions

the __ family, the xl family, trans, gettext

Translation functions whose argument has to stay a literal for the catalogue extractor to find it.

© 2026 Xygeni. All rights reserved