phpunit/php-token-stream
phpunit/php-token-stream is a small PHP library for tokenizing and streaming PHP source code tokens, commonly used by PHPUnit and related tools for parsing, reflection-like inspection, and test-related code analysis.
Installation:
composer require --dev phpunit/php-token-stream
Add to composer.json under require-dev if only needed for testing/analysis.
First Use Case:
Parse a PHP file to detect specific syntax patterns (e.g., echo statements):
use PHPUnit\Util\Token\TokenStream;
$code = '<?php echo "Hello"; ?>';
$tokens = TokenStream::createFromCode($code);
foreach ($tokens as $token) {
if ($token->getId() === T_ECHO) {
echo "Found an echo statement!";
}
}
Where to Look First:
T_FUNCTION, T_CLASS).createFromCode(), createFromFile(), and iteration methods.Token Stream Iteration: Process tokens sequentially to avoid memory overload:
$tokens = TokenStream::createFromFile('src/Example.php');
foreach ($tokens as $token) {
if ($token->isGiven(T_VARIABLE)) {
// Handle variable usage
}
}
Context-Aware Parsing: Track state (e.g., inside a class/method) to enforce rules:
$tokens = TokenStream::createFromCode($code);
$inClass = false;
foreach ($tokens as $token) {
if ($token->isGiven(T_CLASS)) {
$inClass = true;
} elseif ($token->isGiven(T_FUNCTION) && $inClass) {
// Parse method body
}
}
Token Manipulation: Rebuild code by collecting and rewriting tokens (e.g., for refactoring):
$tokens = TokenStream::createFromCode($code);
$rewrittenTokens = [];
foreach ($tokens as $token) {
if ($token->getId() === T_ECHO) {
$rewrittenTokens[] = new Token(T_PRINT, 'print');
} else {
$rewrittenTokens[] = $token;
}
}
$rewrittenCode = Token::toCode($rewrittenTokens);
File-Level Analysis:
Process entire directories with SplFileInfo:
$iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator('src'));
foreach ($iterator as $file) {
if ($file->isFile() && $file->getExtension() === 'php') {
$tokens = TokenStream::createFromFile($file->getPathname());
// Analyze tokens...
}
}
Custom Linter:
mysql_* functions:
$tokens = TokenStream::createFromCode($code);
$deprecatedFunctions = ['mysql_connect', 'mysql_query'];
foreach ($tokens as $token) {
if ($token->isGiven(T_STRING) && in_array($token->getValue(), $deprecatedFunctions)) {
throw new \RuntimeException("Deprecated function used: {$token->getValue()}");
}
}
Test Data Extraction:
@dataProvider arguments dynamically:
$tokens = TokenStream::createFromFile('tests/ExampleTest.php');
$dataProviders = [];
$inDocBlock = false;
foreach ($tokens as $token) {
if ($token->isGiven(T_DOC_COMMENT)) {
$inDocBlock = true;
if (preg_match('/@dataProvider\s+(\w+)/', $token->getValue(), $matches)) {
$dataProviders[] = $matches[1];
}
} elseif ($token->isGiven(T_WHITESPACE)) {
$inDocBlock = false;
}
}
Code Metrics:
$tokens = TokenStream::createFromCode($code);
$complexity = 0;
foreach ($tokens as $token) {
if (in_array($token->getId(), [T_IF, T_FOR, T_WHILE, T_SWITCH])) {
$complexity++;
}
}
Combine with Other Tools:
nikic/php-parser for AST-level operations after token analysis.symfony/finder for file discovery:
use Symfony\Component\Finder\Finder;
$finder = Finder::create()->files()->in('src')->name('*.php');
foreach ($finder as $file) {
$tokens = TokenStream::createFromFile($file->getPathname());
// Analyze...
}
Performance Optimization:
$tokens = TokenStream::createFromFile('large_file.php');
while ($tokens->valid()) {
$token = $tokens->current();
// Process token...
$tokens->next();
}
Error Handling:
try {
$tokens = TokenStream::createFromCode($code);
} catch (\RuntimeException $e) {
// Handle malformed PHP (e.g., log or skip file)
}
Tokenizer Extension Dependency:
tokenizer extension must be enabled. Some shared hosting environments disable it.if (!extension_loaded('tokenizer')) {
throw new \RuntimeException('Tokenizer extension is required. Enable it in php.ini.');
}
PHP Version Incompatibilities:
php-token-stream may not recognize them.$tokens = TokenStream::createFromCode('[Attribute] class Example {}');
// Check for T_ATTRIBUTE (PHP 8.1+)
Token Stream State:
TokenStream consumes it. Reusing the same stream requires re-creation.$tokens = TokenStream::createFromCode($code)->toArray();
Heredoc and Nowdoc:
Dynamic Code:
eval(), create_function(), or $$vars may not be tokenized predictably.Whitespace Sensitivity:
Token::toCode() carefully—it may not match original formatting.Token Dumping:
$tokens = TokenStream::createFromCode($code)->toArray();
print_r(array_map(function($token) {
return [
'id' => $token->getId(),
'value' => $token->getValue(),
'line' => $token->getLine(),
];
}, $tokens));
Line/Column Tracking:
getLine() and getColumn() to pinpoint issues:
foreach ($tokens as $token) {
if ($token->getId() === T_ERROR) {
echo "Error at line {$token->getLine()}, column {$token->getColumn()}";
}
}
Edge Cases:
TokenStream::createFromCode($phpCode) for strings.TokenStream::createFromFile($filePath) for files. Note: Files are read once and cached in memory.How can I help you explore Laravel packages today?