PHPackages                             rlnks/php-mail-tree - 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. rlnks/php-mail-tree

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

rlnks/php-mail-tree
===================

A PHP HTML email builder using an intuitive object-tree nesting approach. Structure your email exactly like you think it — each node is a named property on its parent.

v1.2.2(3w ago)06↓100%1MITPHPPHP &gt;=8.2

Since May 13Pushed 3w agoCompare

[ Source](https://github.com/rlnks/php-mail-tree)[ Packagist](https://packagist.org/packages/rlnks/php-mail-tree)[ Docs](https://github.com/rlnks/php-mail-tree)[ RSS](/packages/rlnks-php-mail-tree/feed)WikiDiscussions main Synced 1w ago

READMEChangelogDependenciesVersions (6)Used By (1)

rlnks/php-mail-tree
===================

[](#rlnksphp-mail-tree)

A PHP library for building HTML emails using an intuitive **object-tree nesting** approach. Structure your email exactly the way you think it — each node is a named property on its parent, and the hierarchy of your PHP code mirrors the hierarchy of the rendered HTML.

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

[](#installation)

```
composer require rlnks/php-mail-tree
```

Requires PHP 8.2+.

---

The concept
-----------

[](#the-concept)

Most email builders make you concatenate strings or call sequential methods. This library lets you build an **object tree** instead. Assign child nodes as named properties — the name you choose becomes documentation.

```
$email->body = new Body();
$email->body->header = new Container();
$email->body->header->logo_col = new Column();
$email->body->header->logo_col->logo = new Image($src, $alt);
```

Each node knows its place in the tree. Call `$email->build()` once and the entire tree renders itself recursively, cascading styles top-down.

---

Structure-first pattern
-----------------------

[](#structure-first-pattern)

Because nodes are just PHP object properties, you can **declare the email skeleton first, then fill in the content separately**. This separates layout decisions from copy decisions — exactly like drag-and-drop in a visual builder, but in code.

```
// ── 1. Skeleton — declare every section up front ──────────────────────────
$email = new EmailDocument($sheet);   // sets theme + registers $sheet as global default
$email->body = new Body();
$email->body->setCSS($sheet->responsiveCss());

$email->body->banner   = FullWidthImage::make();  // src/href filled in step 2
$email->body->gap1     = Spacer::make();
$email->body->intro    = Section::make();
$email->body->gap2     = Spacer::make();
$email->body->features = TwoColumn::make();
$email->body->divider  = Divider::make();
$email->body->footer   = Section::make();

// ── 2. Content — fill each section independently ──────────────────────────
$email->body->banner->col->link->setLink($heroUrl);
$email->body->banner->col->link->img->setSrc($heroSrc, 'Hero');

$email->body->intro->body->title = new Text('Order confirmed!', 'h1');
$email->body->intro->body->desc  = new Text('Thanks for your purchase.', 'div');
$email->body->intro->body->cta   = Button::make('View order', $orderUrl);

$email->body->features->left->img    = new Image($img1, 'Feature A');
$email->body->features->right->title = new Text('Feature **A**', 'h2');   // ** highlights in primaryColor
$email->body->features->right->desc  = new Text('Best feature ever.', 'div');

$email->body->footer->body->copy = new Text('© 2025 Acme Corp', 'div');

// ── 3. Render ──────────────────────────────────────────────────────────────
echo $email->build();
```

**Why this matters:**

- You can comment out an entire section (`// $email->body->features = …`) without touching content code.
- Content writers and layout developers can work in different blocks of the same file.
- The skeleton reads like a wireframe — `banner → gap → intro → gap → features → divider → footer` — matching the mental model of a designer.
- Nodes assigned later (step 2) are still accessible via `->` property access because `HasChildren` stores them in an array under the hood. Order of assignment doesn't matter; render order is insertion order.

---

Node management
---------------

[](#node-management)

Every node supports a full set of tree-management methods. All methods return `$this` (or the affected node) so they can be chained.

### Visibility — `hide()` / `show()`

[](#visibility--hide--show)

A hidden node is completely skipped during rendering. It stays in the tree, so it can be revealed again or toggled conditionally.

```
$email->body->promo = Section::make(sheet: $sheet);

if (!$user->hasPromoAccess()) {
    $email->body->promo->hide();
}

// Chainable at assignment time:
$email->body->notice = (new Text('Beta feature', 'div'))->hide();
```

### Reordering — `moveUp()` / `moveDown()` / `moveToIndex()`

[](#reordering--moveup--movedown--movetoindex)

Nodes track their parent automatically when assigned via `->`. Move methods operate on the parent's child list immediately.

```
$email->body->intro    = Section::make(sheet: $sheet);
$email->body->features = TwoColumn::make(sheet: $sheet);
$email->body->cta      = Section::make(sheet: $sheet);

$email->body->features->moveUp();        // swap features above intro
$email->body->cta->moveToIndex(0);       // jump to first position
$email->body->cta->moveDown(2);          // multi-step shift
```

`moveUp`/`moveDown` accept an optional `$steps` argument (default `1`). Positions clamp at boundaries — no wrap-around.

### Relative positioning — `insertBefore()` / `insertAfter()`

[](#relative-positioning--insertbefore--insertafter)

Position a node relative to a named sibling instead of an absolute index.

```
// Skeleton built in order A → B → C
$email->body->intro    = Section::make(sheet: $sheet);
$email->body->features = TwoColumn::make(sheet: $sheet);
$email->body->footer   = Section::make(sheet: $sheet);

// Inject a new divider between features and footer:
$email->body->divider = Divider::make(sheet: $sheet);
$email->body->divider->insertBefore('footer');

// Or move features after the footer:
$email->body->features->insertAfter('footer');
```

The sibling is identified by the **property name** it was assigned under.

### Lifecycle — `detach()` / `replaceWith()` / `duplicate()`

[](#lifecycle--detach--replacewith--duplicate)

```
// Remove a section from the tree entirely (returns the detached node):
$removed = $email->body->promo->detach();

// Swap a node with a new one, keeping the same key and position:
$email->body->intro->body->title->replaceWith(new Text('New Title', 'h1'));

// Deep-clone a node and insert it immediately after itself:
$copy = $email->body->card->duplicate();
$copy->body->title->replaceWith(new Text('Card 2', 'h2'));
// Duplicate key is auto-generated: "card_2", "card_3", …
```

### Inspection — `getChildren()`

[](#inspection--getchildren)

Returns a snapshot of all direct children, keyed by their property name.

```
foreach ($email->body->getChildren() as $key => $node) {
    if (!$node->isHidden()) {
        echo "$key is visible\n";
    }
}
```

> **Note:** `Image` supports `hide()`/`show()` but not the tree-manipulation methods (`move*`, `insertBefore/After`, `detach`, `replaceWith`, `duplicate`, `getChildren`), since it cannot have children.

---

Quick start with StyleSheet + Presets
-------------------------------------

[](#quick-start-with-stylesheet--presets)

```
use Rlnks\MailTree\Body;
use Rlnks\MailTree\EmailDocument;
use Rlnks\MailTree\Image;
use Rlnks\MailTree\StyleSheet;
use Rlnks\MailTree\Text;
use Rlnks\MailTree\Preset\Button;
use Rlnks\MailTree\Preset\FullWidthImage;
use Rlnks\MailTree\Preset\Section;
use Rlnks\MailTree\Preset\Spacer;
use Rlnks\MailTree\Preset\TwoColumn;

// 1. Define your theme once
$sheet = new StyleSheet([
    'primaryColor'   => '#e63946',
    'fontFamily'     => "Poppins, Arial, sans-serif",
    'containerWidth' => 600,
    'marginWidth'    => 30,
]);

// 2. Bootstrap the email — passing $sheet directly sets the theme AND registers
//    it as the global default so every preset resolves it automatically.
$email = new EmailDocument($sheet);
$email->setSubject('Your order is confirmed');
$email->addLink('https://fonts.googleapis.com/css2?family=Poppins:wght@400;700', 'stylesheet');

$email->body = new Body();
$email->body->setCSS($sheet->responsiveCss());

// 3. Build the tree with presets — no sheet: $sheet needed anywhere
$email->body->banner    = FullWidthImage::make($heroSrc, 'Hero', href: $heroUrl);
$email->body->gap1      = Spacer::make();

$email->body->intro     = Section::make();
$email->body->intro->body->title = new Text('Order confirmed!', 'h1');
$email->body->intro->body->desc  = new Text('Thanks for your purchase.', 'div');
$email->body->intro->body->cta   = Button::make('View order', $orderUrl);

// Override specific CSS on a preset without touching the theme:
$email->body->gap2      = Spacer::make(bg: '#f0f0f0');  // explicit bg, height from sheet

$email->body->features  = TwoColumn::make();
$email->body->features->left->img   = new Image($img1, 'Feature A');
$email->body->features->right->title = new Text('Feature A', 'h2');

echo $email->build();
```

---

Complete example
----------------

[](#complete-example)

A production-ready multi-file structure that demonstrates every major pattern at once: separated styles and translations, skeleton-first construction, images and links defined as named variables, a single `build()` call, and multiple output variants from that one render.

```
my-email/
├── email.php           ← main template (entry point)
├── styles.php          ← $sheet->define() — all named section styles
└── translations.php    ← return [...] — all translatable strings

```

---

### `styles.php` — named section styles

[](#stylesphp--named-section-styles)

```
