PHPackages                             wik/lexer - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Templating &amp; Views](/categories/templating)
4. /
5. wik/lexer

ActiveLibrary[Templating &amp; Views](/categories/templating)

wik/lexer
=========

A modern AST-based view engine for PHP 8.1+ with custom syntax, layout system, and component support

v0.0.12(1mo ago)012↓100%MITPHPPHP ^8.1

Since Mar 5Pushed 1mo agoCompare

[ Source](https://github.com/tuinhanne/lexer-view-engine)[ Packagist](https://packagist.org/packages/wik/lexer)[ RSS](/packages/wik-lexer/feed)WikiDiscussions main Synced 1mo ago

READMEChangelogDependencies (2)Versions (13)Used By (0)

Wik/Lex
=======

[](#wiklex)

A modern, production-grade **AST-based view engine** for PHP 8.1+. Works with any PHP project — no framework required.

```
composer require wik/lexer

```

---

Features
--------

[](#features)

- **AST compilation pipeline** — Lexer → Parser → Validator → Optimizer → PHP file
- **File cache** with atomic writes; production-mode precompiled index
- **Dependency graph cache** — automatically invalidates compiled templates when a layout, partial, or component they depend on changes
- **Layout inheritance** — `#extends`, `#section`, `#yield`, `#parent`
- **Components** — PascalCase tags, named slots, dynamic props, class mounting
- **`$loop` variable** — full metadata inside every `#foreach`
- **Include system** — `#include`, `#includeIf`, `#includeWhen`, `#includeFirst`
- **Sandbox mode** — expression whitelist, raw-echo control, 50+ always-blocked functions
- **Custom directives** — register any PHP callable as a template directive
- **Config file** — `lex.config.json` at the project root; `Lexer::fromConfig()` factory
- **Chrome DevTools extension** — component tree, section inspector, cache viewer, error overlay, hover inspector ([lex-devtools](../lexer-debug-extension/))

---

Requirements
------------

[](#requirements)

PHP`^8.1`Extensions`mbstring` (recommended), `igbinary` (optional, faster AST cache)Dependencies`phpunit/phpunit`, `psr/http-server-middleware`---

Installation
------------

[](#installation)

```
composer require wik/lexer
```

---

Quick Start
-----------

[](#quick-start)

### Option A — with `lex.config.json` (recommended)

[](#option-a--with-lexconfigjson-recommended)

Create `lex.config.json` in the project root:

```
{
  "viewPaths":  ["views", "resources/views"],
  "production": false,
  "sandbox":    false
}
```

Then use the config-file factory anywhere in your code:

```
use Wik\Lexer\Lexer;

$lexer = Lexer::fromConfig();   // reads lex.config.json, walks up from cwd
echo $lexer->render('home', ['title' => 'Hello World', 'user' => $user]);
```

### Option B — manual fluent API

[](#option-b--manual-fluent-api)

```
use Wik\Lexer\Lexer;

$lexer = (new Lexer())
    ->paths([__DIR__ . '/views']);

echo $lexer->render('home', ['title' => 'Hello World', 'user' => $user]);
```

**`views/home.lex`**

```
{{ $title }}

#isset($user)
    Welcome back, {{ $user->name }}!
#endisset

```

---

Template Syntax
---------------

[](#template-syntax)

### Echo

[](#echo)

SyntaxOutput`{{ $expr }}`HTML-escaped (safe)`{!! $expr !!}`Raw / unescaped```
{{ $post->title }}
{!! $post->htmlBody !!}

```

### Comments

[](#comments)

HTML comments are **stripped at lex time** — they never appear in compiled output.

```

```

### Escaping `#`

[](#escaping-)

Prefix `#` with a backslash to output it literally without triggering a directive:

```
\#truncate($text, 60)

```

Only `\#` followed by a letter is treated as an escape; `\#123` or standalone `\` are output as-is.

---

Directives
----------

[](#directives)

### Conditionals

[](#conditionals)

```
#if($user->isAdmin())
    Admin panel
#elseif($user->isModerator())
    Moderator panel
#else
    Guest view
#endif

```

```
#unless($user->isVerified())
    Please verify your email.
#endunless

```

### Existence Checks

[](#existence-checks)

```
#isset($sidebar)
    {{ $sidebar }}
#endisset

#empty($notifications)
    No new notifications.
#endempty

```

### Loops

[](#loops)

**`#foreach`** — with full `$loop` variable:

```
#foreach($posts as $post)

        {{ $loop->iteration }}. {{ $post->title }}

        #if($loop->first)
            Latest
        #endif

#endforeach

```

**`$loop` properties:**

PropertyTypeDescription`$loop->index``int`0-based position`$loop->iteration``int`1-based position`$loop->count``int`Total items`$loop->remaining``int`Items left after current`$loop->first``bool`Is this the first item?`$loop->last``bool`Is this the last item?`$loop->even``bool`Even index (0, 2, 4…)`$loop->odd``bool`Odd index (1, 3, 5…)`$loop->depth``int`Nesting depth (1 = outermost)`$loop->parent``object|null`Parent `$loop` in nested loops**`#for`**

```
#for($i = 1; $i isNotEmpty())
    {{ $queue->pop() }}
#endwhile

```

**Loop control:**

```
#foreach($items as $item)
    #if($item->isHidden())
        #continue
    #endif

    #if($item->isLast())
        #break
    #endif

    {{ $item->name }}
#endforeach

```

Multi-level break/continue: `#break(2)`, `#continue(2)`

### Switch

[](#switch)

```
#switch($status)
    #case('active')
        Active
    #break
    #case('banned')
        Banned
    #break
    #default
        Unknown
#endswitch

```

---

Includes
--------

[](#includes)

```

#include('partials.header')

#include('partials.nav', ['active' => 'home'])

#includeIf('partials.sidebar')

#includeWhen($user->isAdmin(), 'partials.admin-bar')

#includeFirst(['theme.header', 'partials.header'])

```

Included templates are rendered in an **isolated scope** — their sections do not leak into the parent layout's `#yield` slots.

---

Layout Inheritance
------------------

[](#layout-inheritance)

**`views/layouts/app.lex`**

```
>

    #yield('title', 'My App')
    #stack('styles')

    #yield('content')

    #stack('scripts')

```

**`views/pages/home.lex`**

```
#extends('layouts.app')

#section('title')
    Home Page
#endsection

#section('content')
    Welcome
    Hello, {{ $user->name }}!
#endsection

#push('scripts')

#endpush

```

### `#parent` — Extend a Section

[](#parent--extend-a-section)

```
#section('sidebar')
    #parent
    Extra sidebar content appended after the layout's sidebar.
#endsection

```

### `#stack` with a Default

[](#stack-with-a-default)

```
#stack('scripts', '')

```

---

Components
----------

[](#components)

### Self-closing

[](#self-closing)

```

```

### With Children / Slots

[](#with-children--slots)

```

    {{ $post->excerpt }}

        Read more

```

**`views/components/card.lex`**

```

    {{ $title }}
    {{ $slot }}
    #yield('footer')

```

### Component Discovery

[](#component-discovery)

Components are resolved automatically from a `components/` subdirectory inside each configured view path (e.g. `views/components/`). No extra configuration is needed.

Any tag whose name is **not** a standard HTML5/SVG/MathML element is treated as a component — both PascalCase and kebab-case names work:

```

```

Given any of the above, Lex looks for the component file (in order):

1. `user-profile.lex`
2. `UserProfile.lex`
3. `userprofile.lex`

To register a component explicitly by file path:

```
$lexer->component('Alert', __DIR__ . '/views/components/alert.lex');
```

### Component Classes

[](#component-classes)

```
namespace App\View\Components;

class Alert
{
    public string $class;

    public function mount(string $type, string $message): void
    {
        $this->class   = 'alert alert-' . $type;
        $this->message = $message;
    }
}
```

```
$lexer->componentClassNamespace('App\\View\\Components');
```

Props are automatically injected into `mount()` via reflection. All public properties of the class instance are available in the component template.

Auto-discovery looks for `{PascalCase}Component` — e.g. `` resolves to `App\View\Components\AlertComponent`.

To register a class explicitly:

```
$lexer->registerComponentClass('Alert', App\View\Components\AlertComponent::class);
```

### Prop Types

[](#prop-types)

SyntaxPHP typeExample`prop="value"``string` literal`title="Hello"``:prop="$expr"`PHP expression`:user="$currentUser"``prop` (bare)`true` (boolean)`closable`---

Raw PHP Blocks
--------------

[](#raw-php-blocks)

For standalone PHP projects that need inline PHP logic:

```
#php
    $total   = array_sum(array_column($items, 'price'));
    $tax     = $total * 0.1;
    $display = number_format($total + $tax, 2);
#endphp

Total: ${{ $display }}

```

> **Note:** `#php` blocks are disabled in sandbox mode.

---

Debug Helpers
-------------

[](#debug-helpers)

```
#dump($variable)
#dd($variable)

```

---

Chrome DevTools Extension
-------------------------

[](#chrome-devtools-extension)

In **development mode** (default), every `render()` call automatically injects a JSON debug payload into the HTML response. Install the [Lex DevTools](../lexer-extension/) Chrome extension to inspect it.

```
chrome://extensions → Load unpacked → select lexer-debug-extension/dist/

```

The DevTools panel provides:

TabWhat you see**Components**Full component tree, props, slots, render times**Sections**All `#section` / `#yield` pairs with content preview**Cache**Hit/miss per template, compiled file paths**Network**Lex render time per request via `X-Lex-*` headers**Timeline**Gantt chart of component render timesThe **Error Overlay** intercepts `TemplateSyntaxException` and similar errors and shows the file, line, column, and a source snippet with an "Open in VS Code" button.

In **production mode** the debugger is **never activated** — zero overhead:

```
$lexer->setProduction();   // disables LexDebugger automatically
```

See the [DevTools guide →](docs/07-devtools.md) for the full setup.

---

Custom Directives
-----------------

[](#custom-directives)

```
$lexer->directive('datetime', function (string $expression): string {
    return "";
});
```

**Template:**

```
#datetime($post->created_at)

```

Custom directives are resolved at **parse time** — the callable runs once during compilation, not on every render.

---

Configuration
-------------

[](#configuration)

### Config file (`lex.config.json`)

[](#config-file-lexconfigjson)

Place `lex.config.json` at the project root. All paths may be relative (resolved from the file's own directory) or absolute.

```
{
  "viewPaths":  ["views", "resources/views"],
  "production": false,
  "sandbox":    false
}
```

FieldTypeDefaultDescription`viewPaths``string[]``["views","resources/views"]`Directories scanned for `.lex` templates`production``bool``false`Enable production mode on startup`sandbox``bool``false`Enable secure sandbox modeThe compiled template cache is always placed at `{projectRoot}/.lexer/`:

PathContents`.lexer/compiled/`Compiled PHP files (`{md5}.php`)`.lexer/ast/`Serialised AST snapshots (`{md5}.ast`)`.lexer/compiled/index.php`Precompiled view index (production mode)`.lexer/view_dependencies.json`Dependency graph for cache invalidationThe [Lex LSP extension](../lexer-view-extension/) also reads the same file to power IntelliSense.

`LexConfig` walks up from the current working directory to find the file, so you can call `Lexer::fromConfig()` from anywhere inside the project without passing a path.

### Fluent API

[](#fluent-api)

```
use Wik\Lexer\Lexer;
use Wik\Lexer\Security\SandboxConfig;

$lexer = (new Lexer())
    // View directories (dot-notation resolution)
    ->paths([__DIR__ . '/views'])

    // Add a single directory without replacing existing ones
    ->addPath(__DIR__ . '/views/vendor')

    // Enable production mode (precompiled index, skip source checks)
    ->setProduction()

    // Custom HTML escaper
    ->setEscaper(new MyCustomEscaper())

    // Sandbox mode
    ->enableSandbox()
    ->setSandboxConfig(
        SandboxConfig::secure()
            ->withAllowedFunctions(['strtolower', 'strtoupper', 'number_format'])
    )

    // Component class namespace
    ->componentClassNamespace('App\\View\\Components')

    // Register a component explicitly by file path
    ->component('Alert', __DIR__ . '/views/components/alert.lex')

    // Custom directives
    ->directive('money', fn($e) => "")
    ->directive('uppercase', fn($e) => "");
```

---

Sandbox Mode
------------

[](#sandbox-mode)

Sandbox mode restricts what template authors can do — useful for user-submitted templates.

```
use Wik\Lexer\Security\SandboxConfig;

// Permissive (only removes always-blocked functions like exec, eval, system…)
$config = SandboxConfig::permissive();

// Secure (raw echo forbidden, custom directives forbidden, strict function whitelist)
$config = SandboxConfig::secure()
    ->withAllowedFunctions(['date', 'number_format', 'strtolower'])
    ->withRawEcho(false);

$lexer->enableSandbox()->setSandboxConfig($config);
```

**Always-blocked** regardless of config: `eval`, `exec`, `system`, `shell_exec`, `passthru`, `popen`, `proc_open`, `file_get_contents`, `file_put_contents`, `include`, `require`, `curl_exec`, backtick operator, and 40+ others.

---

Loaders
-------

[](#loaders)

### File Loader (default)

[](#file-loader-default)

```
$lexer->paths([
    __DIR__ . '/views',
    __DIR__ . '/views/vendor',
]);
```

Template `'layouts.app'` resolves to `views/layouts/app.lex`.

### Namespace Loader

[](#namespace-loader)

```
use Wik\Lexer\Loader\NamespaceLoader;

$loader = new NamespaceLoader();
$loader->addNamespace('admin', __DIR__ . '/views/admin');
$loader->addNamespace('mail',  __DIR__ . '/views/mail');
```

Template `'admin::dashboard'` resolves to `views/admin/dashboard.lex`.

### Memory Loader (testing)

[](#memory-loader-testing)

```
use Wik\Lexer\Loader\MemoryLoader;

$loader = new MemoryLoader();
$loader->set('greeting', 'Hello, {{ $name }}!');
```

---

Render a File Directly
----------------------

[](#render-a-file-directly)

```
echo $lexer->renderFile('/absolute/path/to/template.lex', ['key' => 'value']);
```

---

Escaping
--------

[](#escaping)

The default escaper uses `htmlspecialchars(ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')`.

Implement a custom escaper:

```
use Wik\Lexer\Contracts\EscaperInterface;

class MarkdownEscaper implements EscaperInterface
{
    public function escape(mixed $value): string
    {
        return htmlspecialchars((string) $value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
    }
}

$lexer->setEscaper(new MarkdownEscaper());
```

---

Production Mode
---------------

[](#production-mode)

**Dev mode (default — production off):** on every `render()` call Lex checks whether the source `.lex` file has changed and recompiles it automatically. No manual cache management is needed during development. `LexDebugger` is also active in dev mode — it injects the `__lex_debug__` payload that powers the Chrome DevTools extension.

**Production mode:** all source-file I/O is skipped. Templates are served directly from a precompiled index — zero recompilation per request. Templates must be compiled before deployment to keep compiled files up to date. `LexDebugger` is disabled automatically — no debug data is ever injected.

```
// In your application bootstrap
$lexer->setProduction();          // enable — also disables LexDebugger
// $lexer->setProduction(false);  // revert to dev mode if needed
```

---

Dependency Graph Cache
----------------------

[](#dependency-graph-cache)

Lex maintains a dependency graph so that when a shared template changes, every template that imports it is automatically recompiled on the next request — without you touching any code.

### How it works

[](#how-it-works)

When a template is compiled for the first time, Lex walks its AST and records every static dependency it finds:

SourceTracked as`#extends('layouts.app')`layout dependency`#include('partials.header')`include dependency`#includeIf` / `#includeWhen` / `#includeFirst`include dependencies (static string args only)``, ``component tag dependencyThe graph is persisted to `.lexer/view_dependencies.json`:

```
{
  "/abs/views/pages/home.lex": {
    "/abs/views/layouts/app.lex": 1712000000,
    "/abs/views/partials/header.lex": 1712000001
  },
  "/abs/views/pages/about.lex": {
    "/abs/views/layouts/app.lex": 1712000000
  }
}
```

Keys are absolute template paths; nested keys are absolute dependency paths; values are the Unix timestamps (`filemtime`) recorded at compile time.

### Invalidation flow

[](#invalidation-flow)

```
header.lex modified  (mtime changes)
         │
         ▼
Next render of home.lex
  → isStale('home.lex') checks header.lex mtime  → changed!
  → compiled cache for home.lex is cleared
  → full recompile runs
  → dependency graph is updated with new mtime

```

Dynamic include expressions (e.g. `#include($varName)`) cannot be statically resolved and are silently skipped — only string literals are tracked.

### Cache clear

[](#cache-clear)

To clear the cache, delete the `.lexer/` directory or call `FileCache::flush()` — this removes all compiled files, AST snapshots, and `view_dependencies.json`, forcing a clean slate on the next compile run.

### Querying the graph (tooling / advanced use)

[](#querying-the-graph-tooling--advanced-use)

```
use Wik\Lexer\Cache\DependencyGraph;

$graph = new DependencyGraph('/path/to/project/.lexer');

// Which templates does home.lex depend on?
$graph->getDeps('/abs/views/pages/home.lex');
// → ['/abs/views/layouts/app.lex' => 1712000000, ...]

// Which templates depend on header.lex?
$graph->getDependents('/abs/views/partials/header.lex');
// → ['/abs/views/pages/home.lex', '/abs/views/pages/about.lex']

// Is home.lex stale (any dep mtime changed)?
$graph->isStale('/abs/views/pages/home.lex');  // bool

// Full forward map
$graph->all();
```

---

Exceptions
----------

[](#exceptions)

ExceptionWhen`TemplateSyntaxException`Parse/compile error with file, line, column, and source snippet`TemplateRuntimeException`Infinite layout loop, component recursion limit exceeded`ViewException`Template not found, no view paths configured`LexerException`Unterminated `{{ }}` or `{!! !!}``ParseException`Unmatched blocks, unknown directives`CompilerException`Cache directory not writable---

Architecture
------------

[](#architecture)

```
Template source
    │
    ▼
Lexer              character-by-character tokenizer (no regex structural parsing)
    │  Token[]
    ▼
Parser             explicit stack → nested AST
    │  Node[]
    ▼
DependencyGraph    walk AST → record #extends / #include / component deps + mtimes
    │  Node[]
    ▼
AstValidator       sandbox enforcement, structural checks (optional)
    │  Node[]
    ▼
OptimizePass       merge adjacent TextNodes, remove empty nodes (optional)
    │  Node[]
    ▼
Code generation    Node::compile() → PHP source string
    │  string
    ▼
FileCache          atomic write → .lexer/compiled/{md5(key)}.php
    │  path
    ▼
include()          executed in isolated scope with $__env injected

```

**On subsequent requests:** before hitting the cache, `DependencyGraph::isStale()` checks whether any recorded dependency has a changed `filemtime`. If stale, the compiled cache is cleared before the pipeline runs — ensuring the template is always up to date.

---

Testing
-------

[](#testing)

```
composer install
vendor/bin/phpunit
```

---

License
-------

[](#license)

MIT — see [LICENSE](LICENSE).

###  Health Score

39

—

LowBetter than 86% of packages

Maintenance96

Actively maintained with recent releases

Popularity8

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity39

Early-stage or recently created project

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~0 days

Total

12

Last Release

59d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/927877cd465dba79544fe63d02aa12180ed9d778ff4b4c340121ed1c0a32e368?d=identicon)[tuinhanne](/maintainers/tuinhanne)

---

Top Contributors

[![tuinhanne](https://avatars.githubusercontent.com/u/66679347?v=4)](https://github.com/tuinhanne "tuinhanne (21 commits)")

---

Tags

componentstemplateviewastengineLexer View

### Embed Badge

![Health badge](/badges/wik-lexer/health.svg)

```
[![Health](https://phpackages.com/badges/wik-lexer/health.svg)](https://phpackages.com/packages/wik-lexer)
```

###  Alternatives

[eftec/bladeone

The standalone version Blade Template Engine from Laravel in a single php file

8208.4M87](/packages/eftec-bladeone)[anourvalar/office

Generate documents from existing Excel &amp; Word templates | Export tables to Excel (Grids)

24085.2k](/packages/anourvalar-office)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
