oskarstark/trimmed-non-empty-string
A tiny PHP value object that trims surrounding whitespace from strings and guarantees the result is non-empty, throwing an exception when validation fails. Use it to keep user input and domain values clean, predictable, and safe.
oskarstark/trimmed-non-empty-string is a tiny, framework-agnostic value object that guarantees a string has been trimmed and is not empty. There is nothing to register, no config to publish, no service provider — it is a plain PHP class you composer require oskarstark/trimmed-non-empty-string and start type-hinting.
First steps
composer require oskarstark/trimmed-non-empty-string (the 1.x line is stable; ^1.11 is a safe constraint — see the note on 1.11.2 below).src/ before the README — it is a handful of classes: the value object itself plus the exception thrown when the string is empty (or whitespace-only) after trimming. Confirm the exact factory method name (fromString() / from()) and the accessor (toString() / __toString() / value()) there; it takes two minutes and saves guessing.tests/ for the edge-case matrix the maintainers care about (empty string, whitespace-only, leading/trailing whitespace, already-clean input). That is effectively the spec.First use case: anything that arrives from a user or a config file and is then trusted to be non-blank — a form field, a webhook payload name, a .env-backed setting, a header value. Replace trim($request->input('name')) !== '' scattered across controllers with one constructor call and let the exception (or your own catch) handle the failure path.
On 1.11.2 specifically: this release is tooling-only — ergebnis/php-cs-fixer-config with PHP 8.2 rules, a Rector config, PHP_CS_FIXER_IGNORE_ENV cleanup, and assertNotSame test cosmetics. No runtime, API, or behaviour changes. Bump it without a second thought; it is a patch on an already-published API.
Constructor-time validation in DTOs / form objects. Parse the request once, build typed objects, pass those around. The value object does the trimming and the emptiness check in the same place, so downstream code never needs to re-trim():
final class CreateProjectData
{
public function __construct(
public readonly TrimmedNonEmptyString $name,
) {}
public static function fromRequest(ProjectRequest $request): self
{
return new self(
TrimmedNonEmptyString::fromString((string) $request->input('name')),
);
}
}
Catch the package exception in the request/DTO factory and rethrow as ValidationException::withMessages([...]) so Laravel returns a 422 rather than a 500. This is the pattern you will write on day one and reuse everywhere.
Wrap it in a validation rule, not in every controller. A small TrimmedNonEmpty rule (Rule::class returning a closure that tries the factory and fails on exception) keeps validation in FormRequest::rules() where the rest of the team expects it, and keeps the value object for the parsing step afterwards. Do both: rule for the user-facing message, factory for the invariant.
Eloquent casts for "optional but not blank" columns. A custom cast (CastsAttributes) that maps null ↔ null and otherwise delegates to the factory gives you the trim on save and the guarantee on load. Beware: this silently rewrites stored values, so pair it with a backfill/migration check if the column already has untrimmed data.
Config objects over config() calls. Resolve config once in a service provider into typed constructor arguments. A TrimmedNonEmptyString-typed parameter means a stray trailing newline pasted into .env fails at boot with a clear exception instead of producing a 404 on a route prefix three weeks later. This is the highest-leverage use of the package in a Laravel app.
Collection boundaries. When mapping an array of inputs (tags, recipients, header lines), use the factory as the filter predicate and let the exception drop invalid entries, or try/catch per item to collect messages. Prefer explicit per-item reporting over array_filter silently swallowing the fact that someone submitted a whitespace-only row.
Equality and comparisons. Because values are normalised at construction, ===-style comparisons between two instances of the same class are finally meaningful. Compare the value objects in domain code, not the raw strings.
trim(). That means ASCII whitespace by default and nothing else — no zero-width spaces, no non-breaking spaces, no Unicode whitespace, no case-folding, no normalisation. If your input can contain \u{00A0} (pasted from a browser or Excel), it will survive and your "non-empty" object will look blank in the UI. Pre-sanitise, or wrap the factory in a helper that strips your own character set first.ValidationException. Letting it bubble from a controller yields a 500. Wrap it at the boundary (requests, jobs, console commands) — and don't do a broad catch (\Throwable) in a job that hides genuine bugs behind a retry loop.serialize()/unserialize() (a single readonly string property will; anything holding a closure or resource won't). If in doubt, put the plain string on the job and build the value object in handle().$obj->value = 'x' will fail loudly, which is the desired behaviour but surprises people migrating from helper-function style.->toString() noise, mapping layers, factories in every fixture). Reserve it for boundaries and invariants — request input, config, external payloads — and let it fall back to string once inside a well-tested method.mb_-based normalisation later without touching call sites.^1.11 and let Renovate/Dependabot merge them; you only need to read the changelog when a 2.0 lands.How can I help you explore Laravel packages today?