Try Live
Add Docs
Rankings
Pricing
Docs
Install
Install
Docs
Pricing
More...
More...
Try Live
Rankings
Enterprise
Create API Key
Add Docs
Blade Parser
https://github.com/stillat/blade-parser
Admin
Blade Parser is a Laravel library for parsing, analyzing, and manipulating Blade templates with
...
Tokens:
8,106
Snippets:
37
Trust Score:
7.4
Update:
2 months ago
Context
Skills
Chat
Benchmark
90.4
Suggestions
Latest
Show doc for...
Code
Info
Show Results
Context Summary (auto-generated)
Raw
Copy
Link
# 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 <?php use Stillat\BladeParser\Document\Document; // Parse a Blade template from text $template = <<<'BLADE' @extends('layouts.app') @section('content') <x-alert type="success"> Hello, {{ $user->name }}! </x-alert> @if ($showDetails) <p>User ID: {{ $user->id }}</p> @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 <?php use Stillat\BladeParser\Document\Document; $template = <<<'BLADE' @extends('layouts.app') @section('content') @include('partials.header') @if ($showContent) @include('partials.content') @endif @include('partials.footer') @endsection BLADE; $document = Document::fromText($template); // Get all directives in the document $allDirectives = $document->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 <?php use Stillat\BladeParser\Document\Document; use Stillat\BladeParser\Nodes\EchoType; $template = <<<'BLADE' <div> {{ $escapedContent }} {!! $rawHtml !!} {{{ $tripleEscaped }}} </div> 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 <?php use Stillat\BladeParser\Document\Document; $template = <<<'BLADE' <x-alert type="danger" :message="$errorMessage" /> <x-card> <x-slot:header> Card Header </x-slot> <x-profile :$user /> </x-card> <x-button wire:click="submit">Save</x-button> 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 use Stillat\BladeParser\Document\Document; $template = <<<'BLADE' <?php $pageTitle = 'Welcome'; ?> <h1><?= $pageTitle ?></h1> @php $users = User::where('active', true)->get(); $count = $users->count(); @endphp @foreach ($users as $user) {{ $user->name }} @endforeach @php ($timestamp = now()) <footer>Generated: <?= $timestamp ?></footer> 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 (<?php ?> 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 $pageTitle = 'Welcome'; ?> // PHP tag content: <?= $pageTitle ?> // PHP tag content: <?= $timestamp ?> ``` ## Querying Comments and Verbatim Blocks Retrieve Blade comments and verbatim blocks that prevent Blade processing. ```php <?php use Stillat\BladeParser\Document\Document; $template = <<<'BLADE' {{-- This is a Blade comment --}} <div> {{-- TODO: Add user avatar --}} {{ $user->name }} </div> @verbatim <div> Vue.js template: {{ message }} Angular template: {{ data.value }} </div> @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 <?php use Stillat\BladeParser\Document\Document; $template = <<<'BLADE' @if ($user->isAdmin()) <div class="admin-panel"> @if ($user->hasPermission('edit')) <button>Edit</button> @elseif ($user->hasPermission('view')) <button>View</button> @else <span>No permissions</span> @endif </div> @endif @forelse ($items as $item) @if ($item->isActive) {{ $item->name }} @endif @empty <p>No items found</p> @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 <?php use Stillat\BladeParser\Document\Document; $template = <<<'BLADE' @switch($status) @case('pending') <span class="badge badge-warning">Pending</span> @break @case('approved') <span class="badge badge-success">Approved</span> @break @case('rejected') @switch($rejectionReason) @case('spam') <span>Marked as spam</span> @break @default <span>Rejected</span> @endswitch @break @default <span class="badge badge-secondary">Unknown</span> @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 <?php use Stillat\BladeParser\Document\Document; use Stillat\BladeParser\Nodes\EchoNode; use Stillat\BladeParser\Nodes\DirectiveNode; use Stillat\BladeParser\Nodes\LiteralNode; $template = <<<'BLADE' {{ $one }} {{ $two }} {{ $three }} @if ($condition) {{ $four }} @endif {{ $five }} BLADE; $document = Document::fromText($template); // Find all sequences of two consecutive echo nodes $pattern = [EchoNode::class, EchoNode::class]; $matches = $document->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 <?php use Stillat\BladeParser\Document\Document; use Stillat\BladeParser\Nodes\DirectiveNode; use Stillat\BladeParser\Nodes\EchoNode; use Stillat\BladeParser\Nodes\LiteralNode; $template = <<<'BLADE' @if ($condition) {{ $hello }} @endif {{ $world }} BLADE; $document = Document::fromText($template); // Get the echo node $echo = $document->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 <?php use Stillat\BladeParser\Document\Document; $template = <<<'BLADE' @@if ($condition) Hello, {{ $name }}! Welcome to {{ $siteName }}. @@endif BLADE; $document = Document::fromText($template); // Extract plain text (unescaped - converts @@ to @) $plainText = $document->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 <?php use Stillat\BladeParser\Document\Document; $template = <<<'BLADE' @if ($oldCondition) @include('old-partial') @endif <x-old-button type="button">Click</x-old-button> 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 // // <x-new-button type="button">Click</x-new-button> // Create a fresh document from modifications (re-parses) $freshDocument = $document->toDocument(); ``` ## Error Handling Detect and handle parsing errors in Blade templates. ```php <?php use Stillat\BladeParser\Document\Document; use Stillat\BladeParser\Errors\ConstructContext; use Stillat\BladeParser\Errors\ErrorType; $template = <<<'BLADE' Hello, {{ $world @verbatim Unclosed verbatim BLADE; $document = Document::fromText($template); // Get all errors $errors = $document->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 <?php use Stillat\BladeParser\Document\Document; use Stillat\BladeParser\Validation\Validators\InconsistentIndentationLevelValidator; use Stillat\BladeParser\Validation\Validators\UnpairedConditionValidator; use Stillat\BladeParser\Validation\Validators\EmptyConditionValidator; $template = <<<'BLADE' @if ($condition) @endif @if @endif @if ($x) @elseif ($x) @endif BLADE; $document = Document::fromText($template); // Use all core validators $document->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 <?php use Stillat\BladeParser\Document\Document; use Stillat\BladeParser\Nodes\AbstractNode; use Stillat\BladeParser\Nodes\DirectiveNode; use Stillat\BladeParser\Validation\AbstractNodeValidator; use Stillat\BladeParser\Validation\ValidationResult; // Create a custom validator class NoRawEchoValidator extends AbstractNodeValidator { public function validate(AbstractNode $node): ValidationResult|array|null { if ($node instanceof \Stillat\BladeParser\Nodes\EchoNode) { if ($node->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 <?php use Stillat\BladeParser\Workspaces\Workspace; // Create a workspace and add a directory $workspace = new Workspace(); $workspace->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 <?php use Stillat\BladeParser\Document\Document; use Stillat\BladeParser\Document\DocumentCompilerOptions; use Stillat\BladeParser\Compiler\CompilerFactory; $template = <<<'BLADE' @if ($condition) Hello, {{ $name }}! @else Hello, Guest! @endif <x-alert type="info" :message="$message" /> BLADE; // Method 1: Compile via Document $document = Document::fromText($template); $compiled = $document->compile(); echo $compiled; // Output: PHP code with <?php if(...) ?> 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 "<?php echo date('Y-m-d', strtotime({$expression})); ?>"; }); $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 <?php return [ 'core_validators' => [ \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.