# Blade Parser
Blade Parser is a powerful PHP library for Laravel that enables parsing, analyzing, and manipulating Blade templates programmatically. The library provides a comprehensive set of tools for working with Blade syntax, including directives, components, echo statements, PHP blocks, and more. It is designed for developers who need to build code analysis tools, linters, formatters, or any application that requires deep inspection of Blade templates.
The core functionality revolves around four major components: the **Parser** which produces a list of nodes from Blade templates, **Documents** which provide an abstraction layer for interacting with parsed templates, **Workspaces** which allow working with multiple templates at once, and the **Compiler** which transforms Blade templates into PHP. Additionally, the library includes an extensible **Validator** system capable of detecting common issues like unpaired conditions, inconsistent indentation, and invalid component parameters.
## Document API - Parsing Templates
The Document class is the primary entry point for parsing and analyzing Blade templates. It provides methods to parse templates from text or files and query the resulting node structure.
```php
Hello, {{ $user->name }}!
@if ($showDetails)
User ID: {{ $user->id }}
@endif
@endsection
BLADE;
$document = Document::fromText($template);
// Or parse from a file path
$document = Document::fromText(file_get_contents('resources/views/welcome.blade.php'), 'resources/views/welcome.blade.php');
// Get all nodes in the document
$nodes = $document->getNodes(); // Returns NodeCollection
// Get the total number of lines
$lineCount = $document->getLineCount(); // Returns int
// Convert document back to string (reflects any modifications)
$output = $document->toString(); // or (string) $document
```
## Querying Directives
The Document class provides methods to find and filter directives within parsed templates. Directives include control structures like `@if`, `@foreach`, includes, sections, and custom directives.
```php
getDirectives(); // Returns NodeCollection of DirectiveNode
// Find a single directive by name
$extends = $document->findDirectiveByName('extends');
if ($extends) {
$layout = $extends->arguments->getStringValue(); // Returns 'layouts.app'
}
// Find all directives with a specific name
$includes = $document->findDirectivesByName('include');
$includes->each(function ($directive) {
echo "Found include: " . $directive->arguments->innerContent . "\n";
});
// Output:
// Found include: 'partials.header'
// Found include: 'partials.content'
// Found include: 'partials.footer'
// Check if a directive exists
$hasIf = $document->hasDirective('if'); // Returns true
$hasAuth = $document->hasDirective('auth'); // Returns false
// Check if document has any directives
$hasDirectives = $document->hasAnyDirectives(); // Returns true
```
## Querying Echo Statements
Echo nodes represent Blade's output syntax including regular echo `{{ }}`, raw echo `{!! !!}`, and triple echo `{{{ }}}`.
```php
{{ $escapedContent }}
{!! $rawHtml !!}
{{{ $tripleEscaped }}}
BLADE;
$document = Document::fromText($template);
// Get all echo statements
$echoes = $document->getEchoes();
echo "Found {$echoes->count()} echo statements\n"; // Output: Found 3 echo statements
// Filter by echo type
$echoes->each(function ($echo) {
match ($echo->type) {
EchoType::Echo => print "Normal echo: {$echo->innerContent}\n",
EchoType::RawEcho => print "Raw echo: {$echo->innerContent}\n",
EchoType::TripleEcho => print "Triple echo: {$echo->innerContent}\n",
};
});
// Output:
// Normal echo: $escapedContent
// Raw echo: $rawHtml
// Triple echo: $tripleEscaped
// Access the inner content (without delimiters)
$firstEcho = $echoes->first();
$content = $firstEcho->innerContent; // Returns ' $escapedContent '
```
## Querying Components
The Document class provides comprehensive methods for finding and analyzing Blade components, including both class-based and anonymous components.
```php
Card Header
Save
BLADE;
$document = Document::fromText($template);
// Get all component tags (including closing tags)
$allComponents = $document->getComponents();
echo "Total component tags: {$allComponents->count()}\n"; // Includes opening and closing tags
// Get only opening/self-closing component tags
$openingTags = $document->getOpeningComponentTags();
echo "Opening/self-closing components: {$openingTags->count()}\n";
// Find components by tag name
$slots = $document->findComponentsByTagName('slot');
$alerts = $document->findComponentsByTagName('alert');
// Find a single component by tag name
$button = $document->findComponentByTagName('button');
if ($button) {
echo "Button inner content: {$button->innerContent}\n";
// Access component parameters
$button->getParameters()->each(function ($param) {
echo "Parameter: {$param->name} = {$param->value}\n";
});
}
// Check component properties
$alert = $document->findComponentByTagName('alert');
if ($alert) {
echo "Is self-closing: " . ($alert->isSelfClosing ? 'yes' : 'no') . "\n"; // yes
echo "Tag name: {$alert->getTagName()}\n"; // alert
// Check for specific parameters
if ($alert->hasParameter('type')) {
$typeParam = $alert->getParameter('type');
echo "Type value: {$typeParam->value}\n"; // danger
}
}
```
## Querying PHP Blocks and Tags
The Document class can retrieve PHP code blocks created with `@php`/`@endphp` directives as well as raw PHP tags.
```php
= $pageTitle ?>
@php
$users = User::where('active', true)->get();
$count = $users->count();
@endphp
@foreach ($users as $user)
{{ $user->name }}
@endforeach
@php ($timestamp = now())
BLADE;
$document = Document::fromText($template);
// Get @php/@endphp blocks (paired directives without arguments)
$phpBlocks = $document->getPhpBlocks();
echo "PHP blocks: {$phpBlocks->count()}\n"; // Output: 1 (only the multi-line @php/@endphp)
// Note: @php ($timestamp = now()) is a directive with arguments, not a PhpBlockNode
// Get raw PHP tags ( and = ?>)
$phpTags = $document->getPhpTags();
echo "PHP tags: {$phpTags->count()}\n"; // Output: 3
$phpTags->each(function ($tag) {
echo "PHP tag content: " . trim($tag->content) . "\n";
});
// Output:
// PHP tag content:
// PHP tag content: = $pageTitle ?>
// PHP tag content: = $timestamp ?>
```
## Querying Comments and Verbatim Blocks
Retrieve Blade comments and verbatim blocks that prevent Blade processing.
```php
{{-- TODO: Add user avatar --}}
{{ $user->name }}
@verbatim
@endforelse
BLADE;
$document = Document::fromText($template);
// Resolve structural relationships (required for structure queries)
$document->resolveStructures();
// Get all conditional structures (including nested)
$allConditions = $document->getAllConditions();
echo "Total conditions: {$allConditions->count()}\n"; // Output: Total conditions: 3
// Get only root-level conditions (not nested)
$rootConditions = $document->getRootConditions();
echo "Root conditions: {$rootConditions->count()}\n"; // Output: Root conditions: 2
// Get all structures (conditions, loops, etc.)
$allStructures = $document->getAllStructures();
// Get forelse structures
$forElse = $document->getAllForElse();
echo "ForElse blocks: {$forElse->count()}\n"; // Output: ForElse blocks: 1
// Access directive relationships
$ifDirective = $document->findDirectiveByName('if');
if ($ifDirective && $ifDirective->isClosedBy) {
echo "If is closed by: @{$ifDirective->isClosedBy->content}\n"; // Output: If is closed by: @endif
// Get all chained closing directives (elseif, else, endif)
$chain = $ifDirective->getChainedClosingDirectives();
echo "Chain length: {$chain->count()}\n";
}
```
## Switch Statement Analysis
Analyze `@switch`/`@case`/`@default`/`@endswitch` structures in templates.
```php
Pending
@break
@case('approved')
Approved
@break
@case('rejected')
@switch($rejectionReason)
@case('spam')
Marked as spam
@break
@default
Rejected
@endswitch
@break
@default
Unknown
@endswitch
BLADE;
$document = Document::fromText($template);
$document->resolveStructures();
// Get all switch statements (including nested)
$allSwitches = $document->getAllSwitchStatements();
echo "Total switch statements: {$allSwitches->count()}\n"; // Output: Total switch statements: 2
// Get only root-level switch statements
$rootSwitches = $document->getRootSwitchStatements();
echo "Root switch statements: {$rootSwitches->count()}\n"; // Output: Root switch statements: 1
```
## Node Pattern Matching
Find sequences of specific node types within a document using pattern matching.
```php
findNodePattern($pattern);
echo "Found " . count($matches) . " consecutive echo pairs\n"; // Output: Found 2 consecutive echo pairs
foreach ($matches as $match) {
// Each match is an array of nodes including the literal between them
$firstEcho = $match[0]->content;
$secondEcho = $match[2]->content; // Index 2 because index 1 is the literal between
echo "Match: {$firstEcho} followed by {$secondEcho}\n";
}
// Output:
// Match: {{ $one }} followed by {{ $two }}
// Match: {{ $two }} followed by {{ $three }}
// Find echo followed by directive
$pattern2 = [EchoNode::class, DirectiveNode::class];
$matches2 = $document->findNodePattern($pattern2);
echo "Echo-Directive sequences: " . count($matches2) . "\n";
```
## Node Traversal and Navigation
Navigate through document nodes relative to a specific node.
```php
getEchoes()->first();
// Get all nodes before this echo
$nodesBefore = $document->getNodesBefore($echo);
echo "Nodes before echo: {$nodesBefore->count()}\n"; // Output: Nodes before echo: 3
// Get all nodes after this echo
$nodesAfter = $document->getNodesAfter($echo);
echo "Nodes after echo: {$nodesAfter->count()}\n";
// Type-specific queries
$firstDirective = $document->firstOfType(DirectiveNode::class);
$lastDirective = $document->lastOfType(DirectiveNode::class);
$allLiterals = $document->allOfType(LiteralNode::class);
echo "First directive: @{$firstDirective->content}\n"; // Output: First directive: @if
echo "Last directive: @{$lastDirective->content}\n"; // Output: Last directive: @endif
echo "Literal count: {$allLiterals->count()}\n";
```
## Text Extraction and Manipulation
Extract plain text content from templates and perform text-based operations.
```php
extractText();
echo "Unescaped: {$plainText}\n";
// Output: @if ($condition)
// Hello, !
// Welcome to .
// @endif
// Extract text with escape sequences preserved
$escapedText = $document->extractText(false);
echo "Escaped: {$escapedText}\n";
// Output: @@if ($condition)
// Hello, !
// Welcome to .
// @@endif
// Get text at specific offset range
$text = $document->getText(4, 10); // Get 6 characters starting at offset 4
// Get word at a specific offset
$word = $document->getWordAtOffset(5);
// Get line excerpt around a specific line
$lines = $document->getLineExcerpt(3, 2); // Line 3 with 2 lines of context above/below
// Returns: [1 => 'line1', 2 => 'line2', 3 => 'line3', 4 => 'line4', 5 => 'line5']
// Get all lines as array
$allLines = $document->getLines();
```
## Node Modification
Modify directive names, arguments, and component properties then output the changed document.
```php
Click
BLADE;
$document = Document::fromText($template);
// Modify directive name
$ifDirective = $document->findDirectiveByName('if');
$ifDirective->setName('unless');
// Modify directive arguments
$includeDirective = $document->findDirectiveByName('include');
$includeDirective->setArguments("('new-partial')");
// Modify component tag
$button = $document->findComponentByTagName('old-button');
$button->rename('new-button');
// Remove a parameter from component
// $button->removeParameter('type');
// Output modified document
$output = $document->toString();
echo $output;
// Output:
// @unless ($oldCondition)
// @include ('new-partial')
// @endif
//
// Click
// Create a fresh document from modifications (re-parses)
$freshDocument = $document->toDocument();
```
## Error Handling
Detect and handle parsing errors in Blade templates.
```php
getErrors();
echo "Total errors: {$errors->count()}\n";
// Get first error
$firstError = $document->getFirstError();
if ($firstError) {
echo "First error: {$firstError->getErrorMessage()}\n";
// Output: [BLADE_P001001] Unexpected end of input while parsing echo on line 1
}
// Get first fatal error (critical parsing failures)
$fatalError = $document->getFirstFatalError();
if ($fatalError) {
echo "Fatal error: {$fatalError->getErrorMessage()}\n";
// Output: [BLADE_P003001] Unexpected end of input while parsing verbatim on line 3
}
// Check for fatal errors
if ($document->hasFatalErrors()) {
echo "Document has fatal parsing errors!\n";
}
// Check for specific error on a line
$hasEchoError = $document->hasErrorOnLine(1, ErrorType::UnexpectedEndOfInput, ConstructContext::Echo);
// Errors include line/column information
$errors->each(function ($error) {
echo "Line {$error->line}, Column {$error->column}: {$error->getErrorMessage()}\n";
});
```
## Validation System
Use the built-in validation system to detect common issues in Blade templates.
```php
withCoreValidators();
// Or add specific validators
$document->withValidator(new InconsistentIndentationLevelValidator);
$document->withValidator(new UnpairedConditionValidator);
$document->withValidator(new EmptyConditionValidator);
// Run validation
$document->validate();
// Get validation errors
$validationErrors = $document->getValidationErrors();
echo "Validation issues found: {$validationErrors->count()}\n";
$validationErrors->each(function ($error) {
echo "{$error->getErrorMessage()}\n";
});
// Output examples:
// [BLADE_V011] Inconsistent indentation level of 7 for [@endif]; parent [@if] has a level of 0 on line 3
// [BLADE_V002] Empty @if directive on line 5
// [BLADE_V005] Duplicate condition expression [$x] in @elseif on line 9
// Get validator instance for more control
$validator = $document->validator();
echo "Total validators: {$validator->getValidatorCount()}\n";
```
## Custom Validators
Create custom validators to enforce project-specific rules.
```php
type === \Stillat\BladeParser\Nodes\EchoType::RawEcho) {
return new ValidationResult(
$node,
'Raw echo {!! !!} is not allowed. Use {{ }} with proper escaping.'
);
}
}
return null;
}
}
class DeprecatedDirectiveValidator extends AbstractNodeValidator
{
private array $deprecated = ['inject', 'php'];
public function validate(AbstractNode $node): ValidationResult|array|null
{
if ($node instanceof DirectiveNode) {
if (in_array($node->content, $this->deprecated)) {
return new ValidationResult(
$node,
"Directive @{$node->content} is deprecated in this project."
);
}
}
return null;
}
}
$template = <<<'BLADE'
{!! $unsafeHtml !!}
@inject('service', 'App\Services\MyService')
{{ $safeContent }}
BLADE;
$document = Document::fromText($template);
$document->withValidator(new NoRawEchoValidator);
$document->withValidator(new DeprecatedDirectiveValidator);
$document->validate();
$document->getValidationErrors()->each(function ($error) {
echo "{$error->getErrorMessage()}\n";
});
// Output:
// Raw echo {!! !!} is not allowed. Use {{ }} with proper escaping.
// Directive @inject is deprecated in this project.
```
## Workspace API
Work with multiple Blade templates at once using the Workspace class.
```php
addDirectory('/path/to/resources/views');
// Or add individual files
$workspace->addFile('/path/to/specific/template.blade.php');
// Get document count
echo "Templates found: {$workspace->getDocumentCount()}\n";
// Get all documents
$documents = $workspace->getDocuments();
// Find directives across all templates
$allIncludes = $workspace->findDirectivesByName('include');
echo "Total @include directives: " . count($allIncludes) . "\n";
// Each directive knows its source document
foreach ($allIncludes as $include) {
$filePath = $include->getDocument()->getFilePath();
echo "Include in: {$filePath}\n";
}
// Validate all templates in workspace
$workspace->withCoreValidators();
$errors = $workspace->validate();
// Get all validation errors across workspace
$allErrors = $workspace->getValidationErrors();
foreach ($allErrors as $error) {
echo "{$error->getErrorMessage()}\n";
}
// Clean up when done
$workspace->cleanUp();
```
## Compiler API
Compile Blade templates to PHP using the compiler.
```php
BLADE;
// Method 1: Compile via Document
$document = Document::fromText($template);
$compiled = $document->compile();
echo $compiled;
// Output: PHP code with statements
// Method 2: Use compiler directly with options
$document = Document::fromText($template);
$options = new DocumentCompilerOptions();
$options->failOnParserErrors = true;
$options->failStrictly = false;
try {
$compiled = $document->compile($options);
} catch (\Stillat\BladeParser\Errors\Exceptions\CompilationException $e) {
echo "Compilation failed: {$e->getMessage()}\n";
}
// Method 3: Use CompilerFactory for more control
$compiler = CompilerFactory::makeCompiler();
// Configure compiler
$compiler->setFailOnParserErrors(true);
$compiler->ignoreDirectives(['custom']);
// Register custom directives
$compiler->directive('datetime', function ($expression) {
return "";
});
$compiled = $compiler->compileString($template);
```
## Artisan Validation Command
The library includes a configurable `blade:validate` command for validating all Blade templates in a Laravel project.
```bash
# Publish configuration files
php artisan vendor:publish --tag=blade
# Run validation
php artisan blade:validate
# Example output:
# Validating Blade templates...
#
# resources/views/users/index.blade.php
# Line 15: [BLADE_V011] Inconsistent indentation level of 4 for [@endif]
# Line 23: [BLADE_V002] Empty @if directive
#
# resources/views/layouts/app.blade.php
# Line 45: [BLADE_V001] Unpaired condition directive [@if]
#
# Found 3 issues in 2 files.
```
Configuration in `config/validation.php`:
```php
[
\Stillat\BladeParser\Validation\Validators\UnpairedConditionValidator::class,
\Stillat\BladeParser\Validation\Validators\EmptyConditionValidator::class,
\Stillat\BladeParser\Validation\Validators\InconsistentIndentationLevelValidator::class,
// ... more validators
],
'ignore_directives' => [
'custom_directive',
],
'options' => [
\Stillat\BladeParser\Validation\Validators\DirectiveArgumentSpacingValidator::class => [
'expected_spacing' => 1,
],
],
];
```
## Summary
Blade Parser is ideal for building development tools that need to understand Blade template structure. Common use cases include: creating IDE plugins with syntax highlighting and error detection, building custom linters to enforce team coding standards, developing automated refactoring tools for template migrations, implementing template analysis for security audits, and creating documentation generators that extract information from templates. The library's node-based representation makes it easy to traverse, analyze, and modify any part of a Blade template programmatically.
Integration with existing Laravel projects is straightforward - simply install via Composer and use the Document class as the primary entry point. For large-scale analysis, the Workspace API efficiently handles multiple templates, while the extensible validator system allows teams to create custom rules that match their specific requirements. The compiler maintains compatibility with Laravel's Blade compiler while providing additional hooks for custom processing and analysis.