In Laravel, what does Str::of() return, and why does comparing the end of a Stringable chain with === to a string fail?
answer
- Illuminate\Support\Stringable
- each call returns new static
- __toString, toString(), value()
- exactly() for comparison
- when(), pipe() and is() in chains
basics
~10 sStr::of($value) and str($value) return an Illuminate\Support\Stringable object whose methods each return a new Stringable. It is not a PHP string, so === against a string is false; call toString() or use exactly().
solid answer
~30 s`Str::of()` (or the `str()` helper) wraps a value in `Illuminate\Support\Stringable`, which exposes most `Str` helpers as instance methods: `Str::of($name)->squish()->limit(40)->slug()`. Each method returns `new static(...)`, so the original is never changed. The result is still an **object**: `=== 'blue-mug'` is `false`, `is_string()` is `false`, and a `string` parameter under `declare(strict_types=1)` throws a `TypeError`. It converts wherever PHP casts it — string interpolation, `echo`, Blade's `{{ }}`, `json_encode` via `jsonSerialize()` — but for comparisons and typed APIs, end with `->toString()` (or `->value()`) or compare with `->exactly('blue-mug')`. Chains also offer `when()`, `whenContains()`, `pipe()` and `is()` for conditional and pattern steps.
code
php · 11 lines<?php
use Illuminate\Support\Str;
$slug = Str::of(' Blue Mug ')->trim()->slug();
var_dump($slug === 'blue-mug'); // bool(false): object vs string
var_dump($slug->exactly('blue-mug')); // bool(true)
var_dump($slug->toString() === 'blue-mug'); // bool(true)
echo "/products/{$slug}"; // /products/blue-muggo deeper
Know that Str::of() and str() return a Stringable you can chain, and that you call toString() to get a plain string back.
Explain why === fails, which contexts convert automatically through __toString() or jsonSerialize(), and how when() and pipe() fit into chains.
Keep Stringable objects inside the presentation layer, convert at boundaries, and catch strict-type and array-key failures in review.
Decide team conventions for fluent string APIs versus static helpers so readability gains do not create type confusion at module boundaries.
## What Str::of() gives you `Str::of($value)` is a one-line factory: `return new Stringable($string)`. The global `str($value)` helper does the same. **`Illuminate\Support\Stringable`** stores the value in a protected `$value` property and exposes most `Str` helpers as instance methods, so a series of transformations reads left to right: ```php <?php use Illuminate\Support\Str; $label = Str::of(' blue ceramic mug ') ->squish() // 'blue ceramic mug' ->title() // 'Blue Ceramic Mug' ->limit(12) // 'Blue Ceramic...' ->append(' (gift)'); ``` The static equivalent nests calls inside out — `Str::limit(Str::title(Str::squish($name)), 12).' (gift)'` — which is harder to read and edit. ## Immutable, like the helpers underneath Each method is a thin wrapper: `return new static(Str::slug($this->value, ...))`. So: - the original `Stringable` is **never modified** by a chain step; - you can keep a base value and branch from it (`$base->slug()` and `$base->upper()` are independent); - forgetting to use the result is a silent no-op, just as with collections. ## An object, not a string This is the part interviewers probe. At the end of a chain you hold an **object**: | Operation | Result with a Stringable | |---|---| | `$s === 'blue-mug'` | always `false` (object vs string) | | `is_string($s)` | `false` | | `"Slug: {$s}"`, `echo $s` | works, via `__toString()` | | Blade `{{ $s }}` | works, escaped like any string | | `json_encode(['slug' => $s])` | `{"slug":"blue-mug"}` via `jsonSerialize()` | | `function save(string $slug)` under `strict_types=1` | `TypeError` | | array key `$map[$s]` | error: objects cannot be array keys | Ways to get back to a plain string: 1. `->toString()` or `->value()` — explicit and readable; 2. `(string) $s` — a cast through `__toString()`; 3. compare without converting using `->exactly('blue-mug')`, which also accepts another `Stringable`. A good habit is to end every chain whose result leaves the view layer with `->toString()`. ## Conditional and pattern steps `Stringable` uses the `Conditionable` trait, and adds string-specific conditions: - `when($condition, fn ($s) => …)` and `unless(...)` apply a step only when a condition holds; - `whenContains`, `whenStartsWith`, `whenEndsWith`, `whenEmpty`, `whenIs` and friends test the current value; - `pipe(fn ($s) => …)` runs any callable and wraps the result in a new `Stringable`; - `is('MUG-*')` delegates to `Str::is()`, where `*` matches any run of characters. ```php <?php $title = str($order->product_name) ->squish() ->when($order->is_gift, fn ($s) => $s->append(' (gift)')) ->limit(40) ->toString(); ``` ## A worked order-page chain ```php <?php use Illuminate\Support\Str; $headline = Str::of($order->product_name) ->squish() // collapse stray spaces ->whenStartsWith('Gift:', fn ($s) => $s->after('Gift:')->trim()) ->title() ->limit(30, preserveWords: true) ->when($order->is_gift, fn ($s) => $s->append(' (gift)')) ->toString(); // plain string for the view model ``` Each step reads in the order it runs, conditions sit next to the step they guard, and the final `toString()` makes the boundary explicit. Written with static calls, the same logic needs temporary variables or deeply nested parentheses. Things to notice in review: 1. every closure receives the **current** `Stringable` and should **return** the next one — `when()` falls back to `?? $this`, so a closure that forgets to return leaves the value unchanged and the step silently does nothing; 2. `after()`, `before()`, `between()` and friends return **new** values, so they can be combined freely; 3. the conversion happens once, at the end. ## When to prefer the static helpers - a **single** transformation: `Str::slug($name)` is shorter than `Str::of($name)->slug()->toString()`; - code where types matter and you do not want to remember the final conversion; - hot loops, where creating an object per step is measurable overhead compared with plain calls. Use `Str::of()` when there are **three or more steps** or conditional steps, and convert explicitly at the end. ## Extending it `Stringable` is `Macroable`, so `Stringable::macro('sku', fn () => …)` adds a chainable method, registered in a service provider's `boot()` just like collection macros.
- How do you apply a step in a Laravel Stringable chain only when a condition holds?Use `when($condition, fn ($s) => $s->append(' (gift)'))`, from the `Conditionable` trait; the closure receives the current `Stringable` and its return value continues the chain. For conditions on the string itself there are helpers such as `whenContains()`, `whenStartsWith()` and `whenEmpty()`.
- What does Str::is() match in Laravel, and how does it treat an asterisk?`Str::is($pattern, $value)` returns `true` for an exact match or a wildcard match, where `*` stands for any run of characters, anchored at both ends: `Str::is('MUG-*', 'MUG-042')` is `true`, `Str::is('MUG-*', 'XMUG-042')` is `false`. The pattern can be an array, and a third argument makes it case-insensitive.
saying these in an interview costs you the question
- Believes Str::of() returns a plain PHP string.
- Thinks each chained method modifies the same Stringable in place.
- Compares a Stringable to a string with === and trusts the result.
- Passes a Stringable to a strictly typed string parameter without converting it.
- Says a Stringable cannot be echoed or used in Blade.