PHPackages                             abivia/penknife - 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. abivia/penknife

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

abivia/penknife
===============

A small template engine.

1.3.0(7mo ago)140MITPHPPHP ^8.4

Since Mar 19Pushed 7mo ago1 watchersCompare

[ Source](https://github.com/abivia/penknife)[ Packagist](https://packagist.org/packages/abivia/penknife)[ RSS](/packages/abivia-penknife/feed)WikiDiscussions main Synced 1mo ago

READMEChangelogDependencies (1)Versions (9)Used By (0)

Abivia\\Penknife
================

[](#abiviapenknife)

Penknife: a small blade
-----------------------

[](#penknife-a-small-blade)

Penknife is a tiny but powerful template engine implemented in PHP 8.4. Change log below.

Penknife supports:

- Variables
- Conditionals
- Nested loops
- Custom tokens
- Template includes
- Layouts/slots (pushing data and variables into a parent template)
- Custom directives

The `format()` method takes template text and a variable resolution callback as arguments.

Example:

```
$engine = new \Abivia\Penknife\Penknife();
echo $engine->format(
   '{{first}} {{last}}', function (string $expression, $type) {
       return match ($expression) {
           'first' => 'Jane',
           'last' => 'Doe',
       };
   });
);
```

Output:

```
Jane Doe

```

Variables can have a default value: `{{somevar, none}}` will emit "none" if the resolution function returns a null value for `somevar`.

The `type` argument to the callback is either `PenKnife::RESOLVE_DIRECTIVE`or `PenKnife::RESOLVE_EXPRESSION`. Directives allow extension of the command structure and are described below.

Conditionals
------------

[](#conditionals)

Penknife supports if-then-else statements.

```
{{?variable}}
true part
{{!?variable}}
Optional false part
{{/?variable}}

```

If the resolution callback returns a value that evaluates as `empty()` then the condition is false, otherwise it is true.

Loops
-----

[](#loops)

A loop is indicated with an ampersand followed by the name of an array. Elements of the array can be accessed by the loop variable `loop`. Nested loops are accessed by their nesting level, `loop2`, `loop3`, etc. for ease of use, `loop` is the same as `loop1`. The current index of the array can be obtained by using a # with an optional bias, which is useful for numbering from 1. If the array is associative, this will return the current key. An associative array with a numerical bias will return the numerical position.

```
$template = "looping:\n{{@list}}index {{loop.#}} line {{loop1.#.1}} value {{loop}}\n{{/@list}}";
$engine = new \Abivia\Penknife\Penknife();
echo $engine->format($template, function ($expr) {
    return $expr === 'list' ? ['first' => 'one', 'second' => 'two'] : null;
});
```

Output:

```
looping:
index first line 1 value one
index second line 2 value two

```

### Nested Loops

[](#nested-loops)

Loops can be nested:

```
$testObj = new Penknife();
$template = "looping:"
    . "\n{{@list}}index {{loop.#}} line {{loop1.#.1}} value {{loop.name}}"
    . "\n{{@loop.data}}{{loop2}} {{/@loop.data}}"
    . "\n{{/@list}}";
$engine = new \Abivia\Penknife\Penknife();
echo $engine->format($template, function ($expr) {
    if ($expr === 'list') {
        return [
            [
                'name' => 'slice one',
                'data' => [1, 2, 3, 4],
            ],
            [
                'name' => 'slice two',
                'data' => [4, 5, 6],
            ],
        ];
    }
    return null;
});
```

Output:

```
looping:
index 0 line 1 value slice one
1 2 3 4
index 1 line 2 value slice two
4 5 6

```

### Named loops

[](#named-loops)

Since keeping track of loop variables like loop1, loop2, etc. can be tedious, it is possible to give a loop a name:

```
{{@loop.data,dataLoop}}{{dataLoop.element}}{{/@loop.data}}

```

### Empty loops

[](#empty-loops)

You can use !@ before the end of a loop to handle empty loops

```
{{@list}}
    {{loop.lastName}}, {{loop.firstName}}"
{{!@list}}
    Empty!
{{/@list}}

```

System directives
-----------------

[](#system-directives)

System directives have the form {{:name \[arguments\]}}.

NameUseDescriptionexport{{:export export\_list}}Exports one or more variables to a parent template.include{{:include file\_path}}Includes the named template file, if it exists.inject{{:inject section\_name}}Specifies the start of a parent injection section.parent{{:parent file\_path}}Names a parent template file.All other directives are passed to the resolver callback with the type set to RESOLVE\_DIRECTIVE. New directives may be added in the future. Penknife will never use an internal directive name starting with an underscore.

### Export directive

[](#export-directive)

The value of variables in the current environment can be exported to the parent by listing them, separated by spaces. It is possible to rename a variable by prefixing it with the exported name and a colon. For example {{:export banner:title dated}} will put the value of "title" into the "banner" variable, the value of "dated" into the "dated" variable, and the text following the inject directive into the "header" variable. These will then be passed to the parent template.

### Include directive

[](#include-directive)

This will read the included file into the template. Use the includePath() method to set a base directory for template includes.

### Inject directive

[](#inject-directive)

Defines the beginning of text that will be stored into the named section and passed to the parent template. The output will be passed as the named variable to the parent template. For example, {{:inject header}} will pass the variable "header" into the parent template. If no parent is specified then the contents of the inject block are not output anywhere.

### Parent directive

[](#parent-directive)

If a parent template is specified, then the values of any slots defined by an inject directive, along with any exported variables, will be passed to that template. Only one parent can be specified. Parent directives after the first one are ignored.

Example of the inject/parent mechanism:

Template

```
{{:parent some-file}}
{{:inject one}}
This is the first part.
{{:inject two}}
This is the second part.

```

Parent template stored in some-file

```
{{two}}
and then there was
{{one}}

```

Resulting output:

```
This is the second part.
and then there was
This is the first part.

```

Alternate Syntax
----------------

[](#alternate-syntax)

All tokens in Penknife can be modified via the `setToken()` and `setTokens()` methods. The default tokens are:

NameValueUsageargs,Separate arguments inside a commandclose}}Closes a commandelse!Else operatorend/End operator, terminates an if or loopif?Conditional operatorindex\#Current loop indexloop@Starts a loopopen{{Opens a commandscope.Scope operatorsystem:System directive```
$engine = new \Abivia\Penknife\Penknife();
// Note the spaces in the if and else tokens
$engine->setTokens([
    'open' => '**>',
    'if' => 'if ',
    'else' => 'else ',
     'end' => '~',
]);

// This is now a valid template:
$template = "conditional:TRUEFALSE."
```

Changelog
=========

[](#changelog)

### 1.3.0 2025-10-08

[](#130-2025-10-08)

- Added the export, inject and parent directives.

### 1.2.2 2025-09-26

[](#122-2025-09-26)

- Bugfix. Include overwrote the rest of the template. Fixed test.

### 1.2.1 2025-09-25

[](#121-2025-09-25)

- Added the includePath() method to set a base path for the include directive.

1.2.0 2025-09-25
----------------

[](#120-2025-09-25)

- Added the directive mechanism and the include directive.

### 1.1.2 2025-09-23

[](#112-2025-09-23)

- Pull version out of composer, let git manage versions through tags.

### 1.1.1 2025-09-23

[](#111-2025-09-23)

- Handle looping on an array of objects as well as an array of arrays.

1.1.0 2025-09-22
----------------

[](#110-2025-09-22)

- Added handling for empty loops.
- Overhauled template parsing to fix an obscure bug.
- Handle case where nested conditionals test the same expression. This was breaking the old parser.
- Fixed a documentation error.

1.0.0 Initial release
---------------------

[](#100-initial-release)

###  Health Score

39

—

LowBetter than 86% of packages

Maintenance65

Regular maintenance activity

Popularity12

Limited adoption so far

Community7

Small or concentrated contributor base

Maturity60

Established project with proven stability

 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 ~29 days

Recently: every ~4 days

Total

8

Last Release

214d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/42cd72dce530e94e60853407cdab69ead232423272b482218ae686c740ce0e99?d=identicon)[abivia](/maintainers/abivia)

---

Top Contributors

[![instancezero](https://avatars.githubusercontent.com/u/2599327?v=4)](https://github.com/instancezero "instancezero (13 commits)")

---

Tags

bladetemplateabivia

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/abivia-penknife/health.svg)

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

###  Alternatives

[eftec/bladeone

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

8208.4M87](/packages/eftec-bladeone)[jenssegers/blade

The standalone version of Laravel's Blade templating engine for use outside of Laravel.

8661.2M109](/packages/jenssegers-blade)[sineld/bladeset

A very simple blade extension which allows variables to be set within blade templates.

4423.2k](/packages/sineld-bladeset)[fiskhandlarn/blade

A library for using Laravel Blade templates in WordPress/WordPlate.

365.8k](/packages/fiskhandlarn-blade)[leitsch/kirby-blade

Enable Laravel Blade Template Engine for Kirby 4 and Kirby 5

219.2k](/packages/leitsch-kirby-blade)[eftec/bladeonehtml

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

1018.1k5](/packages/eftec-bladeonehtml)

PHPackages © 2026

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