spatie/string
spatie/string is a PHP package for fluent, chainable string handling. Wrap any native string with the string() helper, then chain methods like between, toUpper, and toLower, or use array-style offsets to read and modify individual characters easily.
Install and confirm the PHP requirement. spatie/string 3.x requires PHP 8.0+ and has no Laravel service provider, config file, or published assets. That means composer require spatie/string is the entire setup:
composer require spatie/string
There is nothing to register in config/app.php, nothing to publish, and nothing to add to .env. If you're on PHP 7.x you'll be pinned to 2.x, which is a different (and effectively unmaintained) line — check composer show spatie/string before assuming the API in the README matches what you have.
Where to look first. The whole package is essentially one class, Spatie\String\String, plus the global string() helper and one exception class. Read the class source rather than the README — the docblocks in the README are slightly out of sync with the actual signatures (notably contains()), and the class is short enough to skim in a few minutes. Everything else — slugify(), isEmail(), replace(), titleize() — comes from the Anahkiasen/underscore-php integration, so treat that repository's StringsMethods class as your second reference.
First use case. The natural entry point in a Laravel app is display formatting in Blade, where you want a one-liner instead of a temp variable:
{{ string($post->excerpt)->tease(120) }}
{{ string($user->name)->possessive() }} profile
{{ string($path)->lastSegment('/') }}
The mental model is: string() wraps a native string in a value object, every transformation returns a new String, and you echo it (or cast to (string)) at the end of the chain. It's a formatting DSL, not a validation or sanitisation library — reach for it when you're shaping text for output, and keep Illuminate\Support\Str for the structural work.
Treat it as a view/presentation layer, not a domain layer. The package has no framework coupling and no configuration, so the safest adoption pattern is to confine it to Blade templates, mail templates, and API resource toArray() methods. That keeps the fluent style where it reads best and avoids leaking a third-party class into your models or services.
// app/Http/Resources/ArticleResource.php
public function toArray($request): array
{
return [
'title' => (string) string($this->title)->toUpper(),
'teaser' => (string) string(strip_tags($this->body))->tease(160),
'author_url' => (string) string($this->author_slug)->prefix('/authors/'),
];
}
Chain for readability, cast for type safety. Every method except the boolean checks returns \Spatie\String\String, so a chain terminates in either an echo/string interpolation context or an explicit (string) cast. Being explicit about the cast makes the boundary obvious to the next developer and prevents the object from accidentally being JSON-serialised as an object.
Use segment helpers instead of explode() juggling. This is where the package earns its keep over raw PHP. Paths, dotted config-style keys, and delimited identifiers become declarative:
string('foo/bar/baz')->segment('/', 1); // bar
string('foo/bar/baz')->segment('/', -1); // baz (negative index)
string('foo/bar/baz')->firstSegment('/'); // foo
string('foo/bar/baz')->lastSegment('/'); // baz
string('foo/bar/baz')->pop('/')->pop('/'); // foo
Note that segment() returns an empty String when the index is out of range rather than throwing — which is convenient for optional URL parts, but hides typos. If a segment is mandatory, assert on the result rather than trusting the chain.
Build composable formatting helpers rather than repeating chains. Because there's no config, the extension point you get for free is your own wrapper functions. Define the conventions once:
// app/Support/text.php
function teaser(?string $value, int $length = 140): string
{
return $value ? (string) string($value)->tease($length) : '';
}
This gives you null-safety (the package's constructor is strict and will throw on null, arrays, or non-__toString objects), a single place to change the ellipsis, and an easy path to swap the implementation later.
Use offset access for narrow, intentional mutations — not for parsing. ArrayAccess on the String object reads and writes single characters:
echo string('hello')[1]->toUpper(); // E
$colour = string('gray');
$colour[2] = 'e';
echo $colour->toUpper(); // GREY
This is fine for formatting tricks like masking or case-correction. It is not a substitute for mb_* functions — see the gotchas below.
Reach into the underscore integration sparingly. slugify() is the one genuinely useful inherited method for Laravel work, and it chains cleanly with the native ones:
(string) string($title)->slugify(); // i-am-a-slug
(string) string($title)->slugify()->between('i', 'a-slug');
Everything else from underscore is a judgement call: the dependency is old, and several of its methods return scalars rather than String, which silently breaks chaining. Prefer Str::slug() for slug generation in new code unless you specifically need the chained behaviour.
You probably already have this in Laravel. Since Laravel 7, Illuminate\Support\Str::of() returns a Stringable with upper(), lower(), substr(), before(), after(), when(), slug(), and more. spatie/string overlaps substantially with it. The honest assessment: adopt spatie/string only if your codebase already uses it, or if you specifically want tease(), possessive(), segment()/pop(), and the string() helper's short name. Running both fluent APIs side by side in one team is the main maintenance cost of this package.
The global string() helper can collide. Because the package registers a global function rather than a namespaced facade, any other dependency defining string() will cause a fatal redeclaration error or win the race depending on autoload order. If you hit a "cannot redeclare string()" or unexplained behaviour from a helper, check composer show for other packages shipping global helpers before debugging your own code.
Construction is strict — null will throw. As of 2.0.1, instantiating with an array, or an object that doesn't implement __toString, throws. null is not coercible either. In Laravel this bites on nullable model columns:
string($user->nickname); // throws when null
string($user->nickname ?? ''); // safe
Wrap it in your own helper (as shown above) rather than sprinkling ?? '' across templates.
contains() breaks the chain. It returns bool, not String, and the README's docblock lists $needle, $caseSensitive, $absolute while the signature shows a single $delimiter. Trust the source, not the docs. Any method returning a scalar — contains(), isEmail(), and several underscore methods — terminates the chain and must be used as a condition, not a link.
if (string('hello world')->contains('world')) { /* ... */ }
// string(...)->contains('x')->toUpper(); <- TypeError
tease() is not an exact truncation. It trims, collapses double spaces, and strips HTML first — then cuts back to a whole word and appends the indicator (default ...). So tease(10) returns Now that..., which is 8 characters plus the ellipsis. Don't use it where a hard character limit is required (meta descriptions, SMS, database columns), and be aware it's doing HTML stripping for you — which is convenient but means you must not double-escape afterwards in Blade.
possessive() has known edge cases. Two were patched historically: an error on empty strings (2.2.1) and incorrect output for "it" (2.2.2). "Bob" → Bob's, "Charles" → Charles'. Since it's name-based heuristics, don't apply it to arbitrary user input you can't visually verify, and test your actual data set — names ending in sibilants behave differently across languages.
Character offset access is byte-oriented, not multibyte. string('héllo')[1] will not give you what you expect on UTF-8 input, because ArrayAccess maps to byte offsets. Use mb_substr/Str::substr for anything non-ASCII, and keep offset manipulation to ASCII-only identifiers.
Debugging is just var_dump-friendly. There is no logging, no config, no facade, and no state — so when something looks wrong, dd((string) $string) and compare against the raw native call. If the chain returns something unexpected, the usual culprit is a scalar-returning method mid-chain (you'd get a TypeError calling a method on bool) or an underscore method silently returning the wrong thing. There is nothing to cache-bust or clear.
Extension points are limited by design — extend, don't patch. String is a self-contained value object, so the clean extension route is composition: wrap it in your own class or helper functions rather than subclassing, since the string() helper returns the concrete Spatie\String\String class and won't know about your subclass. If you need a new method, add a plain function that takes a string and returns a string — it composes with the chain and is trivially unit-testable without the package at all.
Run vendor/bin/phpunit if you fork it. The package ships its own test suite and uses Laravel Pint for style (as of 3.0.1). If you're evaluating a patch for an edge case in possessive() or segment(), the existing tests are the fastest way to understand the intended contracts — and the safest way to verify an upgrade from 2.x to 3.x doesn't shift behaviour on your data.
How can I help you explore Laravel packages today?