Skip to content

Lazy Article properties + metadataOnly option (4.1.0) - #52

Merged
fivefilters merged 4 commits into
masterfrom
claude/readability-lazy-content-dc7rh4
Jul 30, 2026
Merged

Lazy Article properties + metadataOnly option (4.1.0)#52
fivefilters merged 4 commits into
masterfrom
claude/readability-lazy-content-dc7rh4

Conversation

@fivefilters

@fivefilters fivefilters commented Jul 30, 2026

Copy link
Copy Markdown
Owner

Summary

Two related performance/API changes, staged as the v4.1.0 release (CHANGELOG updated; tag to be cut on master after merge).

1. Lazy content-derived Article properties

On every successful parse, Readability::parse() eagerly built the Article with content (a full innerHTML serialization of the extracted body), textContent (a full text walk), length, and the collected image list. Callers that only use contentElement, hasContent() or the metadata paid that cost — potentially multiple MB of string building per request — for values they never read.

  • content, textContent, length and images are now virtual properties on Article (PHP 8.4 property hooks), computed from contentElement on first access and cached in private backing fields.
  • hasContent() checks contentElement !== null instead of content !== null, so it no longer forces a serialization.
  • Image collection (collectImages) moved from Readability onto Article.
  • The Article constructor now takes only the metadata and contentElement.
  • json_encode() and var_dump() output is preserved via JsonSerializable::jsonSerialize() and __debugInfo() — same fields, same order as the eager implementation (both trigger the full serialization, as before).

2. New metadataOnly option

For callers that locate the article body themselves and only want Readability's title/metadata extraction. When set, parse() skips every stage that mutates the document — unwrapNoscriptImages, removeScripts, prepDocument, and grabArticle — making the pass guaranteed read-only on a passed-in \Dom\HTMLDocument. Title and metadata extraction depend on none of those stages (JSON-LD is read before script removal anyway); the article language, normally picked up during grabArticle's traversal, is read directly from the root element instead. The returned Article has the content-derived properties null (hasContent() returns false), the same shape as the no-content case.

Behavioral notes

  • Reads are unchanged: same property names, same types, same values, null in the no-content case, __toString() unchanged.
  • The lazy values are snapshots taken on first read: mutating contentElement after reading content/textContent is not reflected (previously the snapshot was taken at parse time). Documented on the class and in the README.
  • The virtual properties don't appear in get_object_vars() or (array) casts (PHP offers no hook for those); noted in the CHANGELOG. Reflection-based serializers are unaffected — hooked properties are declared properties.

Testing

  • composer test — 489 tests, 1103 assertions, green. New tests pin: the lazy/cached semantics, the read-only behavior of the hooked properties, json_encode/var_dump output, and metadataOnly (metadata extracted, no content, and the passed document byte-identical before/after the parse).
  • composer analyse (Psalm) — no errors.

🤖 Generated with [Claude Code]

claude added 3 commits July 30, 2026 16:39
On every successful parse, Article was built eagerly with content
(a full innerHTML serialization of the extracted body), textContent
(a full text walk), length, and the collected image list — paid even
by callers that only use contentElement, hasContent() or the metadata.

content, textContent, length and images are now virtual properties
(PHP 8.4 property hooks) computed from contentElement on first access
and cached, hasContent() checks contentElement directly, and image
collection moved onto Article. Reads are unchanged; the Article
constructor now takes only the metadata and contentElement.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018iNBFSQM7xGLoYg9Y5EJx4
Virtual (hooked) properties are invisible to json_encode() and
var_dump(), which would silently drop content, textContent, length and
images from dumped articles. jsonSerialize() and __debugInfo() spell
out the full property list in the pre-lazy order, so both produce the
same output as the eager implementation (at the cost of triggering the
serialization the lazy properties otherwise avoid).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018iNBFSQM7xGLoYg9Y5EJx4
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018iNBFSQM7xGLoYg9Y5EJx4
@fivefilters fivefilters changed the title Compute Article content-derived properties lazily Compute Article content-derived properties lazily (4.0.1) Jul 30, 2026
metadataOnly extracts only the title and metadata, skipping content
extraction entirely: parse() skips every stage that mutates the
document (unwrapNoscriptImages, removeScripts, prepDocument and
grabArticle), so a passed-in Dom\HTMLDocument is guaranteed to be left
unmodified. Title and metadata extraction depend on none of those
stages (JSON-LD is read before script removal anyway); the article
language, normally picked up during grabArticle's traversal, is read
directly from the root element instead.

Useful for callers that locate the article body themselves and only
want Readability's title/metadata extraction. The release becomes
4.1.0 rather than 4.0.1: it adds an option, and the Article
constructor signature change from the lazy-properties work is more
than a patch under semver.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018iNBFSQM7xGLoYg9Y5EJx4
@fivefilters fivefilters changed the title Compute Article content-derived properties lazily (4.0.1) Lazy Article properties + metadataOnly option (4.1.0) Jul 30, 2026
@fivefilters
fivefilters marked this pull request as ready for review July 30, 2026 17:50
@fivefilters
fivefilters merged commit 0e2370d into master Jul 30, 2026
4 checks passed
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.

2 participants