Skip to content

Add the v2 API: SentimentResult, analyze(), analyzeMany(), withLexicon() - #10

Merged
davmixcool merged 3 commits into
2.xfrom
2.x-new-api
Aug 19, 2026
Merged

Add the v2 API: SentimentResult, analyze(), analyzeMany(), withLexicon()#10
davmixcool merged 3 commits into
2.xfrom
2.x-new-api

Conversation

@davmixcool

Copy link
Copy Markdown
Owner

Adds the public API planned for v2. All of it is new surface layered over the
existing getSentiment()
— the diff is 626 insertions and 0 deletions. No
existing line of scoring logic was touched.

Why this is separate from the modernization PR

The previous PR raised the PHP floor and typed the codebase. This one adds API.
Keeping them apart means a refactor bug and an API-design bug can never appear in
the same diff.

What's added

$result = $analyzer->analyze('This update is really good!');
$result->compound();    // 0.5355
$result->label();       // 'positive'
$result->isPositive();  // true
$result->toArray();     // ['positive' => …, 'negative' => …, 'neutral' => …, 'compound' => …, 'label' => …]

$results = $analyzer->analyzeMany(['ticket-1' => '', 'ticket-2' => '']); // keys preserved
$slang   = $analyzer->withLexicon(['slaps' => 2.2]);                       // returns a NEW analyzer
  • SentimentResultfinal, immutable, readonly promoted properties.
  • SentimentResult::POSITIVE_THRESHOLD / NEGATIVE_THRESHOLD — ±0.05, the
    VADER convention, public so callers can reclassify without hardcoding.
  • InvalidLexiconTermException for rejected terms.

Scores are unchanged — and this proves it

tests/fixtures/baseline.json is byte-identical to 1.3.0. analyze()
delegates to getSentiment(), and
testAnalyzeAgreesWithGetSentimentAcrossTheBaseline() replays all 355 pinned
cases through the new API and asserts the result object matches the golden
values exactly. That is why the suite goes from 369 to 750 tests. The new API
cannot drift from the scoring contract without failing the same golden master.

tests/ApiContractTest.php is unmodified and still passes, so the frozen
legacy contract (getSentiment(), updateLexicon(), the constructor) is
provably unaffected.

Design decisions worth reviewing

withLexicon() is immutable and clones. A fresh new Analyzer() would
re-parse ~11,000 lines of lexicon files per call; PHP arrays are copy-on-write so
cloning is cheap. __clone() resets $current_sentitext — without it a clone
inherits the previous call's caps-differential flag, which
testCloneDoesNotShareTransientState() pins.

The new API is strict where the legacy one is lenient, deliberately:

Input updateLexicon() (frozen) withLexicon() (new)
['good' => 'abc'] coerced to 0 throws
['cut the mustard' => 3] silently does nothing throws

testLegacyUpdateLexiconStaysLenientWhileWithLexiconIsStrict() asserts both
halves in one test, because the asymmetry looks like an inconsistency someone
would later "tidy up".

Multi-word terms are rejected, not routed into the idiom table. That matcher
has known defects (15 of 21 entries never fire — see KNOWN-DIVERGENCES.md), so
routing would work only in some positions. A clear error beats a feature that
works sometimes.

toArray() uses positive/negative/neutral where the legacy array uses
pos/neg/neu. This is intentional: the legacy shape is frozen for backward
compatibility and can't be renamed, while the new one should read clearly.
SentimentResult does not implement ArrayAccess, so the two can never be
swapped silently.

analyzeMany() uses foreach, not array_map. The parameter is iterable
and array_map rejects Traversable; there's a test passing an ArrayIterator.

Not included

explain() — it needs a trace accumulator threaded through the scoring path, so
it lands in a later release. A stub that throws would be worse than its absence.
Lexicon file loading is also deferred; withLexicon()'s signature can widen
later without breaking callers.

Verification

  • composer matrix — green on PHP 8.1/8.2/8.3/8.4, baseline: identical
  • composer matrix --fresh — same, with a real composer install per container
  • composer stan — PHPStan level 5 clean
  • git diff 1.3.0 -- tests/fixtures/baseline.json — empty
  • Every number in the README's new section was computed from real output and
    verified programmatically, not transcribed by hand

@davmixcool
davmixcool merged commit 6175cb7 into 2.x Aug 19, 2026
4 checks passed
@davmixcool
davmixcool deleted the 2.x-new-api branch August 19, 2026 13:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant