Repository navigation
Releases: DariusIII/Net_NNTP
Release list
Release 4.1.3
Release Notes
Performance: Built-in function call optimisation (PHP 8.5)
Background
PHP 8.5's Zend compiler (zend_compile.c line 4975)
contains a function — zend_try_compile_special_func_ex — that compiles certain built-in calls
directly to single VM opcodes, completely bypassing the normal function-call overhead
(no frame push/pop, no argument marshalling, no symbol table lookup):
| Function(s) | Opcode emitted |
|---|---|
strlen |
ZEND_STRLEN |
is_null, is_bool, is_int, is_string, is_array, is_object, is_resource |
ZEND_TYPE_CHECK |
count / sizeof |
ZEND_COUNT |
in_array |
ZEND_IN_ARRAY |
array_slice |
ZEND_ARRAY_SLICE |
array_key_exists |
ZEND_ARRAY_KEY_EXISTS |
intval, strval, boolval, floatval / doubleval |
ZEND_CAST |
sprintf |
ZEND_FAST_CONCAT-like |
chr, ord |
ZEND_CHR / ZEND_ORD |
get_class, get_called_class, gettype |
ZEND_GET_CLASS / ZEND_GET_TYPE |
The catch: this intrinsification only fires when the call is resolved at compile time.
Inside a namespace (e.g. DariusIII\NetNntp\Protocol), an unqualified call like trim() forces
the engine to check at runtime whether DariusIII\NetNntp\Protocol\trim exists before falling back
to the global. Prefixing with \ eliminates that runtime lookup and allows the compiler to
intrinsify or use the faster frameless call path.
Changes
src/Protocol/Client.php
-
Added
\prefix to all unqualified built-in function calls, enabling compile-time resolution
and opcode intrinsification:
openssl_error_string,strlen,strpbrk,fwrite,fgets,stream_get_meta_data,
trim,substr,feof,str_ends_with,str_starts_with,str_replace,explode,
reset,next,stream_socket_client,stream_set_timeout,stream_set_read_buffer,
fclose,stream_socket_enable_crypto,ltrim,gmdate,implode,strcasecmp,
is_numeric,is_array,is_resource,is_string. -
Removed redundant
|| strlen($line) < 2branch in_getTextResponse().
str_ends_with($line, "\r\n")returningtruealready guaranteesstrlen ≥ 2, so the second
operand was dead code that emitted aZEND_STRLENopcode on every iteration of the hot
receive loop for no reason. -
Removed redundant
(string)cast onsubstr($response, 4)in_getStatusResponse().
substr()always returnsstringwhen given astringinput, so theZEND_CASTopcode
was emitted unnecessarily.
src/Client.php
- Added
\prefix to all unqualified built-in function calls:
implode,strtotime,substr,array_merge,array_keys,array_values,
array_combine,array_slice,array_fill,array_splice,strpos,ltrim,
ctype_digit,str_starts_with,str_ends_with,reset,preg_split,trim,
is_int,is_array,is_string,count.
Other housekeeping
- Removed legacy
package.xml(PEAR descriptor, superseded entirely bycomposer.json).
Release 4.1.2
Performance Improvements in src/
A collection of targeted speed improvements to the NNTP client's hot paths: socket I/O loops, response parsing, and article processing.
All 290 tests pass. PHPStan reports zero errors.
Changes
1. Replace preg_replace with str_replace in _sendArticle()
File: src/Protocol/Client.php
The NNTP dot-stuffing substitution ("\n." → "\n..") is a literal string replacement — no regex features are used. str_replace avoids regex compilation overhead entirely.
Before:
@fwrite($this->_socket, preg_replace("|\n\.|", "\n..", $article) . "\r\n.\r\n");
// ...
@fwrite($this->_socket, preg_replace("|\n\.|", "\n..", $header) . "\r\n" . preg_replace("|\n\.|", "\n..", $body) . "\r\n.\r\n");After:
@fwrite($this->_socket, str_replace("\n.", "\n..", $article) . "\r\n.\r\n");
// ...
@fwrite($this->_socket, str_replace("\n.", "\n..", $header) . "\r\n" . str_replace("\n.", "\n..", $body) . "\r\n.\r\n");2. Reduce redundant trimming in _getStatusResponse()
File: src/Protocol/Client.php
The response was being rtrim'd for logging, then ltrim'd for parsing, then rtrim'd again inside the substr. A single trim() replaces all three operations.
Before:
$this->_logger?->debug('S: ' . rtrim($response, "\r\n"));
$response = ltrim($response);
$this->_currentStatusResponse = [
(int) substr($response, 0, 3),
(string) rtrim(substr($response, 4)),
];After:
$response = trim($response);
$this->_logger?->debug('S: ' . $response);
$this->_currentStatusResponse = [
(int) substr($response, 0, 3),
(string) substr($response, 4),
];3. Throttle _clearOpensslErrors() in _getTextResponse()
File: src/Protocol/Client.php
Previously called _clearOpensslErrors() twice per line read (before and after fgets) inside the reception loop. For large multi-line responses over TLS (e.g., overview or article body fetches with thousands of lines), this called openssl_error_string() in a while loop on every single line — a significant overhead.
Now clears once before the loop and only on the error path.
Before:
while (!feof($this->_socket)) {
if ($hasEncryption) {
$this->_clearOpensslErrors();
}
$received = @fgets($this->_socket, 8192);
if ($hasEncryption) {
$this->_clearOpensslErrors();
}
if ($received === false) {
$meta = stream_get_meta_data($this->_socket);
// ...
}After:
// Clear any stale OpenSSL errors once before entering the loop
if ($hasEncryption) {
$this->_clearOpensslErrors();
}
while (!feof($this->_socket)) {
$received = @fgets($this->_socket, 8192);
if ($received === false) {
if ($hasEncryption) {
$this->_clearOpensslErrors();
}
$meta = stream_get_meta_data($this->_socket);
// ...
}4. Limit explode() in group-parsing methods
File: src/Protocol/Client.php
Added a limit parameter to explode() in _parseGroupLines() and cmdListActive() to stop splitting beyond the required number of fields. Avoids unnecessary array allocations, especially for large group lists.
Before:
// _parseGroupLines()
$arr = explode(' ', trim($line));
// cmdListActive()
$arr = explode(' ', trim($line));After:
// _parseGroupLines()
$arr = explode(' ', trim($line), 4);
// cmdListActive()
$arr = explode(' ', trim($line), 4);5. Disable PHP userspace read buffer on socket
File: src/Protocol/Client.php
Added stream_set_read_buffer($this->_socket, 0) after socket creation to eliminate PHP's internal read buffer, reducing double-buffering with the OS TCP buffer.
Before:
stream_set_timeout($this->_socket, $timeout);After:
stream_set_timeout($this->_socket, $timeout);
stream_set_read_buffer($this->_socket, 0);6. Fast-path overview parsing with array_combine
File: src/Client.php
When no "full" header fields exist in the overview format (the common case), the per-field strpos/substr loop is now skipped entirely in favor of a single array_combine() call. The slow path with header stripping is only used when actually needed.
Before:
foreach ($overview as $key => $article) {
$mapped = [];
for ($i = 0; $i < $fieldCount; $i++) {
$value = $article[$i] ?? '';
if (isset($fullIndices[$i])) {
$pos = strpos($value, ':');
$value = ltrim(substr($value, ($pos === false ? 0 : $pos + 1)), " \t");
}
$mapped[$fieldNames[$i]] = $value;
}
$overview[$key] = $mapped;
}After:
if ($fullIndices === []) {
// Fast path: no "full" header fields — use array_combine directly
foreach ($overview as $key => $article) {
$overview[$key] = array_combine(
$fieldNames,
\array_slice($article, 0, $fieldCount) + array_fill(0, $fieldCount, '')
);
}
} else {
foreach ($overview as $key => $article) {
$mapped = [];
for ($i = 0; $i < $fieldCount; $i++) {
$value = $article[$i] ?? '';
if (isset($fullIndices[$i])) {
$pos = strpos($value, ':');
$value = ltrim(substr($value, ($pos === false ? 0 : $pos + 1)), " \t");
}
$mapped[$fieldNames[$i]] = $value;
}
$overview[$key] = $mapped;
}
}7. Replace explode + array_filter + array_values chain with preg_split
File: src/Client.php
In getReferences(), three chained function calls (plus a closure allocation per iteration) are replaced with a single preg_split() call using PREG_SPLIT_NO_EMPTY.
Before:
foreach ($references as $key => $val) {
$references[$key] = array_values(
array_filter(explode(' ', trim($val)), static fn(string $s): bool => $s !== '')
);
}After:
foreach ($references as $key => $val) {
$references[$key] = preg_split('/\s+/', trim($val), -1, PREG_SPLIT_NO_EMPTY);
}Summary
| # | Location | Optimization | Impact |
|---|---|---|---|
| 1 | Protocol/Client::_sendArticle() |
preg_replace → str_replace |
Eliminates regex compilation on every article post/transfer |
| 2 | Protocol/Client::_getStatusResponse() |
Single trim() instead of 3 separate trims |
Removes redundant string scans on every NNTP response |
| 3 | Protocol/Client::_getTextResponse() |
_clearOpensslErrors() moved out of per-line loop |
Eliminates 2× openssl_error_string() drain per line in TLS responses |
| 4 | Protocol/Client::_parseGroupLines() / cmdListActive() |
explode() with limit parameter |
Stops splitting beyond required fields on large group lists |
| 5 | Protocol/Client::connect() |
stream_set_read_buffer($socket, 0) |
Reduces double-buffering between PHP and OS TCP buffer |
| 6 | Client::getOverview() |
array_combine fast-path |
Skips per-field loop entirely when no full-header stripping needed |
| 7 | Client::getReferences() |
preg_split replaces 3-function chain |
One call instead of three; no closure allocation per iteration |
Release 4.1.1
Release 4.1.0
Release v4.1.0
Namespace rename:
- Renamed root namespace from
Net\NNTPtoDariusIII\NetNntp:Net\NNTP\Client→DariusIII\NetNntp\ClientNet\NNTP\Error→DariusIII\NetNntp\ErrorNet\NNTP\Protocol\Client→DariusIII\NetNntp\Protocol\ClientNet\NNTP\Protocol\ResponseCode→DariusIII\NetNntp\Protocol\ResponseCode
- Updated
composer.jsonPSR-4 autoload to"DariusIII\\NetNntp\\": "src/" - Added
autoload-devPSR-4 entry for test namespace"DariusIII\\NetNntp\\Tests\\": "tests/" - Updated all source files, tests, docs, and examples to use new namespace
Removed all backward-compatible shims:
- Deleted
src/Protocol/Responsecode.php— thedefine()constants shim file- All
NET_NNTP_PROTOCOL_RESPONSECODE_*global constants are no longer defined - Use the
DariusIII\NetNntp\Protocol\ResponseCodeenum instead
- All
- Removed
filesautoload entry fromcomposer.json(no longer needed) - Removed all deprecated API v1.0 and v1.1 methods from
Client:quit()— usedisconnect()isConnected()— was deprecated wrappergetArticleRaw()— usegetArticle()getHeaderRaw()— usegetHeader()getBodyRaw()— usegetBody()getNewNews()— usegetNewArticles()getReferencesOverview()— usegetReferences()
- Removed v1.0 connect shim (passing port as second arg) from both
ClientandProtocol\Client - Removed v1.1 class-name-as-string shim from
getArticle(),getHeader(),getBody()—$implodeis now strictlybool - Removed v1.0 multi-argument shim from
post()— parameter is nowstring|array - Removed v1.0 two-argument shim from
getOverview()
API tightening:
getNewGroups()/getNewArticles()$timeparameter:mixed→int|string_resolveTimestamp()now throws\InvalidArgumentExceptioninstead of returningErrorpost()parameter now typedstring|array(usemail()for the old multi-arg posting style)- Removed
@package_version@/@package_state@PEAR placeholders from X-poster mail header
Tests:
- Added
testDeprecatedMethodsRemoved()— verifies all 7 removed methods no longer exist - Added
testLegacyConstantsNoLongerDefined()— verifies global constants shim is gone - Added type-assertion tests for cleaned-up method signatures (
$implodeisbool,post()isstring|array) - Removed all legacy constant shim tests (47 removed)
- Removed deprecated method existence tests
- Test suite: 290 tests, 441 assertions
Bugfix release
Version 4.0.0
Release v4.0.0
Project structure & namespaces:
- Modernized project to PSR-4 standard — source files moved from
NNTP/tosrc/ - Added PHP namespaces:
Net\NNTP,Net\NNTP\Protocol - Renamed classes from PEAR underscore style to namespaced short names:
Net_NNTP_Client→Net\NNTP\ClientNet_NNTP_Error→Net\NNTP\ErrorNet_NNTP_Protocol_Client→Net\NNTP\Protocol\Client
- Updated
composer.jsonautoload from classmap to PSR-4 ("Net\\NNTP\\": "src/") - Added
filesautoload entry forResponsecode.phpglobal constants - Removed all
require_onceincludes from source files (now handled by Composer autoload) - Removed old
NNTP/directory
PEAR removal:
- Removed all PEAR dependencies, code, and references
- Removed
PEAR_LOG_DEBUGconstant and all_isMasked()calls fromProtocol\Client - Replaced all
PEAR::isError()calls with\Net\NNTP\Error::isError()throughout - Removed
require_once "Log.php"andrequire_once "PEAR.php"from demo files - Removed
grabPearErrors()/PEAR::setErrorHandling()from demo - Removed
PEAR_Errorreferences fromErrorclass - Updated
X-postermail header: removedPEAR::prefix - Replaced all
@link http://pear.php.net/package/Net_NNTPwith GitHub URL
PSR-3 logging:
- Added
psr/logas a dependency — the library now accepts any PSR-3 compatible logger - Type-hinted
$_loggerproperty andsetLogger()parameter withPsr\Log\LoggerInterface - Made
setLogger()public (wasprotected) - Removed deprecated
setDebug()method fromProtocol\Client - Updated demo
Loggerclass to extendPsr\Log\AbstractLogger
Configuration:
- Added
vlucas/phpdotenvas a dependency for demo configuration - Replaced hardcoded
config.inc.phpwith.env-based configuration using phpdotenv - Added
.env.examplewith all available settings and documented defaults - Added
.envto.gitignore
Test suite:
- Added comprehensive PHPUnit 13 test suite (117 tests, 253 assertions):
tests/Unit/ErrorTest.php— Error class construction, isError(), toStringtests/Unit/ClientTest.php— Client instantiation, PSR-3 logger, public API surfacetests/Unit/ProtocolClientTest.php— Protocol layer, throwError, _isConnected, _clearOpensslErrorstests/Unit/ResponsecodeTest.php— All 48 NNTP response code constantstests/Unit/AutoloadTest.php— PSR-4 autoloading, namespaces, dependency wiringtests/Unit/ConfigTest.php—.env.examplecompleteness, phpdotenv integrationtests/Integration/ServerConnectionTest.php— Live server: connect, groups, articles, overview, disconnect
- Updated
phpunit.xmlwith separate Unit and Integration test suites - Updated demo and phpdoc examples to use namespaced classes
Enums & code optimization:
- Created
Net\NNTP\Protocol\ResponseCodeint-backed enum with all 53 NNTP response codes- Each case includes a
description()method returning the RFC description - Supports
ResponseCode::from(int)andResponseCode::tryFrom(int)natively - Added 6 new codes not previously defined:
DisconnectingRequested(205),DisconnectingForced(400),TlsContinue(382),TlsRefused(580),XgtitleFollows(282),XgtitleUnavailable(481)
- Each case includes a
- Converted
Responsecode.phpto a backwards-compatible shim — alldefine()constants now delegate toResponseCode::CASE->value - Replaced all
NET_NNTP_PROTOCOL_RESPONSECODE_*references inProtocol\ClientwithResponseCode::CASE->value - Replaced all bare integer response codes (205, 382, 400, 502, 503, etc.) with enum references
- Converted all
switchblocks tomatchexpressions throughoutProtocol\ClientandClient - Removed all unreachable
breakstatements afterreturn(~50+ instances) - Removed dead code:
if (false) { ... }block,die()calls, unused namespace-level constants - Removed all vim modelines,
{{{ }}}fold markers, and "Local variables" blocks from all source files - Added
declare(strict_types=1)to all source files - Applied constructor property promotion with
readonlytoErrorclass - Refactored duplicated parsing logic in
Protocol\Clientinto private helper methods:
_parseGroupSelected(),_parseArticlePointer(),_fetchTextAndLog(),_parseGroupLines(),
_parseOverviewResponse(),_parseKeyValueResponse(),_parseOverviewFormat(),
_parseNewsgroupDescriptions(),_logAndReturn(),_handleTlsNegotiation() - Extracted
_resolveTimestamp()helper inClientto deduplicate time parsing - Used
str_ends_with()/str_starts_with()instead ofsubstr()comparisons - Used null coalescing assignment (
??=) throughout - Used nullsafe operator (
?->) consistently for logger calls - Used union types for
_sendArticle(string|array)parameter - Expanded test suite to 335 tests / 526 assertions (was 117 / 253):
- Added enum tests: case values,
from(),tryFrom(),description(), int-backed verification - Added legacy constant shim verification
- Added
ResponseCodeautoload and namespace tests
- Added enum tests: case values,
Static analysis:
- Added PHPStan (
phpstan/phpstan) as a dev dependency - Created
phpstan.neonconfiguration at level 8 (strict) - All source files pass PHPStan level 8 with zero errors
- Added proper type annotations (PHPDoc
@return,@param) to all methods throughoutProtocol\ClientandClient - Typed all deprecated method parameters and return types (
getArticleRaw(),getHeaderRaw(), etc.) - Changed
getArticle(),getHeader(),getBody()$implodeparameter frombooltomixedfor v1.1 API backward compatibility - Added
@varPHPDoc annotations to all class properties - Added
.phpstan-cache/to.gitignore - Added
composer scripts:composer analyse,composer test,composer check
Release 3.0.2
What's Changed
- Optimize NNTP hot paths, fix command-length guard, and add XOVER-only integration benchmark by @CicerBro in #4
Full Changelog: 3.0.1...3.0.2