# 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

@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: // PHP tag content: ``` ## Querying Comments and Verbatim Blocks Retrieve Blade comments and verbatim blocks that prevent Blade processing. ```php {{-- TODO: Add user avatar --}} {{ $user->name }} @verbatim
Vue.js template: {{ message }} Angular template: {{ data.value }}
@endverbatim BLADE; $document = Document::fromText($template); // Get all Blade comments $comments = $document->getComments(); echo "Found {$comments->count()} comments\n"; // Output: Found 2 comments $comments->each(function ($comment) { echo "Comment: {$comment->innerContent}\n"; }); // Output: // Comment: This is a Blade comment // Comment: TODO: Add user avatar // Get verbatim blocks $verbatimBlocks = $document->getVerbatimBlocks(); echo "Found {$verbatimBlocks->count()} verbatim blocks\n"; // Output: Found 1 verbatim blocks $verbatimBlocks->each(function ($verbatim) { echo "Verbatim content:\n{$verbatim->innerContent}\n"; }); ``` ## Structural Analysis - Conditions and Loops The Document class can resolve and analyze structural relationships between directives like `@if`/`@endif`, `@foreach`/`@endforeach`, and nested conditions. ```php isAdmin())
@if ($user->hasPermission('edit')) @elseif ($user->hasPermission('view')) @else No permissions @endif
@endif @forelse ($items as $item) @if ($item->isActive) {{ $item->name }} @endif @empty

No items found

@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.