Search by

it-pack / disenchant-it

tidal

There is no license information available for the latest version (1.1.1) of this package.

A library and SDK to find and replace magic scalar values in PHP. Finds and replaces magic scalar values in PHP code with constants or enums.

Package info

github.com/it-pack/disenchant-it

pkg:composer/it-pack/disenchant-it

Statistics

Installs: 1

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

1.1.1 2026-11-03 00:52 UTC

This package is auto-updated.

Last update: 2026-11-03 23:58:59 UTC


README

A library and SDK to find and replace magic scalar values in PHP. Finds and replaces magic scalar values in PHP code with constants or enums.

Purpose

DisenchantIt helps you turn magic scalar values — most often bare string literals or numeric literals used as type tags, keys, timeouts, sizes, status codes, enum-like members, or configuration tokens — into explicit, named constants (and, eventually, backed enums). It targets the exact anti-pattern where the same literal is repeated across methods and classes, silently, with no single source of truth.

It is deliberately a library/SDK, not a black-box tool: its pieces are plain, composable objects (ports + adapters) so they can be embedded in an IDE plugin, a refactoring bot, a test-time linter, or a future CLI — without the library dictating the surrounding workflow.

The hard rule

DisenchantIt never requires, includes, class_exists()s, or otherwise executes/autoloads the PHP files it analyses or rewrites. Everything is pure source-text in, data-structure out:

  • Detection parses source into an AST and reads it; it does not load the class.
  • Rewriting produces modified source text; it does not re-evaluate the file.

This is what makes it safe to run against a class that may already be loaded elsewhere in the same process, and it is the precondition for rewriting without risking a "Cannot redeclare class" fatal. Verifying that a rewrite did not break anything (php -l, or running the project's own test suite against the working tree) is a separate process concern and stays out of DisenchantIt's runtime.

How it works

The work splits into two independent halves that are composed by the caller.

1. Detection (find the candidates)

  • An AST-based scan (via nikic/php-parser) walks every string literal — and, when a naming hint is present, every numeric literal — in a file and records where it sits: class, method, line, and the role it plays (constructor arg, method-call arg, array key/value, comparison, branch value, enum backing value, …).
  • A candidate filter drops literals that are clearly not magic values worth promoting — exception messages, sprintf/printf format strings, enum backing values, and anything that does not look identifier-shaped (prose with spaces or sentence punctuation is excluded). Numeric literals must additionally carry a recoverable naming hint (a variable/property name, an array key, or a trailing unit comment like // ms) and are excluded when trivial (0, 1, 0.0, 1.0).
  • Surviving candidates are grouped by value. Repetition across multiple sites is the strongest signal that a literal is "secretly an enum member" and should become a shared constant.

2. Replacement (place + rewrite)

  • A placement strategy decides where a promoted value's constant should live:
    • SameClassConstantStrategy — all occurrences are in one class → a single private const on that class.
    • PerClassConstantStrategy — occurrences span several classes → each class gets its own same-named private const (no new shared abstraction is invented).
    • CompositeConstantPlacementStrategy — picks SameClass vs PerClass automatically based on whether the value spans one class or several.
  • A rewriter applies the placements to source text:
    • LineBasedRewriter inserts the private const declarations (right after each class's opening brace) and replaces each literal occurrence with self::CONST, operating strictly per recorded line so untouched lines stay byte-for-byte identical. No AST reprinting, no third-party rewrite engine — formatting is preserved by convention.
  • The SemConv pattern (OpenTelemetry-SemConv-inspired) layers on a shared, dependency-free interface that collects the same constants from across the whole scanned tree. Each class's local constant is then aliased through to the shared value (private const string PATH = SemConv::PATH; for strings, private const int MASK = SemConv::MASK; for detected numbers) — existing self::PATH call sites keep working unchanged; only the declaration line changes.

Intended usage (library)

DisenchantIt is meant to be driven programmatically. Build the orchestrator through the composition-root factory and call it on one PHP source string at a time:

use ItPack\DisenchantIt\Factory\DisenchanterFactory;

$disenchanter = DisenchanterFactory::create()->build();

$rewritten = $disenchanter->rewrite($code, minOccurrences: 2);

// Optional SemConv step: produces the rewritten class source AND a shared
// constant registry source (DisenchantResult).
$result = $disenchanter->rewriteWithSemConv($code, minOccurrences: 2);
// $result->code          -> rewritten class source, aliased to SemConv
// $result->semconvSource -> generated shared registry source

DisenchantIt works on a single source string; you decide how it walks a tree, where results are written, and how the shared registry is accumulated. For the full pipeline, placement strategies, SemConv configuration, multi-file workflows, extension points, and current limitations, see docs/USER_GUIDE.md. Runnable, copy-pasteable demos — including the magic-number examples examples/12_magic_numbers.php and examples/13_magic_numbers_semconv.php — live in examples/.

The public API is fronted by DisenchanterInterface (built via DisenchanterFactory) and the Contract ports. Treat the capabilities as stable; expect signature refinements until a 1.0.0 tag.

Architecture (direction of travel)

The library is being shaped around Ports & Adapters:

  • Contract — ports (interfaces) with no internal dependencies.
  • Adapter — concrete implementations (e.g. the nikic-based detector, the line-based rewriter).
  • Core — orchestration that depends only on ports.
  • ValueObject — immutable carriers (detected literals, placements, config).
  • Collaborators are supplied via dependency injection using factories (no inline new); factories are instantiated and expose a build() method.

This keeps every piece independently testable (mock the port) and lets the rewrite engine be swapped (e.g. a future Rector-based adapter) without touching callers.

For a visual overview of the runtime — the 10 core components (Caller, DisenchanterFactory, Disenchanter, Detector, Grouper, Resolver, Placement, Rewriter, SemConv, Recorder), the primary rewrite path, the trust boundary around the analysed source, and the external dependencies — see the DisenchantIt runtime architecture diagram.

Status

Maturing. The hexagonal, port/adapter layout is in place: detection, filtering, naming, placement, line-based rewriting, and the SemConv collect + alias steps are implemented behind Contract ports and Factory composition roots. Magic numbers (numeric literals) are detected when a naming hint is recoverable and promoted to typed constants (private const int … / private const float …) that preserve the literal's raw source token. The public API is fronted by DisenchanterInterface (built via DisenchanterFactory) and the DisenchantResult value object. Expect signature refinements until a 1.0.0 tag.

  • For capabilities, workflows, and current limitations: docs/USER_GUIDE.md.
  • For architecture, execution model, and roadmap: docs/REFACTORING_PLAN.md.

Requirements

  • PHP 8.4 or newer (the codebase intends to leverage readonly classes, property hooks, and lazy objects; the scan-result cache is a bounded content-keyed store rather than a WeakMap, so cross-call reuse does not depend on object lifetime).
  • nikic/php-parser ^5 (detection).
  • nette/php-generator and rector/rector are declared for future adapter work.