Lazy Article properties + metadataOnly option (4.1.0) - #52
Merged
Conversation
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
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
marked this pull request as ready for review
July 30, 2026 17:50
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 theArticlewithcontent(a fullinnerHTMLserialization of the extracted body),textContent(a full text walk),length, and the collected image list. Callers that only usecontentElement,hasContent()or the metadata paid that cost — potentially multiple MB of string building per request — for values they never read.content,textContent,lengthandimagesare now virtual properties onArticle(PHP 8.4 property hooks), computed fromcontentElementon first access and cached in private backing fields.hasContent()checkscontentElement !== nullinstead ofcontent !== null, so it no longer forces a serialization.collectImages) moved fromReadabilityontoArticle.Articleconstructor now takes only the metadata andcontentElement.json_encode()andvar_dump()output is preserved viaJsonSerializable::jsonSerialize()and__debugInfo()— same fields, same order as the eager implementation (both trigger the full serialization, as before).2. New
metadataOnlyoptionFor 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, andgrabArticle— 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 duringgrabArticle's traversal, is read directly from the root element instead. The returnedArticlehas the content-derived propertiesnull(hasContent()returnsfalse), the same shape as the no-content case.Behavioral notes
nullin the no-content case,__toString()unchanged.contentElementafter readingcontent/textContentis not reflected (previously the snapshot was taken at parse time). Documented on the class and in the README.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_dumpoutput, andmetadataOnly(metadata extracted, no content, and the passed document byte-identical before/after the parse).composer analyse(Psalm) — no errors.🤖 Generated with [Claude Code]