Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
/tests/ export-ignore
/tools/ export-ignore
/phpunit.xml export-ignore
/phpstan.neon export-ignore
/.gitattributes export-ignore
/.gitignore export-ignore
/KNOWN-DIVERGENCES.md export-ignore
Expand Down
5 changes: 4 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
php: ['8.1', '8.2', '8.3', '8.4']

steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7

- name: Setup PHP
uses: shivammathur/setup-php@v2
Expand All @@ -38,6 +38,9 @@ jobs:
- name: Run characterization suite
run: composer test

- name: Static analysis
run: composer stan

# The baseline must be reproducible: regenerating it on a clean checkout
# must produce no diff. A diff here means scoring is not deterministic
# across PHP versions, which would invalidate the v2.0 parity guarantee.
Expand Down
85 changes: 85 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,91 @@ All notable changes to this project are documented here.

This project follows [Semantic Versioning](https://semver.org/).

## [2.0.0] - 2026-08-19

### Scores are unchanged

**2.0.0 produces byte-identical scores to 1.3.0** across all 355 pinned cases,
verified on PHP 8.1–8.4 in CI. This release modernizes the codebase; it does not
touch the scoring path. See `MIGRATION.md`.

### Added — new API

- `Analyzer::analyze(string): SentimentResult` — an immutable result object with
`compound()`, `positive()`, `negative()`, `neutral()`, `label()`,
`isPositive()`/`isNegative()`/`isNeutral()` and `toArray()`.
- `Analyzer::analyzeMany(iterable): SentimentResult[]` — preserves input keys.
- `Analyzer::withLexicon(array): static` — immutable; returns a new analyzer.
Stricter than `updateLexicon()`: rejects multi-word terms and non-numeric
values instead of silently coercing or ignoring them.
- `SentimentResult::POSITIVE_THRESHOLD` / `NEGATIVE_THRESHOLD` (±0.05, the VADER
convention) so callers can reclassify without hardcoding.

`getSentiment()` is unaffected and returns the same array as always. Note that
`SentimentResult::toArray()` uses `positive`/`negative`/`neutral` where the
legacy array uses `pos`/`neg`/`neu` — see `MIGRATION.md`.

`explain()` is not included; it is scheduled for 2.2.

### Changed — BREAKING

- **PHP 8.1+ is now required** (`^8.1`). Users on older runtimes stay on `1.x`,
which remains supported.
- Twelve internal methods are now `private`: `IsNegated`, `make_lex_dict`,
`make_emoji_dict`, `score_valence`, `_least_check`, `_but_check`,
`_idioms_check`, `_never_check`, `_punctuation_emphasis`, `_amplify_ep`,
`_amplify_qm`, `_sift_sentiment_scores`.
- `SentiText` is `@internal`; its public properties are now private with
`getWordsAndEmoticons()` / `isCapDifferential()` accessors.
- A missing lexicon file throws `Sentiment\Exceptions\InvalidLexiconException`
instead of calling `die()`.
- Removed `_sentiment_laden_idioms_check()`, which was public and never called.
No behavioural change — it is why `SENTIMENT_LADEN_IDIOMS` never fired.

### Fixed

- Dynamic property creation (`$emoji_lexicon`, `$emojis`), deprecated since PHP
8.2, now declared. The package emits no deprecation notices, enforced by
`failOnDeprecation="true"`.

### Added

- Full parameter, return and property types across `Analyzer`, `SentiText` and
`Config`.
- PHPStan at level 5, wired into CI.
- `MIGRATION.md`.
- `NOTICE.md` and `src/Lexicons/README.md` — attribution and full MIT license
text for the bundled VADER sentiment and emoji lexicons, which are
third-party data from [cjhutto/vaderSentiment](https://github.com/cjhutto/vaderSentiment)
(Copyright (c) 2016 C.J. Hutto). Both ship in the release tarball, as the
license requires. The data itself is unchanged.

### Fixed — documentation

- The README's MIT license links pointed at `/blob/master/LICENSE`, which 404s;
the file is `LICENCE.txt`.

## [1.3.1] - 2026-08-19

### Scores are unchanged

Documentation only. No source file was modified, and every pinned score in the
characterization suite is byte-identical to 1.3.0. **There is no need to
re-score stored text for this release.**

### Added

- `NOTICE.md` and `src/Lexicons/README.md` — attribution and the full MIT
license text for the bundled VADER sentiment and emoji lexicons, which are
third-party data from [cjhutto/vaderSentiment](https://github.com/cjhutto/vaderSentiment)
(Copyright (c) 2016 C.J. Hutto). Both ship in the release tarball, as the
license requires. The lexicon data itself is unchanged.

### Fixed

- The README's MIT license links pointed at `/blob/master/LICENSE`, which 404s;
the file is `LICENCE.txt`.

## [1.3.0] - 2026-08-19

### Fixed
Expand Down
40 changes: 18 additions & 22 deletions KNOWN-DIVERGENCES.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,9 @@
Behaviour that is **pinned in `tests/fixtures/baseline.json` because it is what
the code currently does — not because it is correct.**

v2.0 guarantees byte-identical scores with v1 (see the Scoring Parity section of
the v2 PRD). Everything still listed as outstanding below is therefore
reproduced exactly in v2.0 and fixed in a later release, each with a
`CHANGELOG.md` entry.
v2.0 guarantees byte-identical scores with v1. Everything still listed as
outstanding below is therefore reproduced exactly in v2.0 and fixed in a later
release, each with a `CHANGELOG.md` entry.

Items marked FIXED were corrected deliberately, with their pinned cases re-based
in the same commit and the movement documented here.
Expand Down Expand Up @@ -79,30 +78,27 @@ which only agrees trivially because its table value is zero).

Corpus sections: `idiom/*`, `idiom_sentence/*`.

**Note for v2:** this interacts with the PRD's custom-lexicon design. Multi-word
keys passed to `withLexicon()` land in the term lexicon, not the idiom table, so
they cannot work until the idiom matcher does.
**Note for v2:** this constrains custom lexicons. Multi-word keys passed to
`withLexicon()` would land in the term lexicon, not the idiom table, so they
cannot work until the idiom matcher does — which is why `withLexicon()` rejects
them outright rather than accepting them and doing nothing.

---

## 3. Dynamic property deprecations (PHP 8.2+)
## 3. Dynamic property deprecations (PHP 8.2+) — FIXED in 2.0.0

`Analyzer::__construct()` assigns `$this->emoji_lexicon` and `$this->emojis`
without declaring them (`src/Analyzer.php:25` and `:27`). Deprecated since PHP
8.2; two notices fire on every instantiation.
`Analyzer::__construct()` assigned `$this->emoji_lexicon` and `$this->emojis`
without declaring them (`src/Analyzer.php`). Deprecated since PHP 8.2; two
notices fired on every instantiation. PHP 8.1 emitted nothing, as dynamic
properties were not deprecated there.

**PHP 8.1 emits nothing** — dynamic properties are not deprecated there. The
suite therefore reports 368 passing on 8.2–8.4 and 367 passing plus 1 skipped on
8.1, which is expected, not a gap.
Both are now declared and typed. `phpunit.xml` sets `failOnDeprecation="true"`,
so any new deprecation fails the build, and the
`testKnownDynamicPropertyDeprecationsStillPresent()` tripwire has been removed —
it had done its job.

`phpunit.xml` therefore sets `failOnDeprecation="false"`.

**Scheduled for v2.0**, where declaring the properties is part of the
modernization. `ApiContractTest::testKnownDynamicPropertyDeprecationsStillPresent()`
fails once they are declared — that failure is the signal to flip
`failOnDeprecation` to `true` and delete the test.

---
Fixed on the `2.x` line only. The `1.x` maintenance line still emits these
notices on PHP 8.2+, which is expected: it exists to support PHP < 8.1.

## 4. VADER lexicon file carries a UTF-8 BOM

Expand Down
150 changes: 150 additions & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
# Migrating from 1.x to 2.0

**Your sentiment scores do not change.** 2.0 produces byte-identical `neg`,
`neu`, `pos` and `compound` values to `1.3.0` across the full 355-case
characterization suite, verified on PHP 8.1, 8.2, 8.3 and 8.4. This is enforced
in CI, not asserted by hand.

If you are upgrading from `1.2.2` or earlier, scores **do** change — but that
belongs to `1.3.0`, not to 2.0. See the "Coming from 1.2.2 or earlier" section.

---

## 1. PHP 8.1 is required

```json
"require": { "php": "^8.1" }
```

This is the breaking change that matters. Composer will not resolve 2.0 on an
older runtime, so no amount of API compatibility helps there.

**If you cannot upgrade PHP:** stay on `1.x`. It remains installable, receives
security fixes, and carries the same test suite. `composer require
davmixcool/php-sentiment-analyzer:^1.3` pins you to it.

## 2. What has NOT changed

These are frozen and enforced by `tests/ApiContractTest.php`:

```php
$analyzer = new Analyzer(); // same two optional path arguments
$scores = $analyzer->getSentiment($text); // same ['neg','neu','pos','compound']
$analyzer->updateLexicon(['rubbish' => -1.5]); // same lowercasing, same coercion
```

- `getSentiment()` returns the **same plain array**, same keys, same order, same
rounding. It does not return an object.
- `updateLexicon()` keeps lowercasing keys, keeps coercing non-numeric values to
`0`, and keeps ignoring non-array input.
- The constructor keeps resolving lexicon paths relative to the package's `src/`
directory.

## 2b. The new API (optional)

Nothing below is required. `getSentiment()` keeps working exactly as before; the
new API is opt-in and layered over it, returning the same numbers.

```php
$result = $analyzer->analyze('This update is really good!');

$result->compound(); // 0.6892
$result->label(); // 'positive'
$result->isPositive(); // true
$result->toArray(); // ['positive' => …, 'negative' => …, 'neutral' => …, 'compound' => …, 'label' => …]
```

**`toArray()` keys differ from `getSentiment()` on purpose.** The legacy shape
(`neg`/`neu`/`pos`) is frozen and cannot be renamed; the new one spells the words
out. Do not mix them up — `SentimentResult` does not implement `ArrayAccess`, so
the two can never be swapped silently.

Labels use the VADER convention, exposed as constants:
`compound >= 0.05` is positive, `<= -0.05` negative, neutral between.

```php
$results = $analyzer->analyzeMany(['a' => 'great', 'b' => 'awful']); // keys preserved

$custom = $analyzer->withLexicon(['slaps' => 2.2]); // returns a NEW analyzer
```

`withLexicon()` is **immutable** — assign the return value; the original is
unchanged. It is also stricter than the legacy `updateLexicon()`, which stays
lenient:

| Input | `updateLexicon()` (legacy) | `withLexicon()` (new) |
|---|---|---|
| `['good' => 'abc']` | coerced to `0` | throws `InvalidLexiconTermException` |
| `['cut the mustard' => 3]` | silently does nothing | throws `InvalidLexiconTermException` |
| `['GOOD' => 1.5]` | lowercased | lowercased |

Multi-word terms are rejected rather than routed into the idiom table, because
that matcher has known defects (`KNOWN-DIVERGENCES.md` §2) and would apply them
only in some positions. A clear error beats a feature that works sometimes.

`explain()` is not in 2.0 — it is scheduled for 2.2.

## 3. Accepted breaks

### 3.1 Internal methods are now private

Twelve methods that were `public` but plainly internal are now `private`:

`IsNegated`, `make_lex_dict`, `make_emoji_dict`, `score_valence`,
`_least_check`, `_but_check`, `_idioms_check`, `_never_check`,
`_punctuation_emphasis`, `_amplify_ep`, `_amplify_qm`, `_sift_sentiment_scores`

They were never part of the intended API — they were internals of the VADER port
that happened to be reachable. If you called one directly, open an issue
describing what for; that is a real use case worth designing an API around.

### 3.2 `_sentiment_laden_idioms_check()` has been removed

It was `public`, and it was **never called** — zero call sites in 1.x. Its
absence is exactly why the 12 `SENTIMENT_LADEN_IDIOMS` entries never affected
any score. Removing it changes no behaviour; it only stops the code implying a
feature that does not exist. The underlying divergence remains and is documented
in `KNOWN-DIVERGENCES.md` §2.

### 3.3 `SentiText` is encapsulated

`SentiText::$words_and_emoticons` and `$is_cap_diff` were public properties and
are now private, with `getWordsAndEmoticons()` and `isCapDifferential()`
accessors. The class is marked `@internal`.

### 3.4 A missing lexicon file now throws instead of calling `die()`

```php
use Sentiment\Exceptions\InvalidLexiconException;

try {
$analyzer = new Analyzer('Lexicons/custom.txt');
} catch (InvalidLexiconException $e) {
// handle it
}
```

v1 called `die()`, terminating the host process — behaviour a library should
never impose on the application embedding it. If you passed a custom lexicon
path and relied on the process dying, you now need a `catch`.

## 4. Coming from 1.2.2 or earlier

`1.3.0` fixed a defect in `_never_check()` that zeroed the sentiment of any word
within two tokens of "so" or "this":

| Input | 1.2.2 | 1.3.0 and 2.0 |
|---|---|---|
| `this is good` | 0.0000 | +0.4404 |
| `this is bad` | 0.0000 | -0.5423 |
| `so good` | 0.0000 | +0.4877 |

**Re-score any stored text** containing those words near sentiment terms. This
change is attributable to `1.3.0`; 2.0 inherits it unchanged.

## 5. What has not been fixed

2.0 is a modernization, not a scoring release. Known divergences from reference
VADER — most notably **15 of 21 idioms that never fire** — are reproduced
exactly and remain documented in `KNOWN-DIVERGENCES.md`. Fixing them will be its
own release with its own changelog entry, because it changes output.
65 changes: 65 additions & 0 deletions NOTICE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Third-Party Notices

This package redistributes data files that are **not** its own work. They are
included in every release so that sentiment analysis works offline, with no
network access at runtime.

The package's own source code is licensed separately under `LICENCE.txt`.

---

## VADER Sentiment Lexicon and Emoji Lexicon

**Files**

- `src/Lexicons/vader_sentiment_lexicon.txt`
- `src/Lexicons/emoji_utf8_lexicon.txt`

**Source:** [cjhutto/vaderSentiment](https://github.com/cjhutto/vaderSentiment)

**Modifications:** none of substance. The data is upstream's, with no term
added, removed, or revalued. `emoji_utf8_lexicon.txt` is byte-identical.
`vader_sentiment_lexicon.txt` differs from upstream only by a UTF-8 byte-order
mark on its first line and the absence of a trailing newline — both accidents of
copying, not edits to the data. The byte-order mark has a known side effect, and
is documented in `KNOWN-DIVERGENCES.md`.

**License:** MIT, reproduced in full below as required.

```
The MIT License (MIT)

Copyright (c) 2016 C.J. Hutto

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.```

**Citation**

> Hutto, C.J. & Gilbert, E.E. (2014). VADER: A Parsimonious Rule-based Model for
> Sentiment Analysis of Social Media Text. Eighth International Conference on
> Weblogs and Social Media (ICWSM-14). Ann Arbor, MI, June 2014.

---

## This package

Everything outside `src/Lexicons/` is the work of this package's authors and is
licensed under the MIT License in `LICENCE.txt`. The two licenses are separate:
`LICENCE.txt` does not grant rights to the lexicon data, and the notice above
does not cover this package's code.
Loading