A package to manage and generate string with tokens/shortcodes for Eloquent Models
fomvasss/laravel-str-tokens is a Laravel package for a package to manage and generate string with tokens/shortcodes for eloquent models.
It currently has 4 GitHub stars and 9.143 downloads on Packagist (latest version 2.8.1).
Install it with composer require fomvasss/laravel-str-tokens.
Discover more Laravel packages by fomvasss
or browse all Laravel packages to compare alternatives.
Last updated
Replace [type:field]-style tokens/shortcodes in a string with real values pulled from Eloquent models, config, dates, or arbitrary variables — the same idea as Drupal's token system, built for Laravel.
Any app that builds text from a template ends up writing the same glue code: notification/email bodies with
placeholders, admin-configurable message templates, CMS-style content blocks, SMS text with client data,
generated documents. This package turns that into one small, declarative API: write [order:title] in a
string, hand it an Order model, get the real title back — including nested relations, custom formatting
rules, and values that have nothing to do with a model at all (dates, config, arbitrary variables).
[type:field] token resolution against any Eloquent model — no base class, no interface required[order:manager:email] walks the manager() relation and resolves email on the result, to any depthstrToken*() methods — full control, optional extra arguments ([chat:message:100])[user:name:uppercase] post-processes the resolved value; ships with uppercase, lowercase, trim, clearHtml, urlLink, plus your ownsetEntities(['user' => $user, 'order' => $order])[var:...], [date:...], [config:...]strTokenWhitelist() / strTokenBlacklist(), or a global disable_model_tokens / disable_configs config list — keep secrets like password or api_token out of reach even if a template author triestoken_split_character, token_match_pattern) if [type:field] doesn't fit your projectStrToken) and a container-resolvable class (StrTokenGenerator)illuminate/support) ^9 – ^13composer require fomvasss/laravel-str-tokens
The service provider and StrToken facade are auto-discovered. To customize the config, publish it:
php artisan vendor:publish --provider="Fomvasss\LaravelStrTokens\ServiceProvider"
This creates config/str-tokens.php.
use Fomvasss\LaravelStrTokens\Facades\StrToken;
$text = <<<TXT
Order: [order:title] (#[order:id])
Status: [order:status]
Manager: [order:manager:fullname] <[order:manager:email]>
Channel: [order:channel:name]
Support line: [var:supportPhone]
Today: [date:date]
TXT;
$result = StrToken::setText($text)
->setEntity($order)
->setVar('supportPhone', '+380 44 000 0000')
->replace();
Order: Order #1042 (#1)
Status: Status: active
Manager: Taylor Otwell <[email protected]>
Channel: Telegram
Support line: +380 44 000 0000
Today: 17.08.2026
A token is [type:name]. type identifies what to resolve against (a model, or one of the built-in
namespaces below); name is everything after the first : — it may contain further colons, which is how
nested relations and formatters are expressed (manager:email, name:uppercase).
setEntity()StrToken::setText('[order:title], placed by [order:manager:fullname]')
->setEntity($order)
->replace();
The token type must match Str::snake(class_basename($entity)) — an App\Models\Order instance is
addressed as [order:...], an App\Models\BlogPost as [blog_post:...]. If your token prefix can't (or
shouldn't) match the class name, use setEntities() instead — its keys are whatever you want them to be.
setEntities()StrToken::setText('Manager: [manager:fullname], Client: [client:fullname], Order: [order:title]')
->setEntities([
'manager' => $manager,
'client' => $client,
'order' => $order,
])
->replace();
// Manager: Taylor Otwell, Client: Vasyl Fomin, Order: Order #1042
The array key is the token prefix — it doesn't need to relate to the model's class name at all.
Don't combine
setEntity()andsetEntities(). CallingsetEntities()clears whateversetEntity()set before it; callingsetEntity()aftersetEntities()does not clearsetEntities(), andsetEntity()takes priority. Pick one perreplace()call.
Any real Eloquent relation method on the entity can be walked, to any depth — the relation method's name does not need to match the related model's class name:
// Order::manager() is a belongsTo(User::class) — name doesn't matter
'[order:manager:fullname]'
// walks two relations deep
'[order:manager:company:name]'
// a hasMany/BelongsToMany relation resolves against its FIRST related model
'[order:comments:body]'
A relation that resolves to null (e.g. an unassigned belongsTo) simply produces an empty string —
no error.
Set can_traverse_relations => false in the config to disable this globally; a nested token then resolves
to an empty string instead of walking the relation (see Configuration).
| Token | Resolves to |
|---|---|
| [var:name] | A value set via setVar('name', $value) / setVars([...]). Missing key → ''. |
| [date:format] | The date set via setDate($carbon) (defaults to now()), formatted using date.formats.{format} from the config. [date:raw] returns the Carbon instance itself. |
| [config:some.key] | config('some.key'), unless it matches a disable_configs pattern. |
StrToken::setText('[var:price] — offer valid until [date:long] — app: [config:app.name]')
->setDate(now()->addDays(3))
->setVar('price', '$49')
->replace();
strToken*() methodsDefine a method named Str::camel('str_token_' . $name) on the model to fully control how a token
resolves — e.g. [order:status] looks for strTokenStatus(). This takes priority over relation traversal
and plain field access.
class Order extends Model
{
// [order:status] -> "Status: active"
public function strTokenStatus(): string
{
return 'Status: ' . $this->status;
}
}
Extra arguments — every :-separated segment of the token, including the method's own name, is passed
through as a string argument after the model itself. This is how you build a parameterized token:
class Chat extends Model
{
// [chat:lastMessage] -> full text
// [chat:lastMessage:100] -> first 100 characters
public function strTokenLastMessage(self $chat, string $key, ?string $limit = null): string
{
$text = strip_tags((string) $chat->lastMessage?->content);
return $limit ? \Illuminate\Support\Str::limit($text, (int) $limit) : $text;
}
}
Arguments always arrive as strings (they come from exploding the raw token text) — type-hint them as
?string and cast inside the method, not as int/float, or a strict-typed parameter will throw.
Append :formatterName as the last segment of a token to post-process its resolved value:
'[user:name:uppercase]' // "JOHN"
'[user:email:lowercase]'
'[user:bio:clearHtml]' // strip_tags()
'[user:website:urlLink]' // wraps into <a href="...">...</a>, value is HTML-escaped
Built-in formatters (config('str-tokens.formatters')):
| Key | Effect |
|---|---|
| trim | PHP's trim() |
| uppercase | mb_strtoupper() |
| lowercase | mb_strtolower() |
| clearHtml | strip_tags() |
| urlLink | Wraps the (HTML-escaped) value in <a href='...'>...</a> |
Register your own — a formatter is any callable, an invokable class with a handle(?string $value): string
method, or a global function name:
// config/str-tokens.php
'formatters' => [
// ...defaults...
'money' => fn (?string $v) => number_format((float) $v, 2) . ' UAH',
],
'[order:total:money]'
Only one formatter per token — [user:name:uppercase:trim] does not chain both; the last segment
after the final : is looked up as-is. If you need to combine several transforms, write one custom
strToken*() method that does both (see the [chat:lastMessage:100] example above) rather than trying to
stack formatters.
Templates are often edited by someone other than a developer (an admin UI, a client-configurable notification). Three independent ways to keep sensitive fields out of reach:
class User extends Model
{
// Only these tokens resolve for this model — everything else is silently empty
public function strTokenWhitelist(): array
{
return ['name', 'email'];
}
// Or: block specific ones, allow everything else
public function strTokenBlacklist(): array
{
return ['password', 'remember_token', 'api_token'];
}
}
// config/str-tokens.php — applies to every model
'disable_model_tokens' => ['*password*', '*token*', '*secret*'],
// blocks [config:...] lookups the same way
'disable_configs' => ['app.key', 'auth.*', 'mail.*', 'services.*', 'password', '*token*'],
All four accept Str::is() glob patterns (* wildcards),
and are checked against the full token name — so manager:* blocks every field of that relation in
one line.
Everything below lives in config/str-tokens.php after publishing it; all keys are optional.
| Key | Default | Purpose |
|---|---|---|
| token_split_character | : | Separator inside a token name (type:a:b:c) |
| token_match_pattern | /\[([^\s\[\]:]*):([^\[\]]*)\]/x | The whole token regex — override to use a different bracket style entirely |
| can_traverse_relations | true | Set false to disable nested-relation resolution globally |
| disable_model_tokens | [] | Glob patterns blocked on every model |
| disable_configs | see file | Glob patterns blocked for [config:...] |
| date.formats | short, medium, long, time, date, my | Named formats for [date:name]; add your own key/format pair |
| formatters | trim, uppercase, lowercase, clearHtml, urlLink | See Formatters |
Admin-configurable notification templates. Store the template text (mail subject/body, SMS text) in the DB, let an admin edit it with a list of available tokens, resolve it against the real model when the notification actually sends:
$template = NotificationTemplate::where('key', 'OrderShipped')->first();
$body = StrToken::setText($template->body)
->setEntity($order)
->replace();
Blade.
@php($body = \StrToken::setEntity($article)->setDate($article->created_at)
->setText('[article:title] — published [date:short]')
->replace())
<h3>{!! $body !!}</h3>
Building an SMS/email body with a length cap — combine a custom strToken*() method with its own
parameter instead of relying on formatter chaining (see Custom tokens).
Multi-tenant/CMS content blocks where several unrelated records feed one string — use setEntities()
with descriptive keys instead of forcing everything onto one model's relations.
'' by default. Call ->doNotClearEmptyTokens() to leave genuinely unmatched
token types as literal text instead — this only applies to a token type that doesn't match any known
entity/var/date/config at all; [var:missingKey] still resolves to '' either way, since the var
namespace always produces a value (empty if the key is missing).Stringable object (e.g. a JSON-cast array column
read without a formatter) resolves to '' rather than the literal text Array or a fatal error.[date:raw] returns the actual Carbon instance, not a formatted string — useful when you want to pass
it on to more Carbon methods before printing it yourself.composer test
# or
vendor/bin/phpunit
tx prefix convention ([article:txCategories:name]); not
required otherwise.MIT — see LICENSE.
If this package is useful to you, consider supporting its development:
USDT TRC20 address:
THLgp6DxiAtbNHvgnKV56vk1L38UuUagKf