Skip to content
laravel-tokens
Loading repository…

Tokens

This Laravel package replaces tokens in user-entered text with data from your Eloquent models. It is useful for anything built from a template, such as email templates, notifications, PDF documents, or CMS-managed content, where an editor writes the copy and your application fills in the details.

Tokens are written in the form ##attribute##, and related model data can be reached with dot notation, e.g. ##category.name##.

Requirements

  • PHP: ^8.3
  • Laravel: ^11.0, ^12.0 or ^13.0

Installation

Require the package via Composer.

bash
composer require eighteen73/laravel-tokens

There is no config file to publish and no setup required.

Resolving the Token Manager

All of the work is done by TokenManager. You can construct it directly, resolve it from the container, or use the Tokens facade; they are equivalent.

php
use Eighteen73\LaravelTokens\Facades\Tokens;
use Eighteen73\LaravelTokens\TokenManager;

$manager = new TokenManager();
$manager = app(TokenManager::class);

Tokens::forModel($user)->replaceTokens('Hello ##name##.');

WARNING

TokenManager is stateful: forModel(), maxDepth(), and withoutRelationships() all mutate the instance. Laravel facades cache the instance they resolve, so settings applied through the Tokens facade persist for the rest of the request.

If you've called maxDepth() or withoutRelationships(), use new TokenManager() or app(TokenManager::class) for subsequent work so you start from a clean instance. Note that forModel() swaps the model but leaves those two settings as they were.

The examples below use app(TokenManager::class).

Which Attributes Become Tokens

The TokenManager works out the available tokens from the model itself, so there's nothing to register.

  • For a saved model instance, its loaded attributes are used.
  • For a model class name (or an unsaved instance), the model's factory definition is used if one exists, otherwise the model's $fillable attributes are used.
  • Any $appends accessors are included.
  • Anything listed in the model's $hidden array is excluded, so passwords and tokens won't leak into a token list.
  • Relations are included to a depth of two levels (see Relation Tokens).

Listing Available Tokens

Use plainTokens() to get an array of every token a model supports. This is handy for showing editors a reference list of what they can use in a template.

php
use Eighteen73\LaravelTokens\TokenManager;

$tokens = app(TokenManager::class)
    ->forModel(App\Models\User::class)
    ->plainTokens();

// ['##name##', '##email##', '##category.name##', '##posts.0.title##', ...]

forModel() accepts either a class name or a model instance.

Replacing Tokens

Pass your text to replaceTokens() and any recognised tokens will be swapped for the model's values.

php
use App\Models\User;
use Eighteen73\LaravelTokens\TokenManager;

$user = User::find(1);

echo app(TokenManager::class)
    ->forModel($user)
    ->replaceTokens('Hello ##name##, your email address is ##email##.');

// Hello Ada, your email address is ada@example.com.

Tokens that aren't recognised are left in the text untouched, as are tokens whose relation no longer exists. This means a typo in an editor's template won't break the output; it just shows up as an unreplaced token.

TIP

Any relations needed by the tokens in your text are loaded automatically via loadMissing(), so you don't need to eager load them yourself.

Relation Tokens

Related model data is accessed with dot notation. BelongsTo and HasOne relations are read directly.

php
$user->setRelation('category', new Category(['name' => 'Administrators']));

echo app(TokenManager::class)
    ->forModel($user)
    ->replaceTokens('Category: ##category.name##');

// Category: Administrators

HasMany and BelongsToMany relations include a zero-based index to select which record you want.

php
echo app(TokenManager::class)
    ->forModel($user)
    ->replaceTokens('Your latest post is "##posts.0.title##".');

// Your latest post is "Hello World".

Controlling Relation Depth

Relations are traversed two levels deep by default. You can change that, or turn relation tokens off entirely.

php
// Only follow relations one level deep
app(TokenManager::class)
    ->forModel(User::class)
    ->maxDepth(1)
    ->plainTokens();

// Ignore relations completely
app(TokenManager::class)
    ->forModel(User::class)
    ->withoutRelationships()
    ->plainTokens();

Reducing the depth is worth doing on models with a lot of relations, as each extra level multiplies the number of tokens generated.

Custom Tokens

A model can provide tokens that don't map to an attribute (such as a formatted date, a signed URL, or a calculated total) by implementing the CustomTokens contract.

The interface has two methods: getCustomTokens() returns the token names, and replaceCustomToken() returns the value for a given name.

php
use Eighteen73\LaravelTokens\Contracts\CustomTokens;
use Illuminate\Database\Eloquent\Model;

class User extends Model implements CustomTokens
{
    public function getCustomTokens(): array
    {
        return [
            'full_name',
            'reset_url',
        ];
    }

    public function replaceCustomToken(string $token): string
    {
        return match ($token) {
            'full_name' => "{$this->first_name} {$this->last_name}",
            'reset_url' => route('password.reset', $this),
        };
    }
}

These are then used just like any other token, and appear in plainTokens() alongside the model's attributes.

php
echo app(TokenManager::class)
    ->forModel($user)
    ->replaceTokens('Hi ##full_name##, reset your password at ##reset_url##.');

Custom tokens work on related models too, so a ##category.custom_token## will resolve if Category implements the contract.