Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Utils Laravel Package

nette/utils

nette/utils is a collection of handy PHP utilities used by the Nette Framework: string and array helpers, smart objects, reflection, file system, JSON/HTML tools, callbacks, and more. Lightweight, dependency-free components that simplify everyday PHP development.

View on GitHub
Deep Wiki
Context7

Getting Started

Install

composer require nette/utils

There is no service provider, no facade, no config file, and no publishable assets. Installation is the entire integration step — every class is a plain static utility under the Nette\Utils namespace.

Pick the right major for your Laravel version

nette/utils PHP requirement Fits
^4.1 PHP 8.2+ Laravel 11 / 12
^4.0 PHP 8.0 – 8.6 Laravel 9 / 10 on older PHP
^3.2 PHP 7.2+ Legacy apps only — avoid

The 4.0 branch is still actively maintained: 4.0.11 added PHP 8.6 support, so the line now spans PHP 8.0 through 8.6. That matters if you're pinned to 4.0 because of an older runtime — you can still take patch releases (security and compatibility fixes) without jumping to 4.1's PHP 8.2 floor. There are no API changes in 4.0.11, so nothing to refactor; it's a runtime-compatibility bump only.

Check whether you already have it

Nette utilities are a transitive dependency of latte/latte, nette/forms, nette/application and several testing tools, so the package may already be in your composer.lock:

composer why nette/utils
composer show nette/utils

If it is present via a transitive dependency, pin it explicitly in require before you start calling it, or a future upstream bump can silently change the major version under you. This cuts both ways on the 4.0 branch: a transitive ^4.0 constraint will now happily resolve 4.0.11 on a PHP 8.6 box, which is fine, but it also means upstream constraint changes move you between minors without a code review.

Where to look first

Skip the blog posts. The classes are one-per-file, 200–600 lines each, heavily documented with generics and PHPDoc, and there is no magic:

vendor/nette/utils/src/Utils/Strings.php
vendor/nette/utils/src/Utils/Arrays.php
vendor/nette/utils/src/Utils/Json.php
vendor/nette/utils/src/Utils/FileSystem.php
vendor/nette/utils/src/Utils/Process.php      # 4.1.4+
vendor/nette/utils/src/Utils/Image.php

API reference lives at api.nette.org. Release notes matter here: this library ships frequently and the CHANGELOG is the real documentation for new methods — including maintenance-branch releases like 4.0.11, which are usually one-line platform-support entries rather than feature drops.

First use case — the one that pays for itself immediately

Filename validation on uploads, before you touch Storage:

use Nette\Utils\FileSystem;

$original = $request->file('attachment')->getClientOriginalName();

if (!FileSystem::isValidFilename($original)) {
    // rejects path separators, control/reserved chars,
    // trailing dots/spaces, and Windows device names (CON, LPT1, …)
    throw ValidationException::withMessages([
        'attachment' => 'That filename is not portable across operating systems.',
    ]);
}

That single call catches ../../etc/passwd, aux.txt, report.pdf. and a dozen other cases that a basename() + Str::slug() combo usually misses. From there, the natural second step is Strings::webalize() for slugs and Json::decode() at API boundaries.


Implementation Patterns

1. Unicode-aware slugs that don't mangle non-Latin scripts

Str::slug() transliterates to ASCII, which is usually what you want for URLs. Strings::webalize() is different: it normalises Unicode, strips punctuation, and preserves characters that don't need transliteration.

use Nette\Utils\Strings;

Strings::webalize('Příliš žluťoučký kůň');   // 'prilis-zlutoucky-kun'
Strings::webalize('České Budějovice', '-', false); // case preserved

Practical split: keep Str::slug() for canonical routes, use Strings::webalize() where you need deterministic, locale-independent normalisation (search keys, cache keys, dedupe hashes, import matching). Pair it with Strings::toAscii() when you specifically need ASCII out of German/Central-European text — note that toAscii() had a BC break in 3.2.1 that fixed umlaut handling, so don't compare outputs against an old fixture set.

2. JSON at the boundary — fail loudly, not silently

json_decode() returns null for malformed input and for the literal null, which is how bad payloads slip into your database.

use Nette\Utils\Json;
use Nette\Utils\JsonException;

final class WebhookPayload
{
    public static function fromRaw(string $body): self
    {
        try {
            $data = Json::decode($body, forceArrays: true);
        } catch (JsonException $e) {
            // message includes the decoder error + a snippet of the payload
            report($e);
            throw new InvalidArgumentException('Malformed webhook body', previous: $e);
        }

        return new self($data);
    }
}

Useful specifics:

  • Json::decode($s, forceArrays: true) — always arrays, never stdClass, so downstream data_get() and Arrays:: helpers work uniformly.
  • Json::decodeFile($path) — read fixtures, config snapshots, or queue payloads from disk without a manual file_get_contents + decode + error-check dance.
  • Json::encode($value, pretty: true) — for CLI output and log dumps. Nette throws a JsonException on encoding failures (recursion, invalid UTF-8) instead of returning false.

Caveat on encoder flags: Nette handles JSON flags its own way and does not mirror Laravel's response()->json() defaults. Don't replace response()->json() with Json::encode() inside controllers without diffing the output — you will change escaping of unicode and slashes.

3. Path containment before writing to disk

Anything derived from user input that ends up in a filesystem path should be resolved and checked:

use Nette\Utils\FileSystem;

$base = storage_path('app/imports');
$target = FileSystem::resolvePath($relative, $base);

if (!str_starts_with($target, $base . DIRECTORY_SEPARATOR)) {
    abort(403);
}

resolvePath() normalises ./.. collapses, which kills the classic traversal attack. Combine it with isValidFilename() on the final segment — one guards containment, the other guards portability.

4. Subprocess execution with zero shell surface

The Process class (4.1.4+) is the strongest reason to reach for this package in a Laravel app. Laravel 10+ ships Illuminate\Process, so this is where the package genuinely adds a distinct capability: arguments-as-array execution.

use Nette\Utils\Process;

final class ThumbnailJob implements ShouldQueue
{
    public function handle(): void
    {
        $process = Process::runExecutable('/usr/bin/ffmpeg', [
            '-i', $this->sourcePath,
            '-vf', 'scale=320:-1',
            '-y', $this->targetPath,
        ]);

        $process->setTimeout(120);
        $process->run();

        $process->consumeStdError(function (string $chunk) {
            Log::channel('ffmpeg')->warning(trim($chunk));
        });

        if ($process->getExitCode() !== 0) {
            throw new RuntimeException(
                'ffmpeg failed: ' . $process->getStdError()
            );
        }
    }
}

Key points:

  • runExecutable($binary, array $args) bypasses the shell entirely — an argument containing ;, $(…), or backticks is inert. This is the pattern to standardise on for any user-influenced argument (a filename, a title, a template name).
  • runCommand($string) is the escape hatch for genuine shell pipelines (pipes, redirects, environment expansion). Treat it like DB::raw(): allowed, deliberate, never fed from request data.
  • consumeStdOutput() / consumeStdError() let you stream progress into logs or a cache key while the process runs, instead of hanging until completion and blowing the memory limit on a chatty tool.
  • Timeouts are enforced by the class; a killed process surfaces as a failure you can catch and retry via queue middleware.

Use it for: PDF rendering (wkhtmltopdf, headless Chrome), media transcoding, git operations in a deploy command, Docker/CLI orchestration inside an Artisan command. Do not use it for anything the Symfony Process component is already doing inside your dependency tree — you'd have two process abstractions in one codebase.

5. Search-hit highlighting with regex offsets

Strings::match() / matchAll() can return capture offsets, which is exactly what a "matched in context" search snippet needs:

use Nette\Utils\Strings;

$result = Strings::match($haystack, $pattern, captureOffset: true);

if ($result !== null) {
    [$full, $offset] = [$result[0], $result[0][1]]; // [text, byte offset]
}

Also worth knowing: Strings::replace($subject, $pattern, $replacement, $limit) accepts a callback and defaults $replacement to an empty string (so Strings::replace($s, '/\s+/') just strips whitespace). Laravel's Str has no regex replace at all, so this fills a real gap without pulling in a helper package.

6. Array helpers that complement, not duplicate, Arr

Laravel's Arr covers most ground. Use Arrays:: where it adds something:

use Nette\Utils\Arrays;

Arrays::first($rows, fn ($r) => $r['status'] === 'failed', else: null);
Arrays::last($rows, fn ($r) => $r['status'] === 'failed');
Arrays::firstKey($rows);      // no equivalent in Arr
Arrays::lastKey($rows);
Arrays::filter($assoc, fn ($v, $k) => $k !== 'internal'); // key-aware predicate
Arrays::mapWithKeys($items, fn ($i) => [$i->id => $i->name]);
Arrays::invokeMethod($objects, 'toArray');  // bulk call, skips the loop boilerplate

Rule of thumb: don't mix Arr:: and Arrays:: inside one class. Pick one per file so reviewers aren't context-switching on argument order and callback signatures (Nette passes ($value, $key); Laravel passes ($value, $key) too, but the else/predicate parameter positions differ).

7. Cheap validation outside the validator

Laravel's validation rules are the right tool for request input. Validators:: is useful for infrastructure checks — config sanity in a service provider, a CLI preflight, a health check:

use Nette\Utils\Validators;

if (!Validators::isUrl(config('services.billing.base_url'))) {
    throw new RuntimeException('billing.base_url is not a valid URL');
}

Available: isEmail, isUrl, isNumeric, isInRange, isTypeDeclaration, isBuiltinType, isPhpIdentifier, isClassKeyword. None of these should replace 'email' => 'email' in FormRequests — Laravel's rule uses egulias/email-validator and is RFC-complete; Nette's is a pragmatic check.

8. Paginating non-database sources

Nette\Utils\Paginator only needs a total item count and items-per-page — no query builder, no Collection:

use Nette\Utils\Paginator;

$paginator = new Paginator;
$paginator->setItemsPerPage(25);
$paginator->setItemCount(count($allResults));   // e.g. from an API or a file
$paginator->setPage((int) $request->query('page', 1));

$slice = array_slice($allResults, $paginator->getOffset(), $paginator->getLength());

Useful for proxying a third-party API's results, paginating generated reports, or Artisan command output. It gives you getPageCount(), isFirst(), isLast(), getFirstItemOnPage(), getLastItemOnPage() — but it is not a LengthAwarePaginator, so you'll need a small mapper to keep your JSON shape consistent with the rest of the API.

9. Image inspection without pulling in Intervention

For metadata only — type, dimensions, sanity — Nette\Utils\Image is lighter than a full manipulation library:

use Nette\Utils\Image;

$info = Image::detectTypeFromFile($path);   // [type, width, height] or null
$image = Image::fromFile($path, $warnings);

if ($warnings !== null) {
    Log::warning('GD warning on upload', ['warnings' => $warnings]);
}

The $warnings parameter added in 4.1.4 is the good part: GD warnings that used to leak into PHP's error handler now come back as a string you can log with upload context attached. ImageType is an enum and Image::getSupportedTypes() tells you what the current GD build actually supports (WebP/AVIF availability varies wildly across base images).

10. Wrapping instead of scattering

Nette\Utils\String* and Arrays are static, so they're untestable-by-substitution and they leak a third-party type into your domain if you type-hint them. One thin adapter per concern keeps both problems away:

final class Slugger
{
    public function slug(string $value): string
    {
        return Strings::webalize($value, '-', true);
    }
}

Bind it in a provider, inject it, and your tests stay clean, your domain stays Laravel-shaped, and swapping implementations later is a one-file change.


Gotchas and Tips

No Laravel integration means no safety net. No exception rendering, no config, no facade, no Storage::fake() compatibility. Everything is a bare PHP exception. If a JsonException escapes a controller it becomes a 500, not a 422 — catch and re-map it to ValidationException or an HTTP exception explicitly.

FileSystem is not a Laravel disk. It writes to the real filesystem with fopen/rename/unlink. It knows nothing about Flysystem, S3, or your config/filesystems.php disks, and Storage::fake() in tests will not intercept it. A test that exercises FileSystem::write() will mutate your actual disk. Use it only for local, app-owned paths (cache warmers, build artefacts, temp files under storage_path()), and keep Storage for anything user-facing or remote.

Nette\HtmlStringable is not Illuminate\Contracts\Support\Htmlable. Blade's e() only skips escaping for Laravel's Htmlable. Passing Nette\Utils\Html::el('b') into {{ }} will double-escape it into visible markup. If you use Html at all, call ->render() (or (string)) yourself, or convert to HtmlString.

Process vs Illuminate\Process\Process name collision. Import one, alias the other, and be consistent. A file that uses both is a code-review trap.

proc_open may be disabled. Shared hosting and hardened PHP images frequently disable it via disable_functions. Process will fail at runtime, not at install time. Add a preflight check in your deploy or health check if subprocess execution is a core feature.

Nette\Utils\DateTime extends \DateTime and is strict. DateTime::from() throws on unparseable input (the v4.1.0 "strict behaviour" BC break) instead of returning false or silently coercing like Carbon. It also supports Nette-specific relative syntax ('ago', relativeToSeconds()). The danger: it's mutable, so a $date->modify() mutates in place — the exact bug Carbon's immutability was designed to prevent. Do not mix it with Carbon in the same model; keep CarbonImmutable for app-level dates and use Nette's DateTime only when you specifically need its parser.

Don't swap your validation rules. Validators::isEmail() and Validators::isUrl() are pragmatic, not RFC-complete. Replacing Laravel's email/url rules with them silently loosens input validation.

Strings::length() has a fallback chain — mbstring → iconv → a UTF-8 hack. On an image without ext-mbstring and with a peculiar iconv implementation, results can differ from Str::length(). This is a real risk in slim Docker images. Add mbstring to your base image as a hard requirement rather than relying on the fallback.

Strings::webalize() requires ext-intl as of 4.1.0. Most php:8.x-fpm images ship it, but many Alpine/minimal builds don't. A missing intl is a fatal error at call time, usually discovered in production. Add it to your Dockerfile alongside gd if you're using Image.

Verify Strings::truncate() against Str::limit() before replacing anything. Character-vs-byte behaviour and ellipsis handling differ; a truncation that looks identical on English fixtures will split multibyte characters differently. Write a test with CJK and emoji strings before you swap.

Strings::replace() defaults the replacement to an empty string. Convenient, and also how people accidentally delete text: Strings::replace($s, $pattern) is a strip, not a no-op.

Image is GD-only. No Imagick, no EXIF orientation correction, no colour profiles. If uploads come from phones, you need orientation handling that this class won't give you. Confirm your GD build supports the formats you accept via Image::getSupportedTypes() at boot rather than discovering it on the first AVIF upload.

ArrayHash/ArrayList fight with Eloquent. Magic property access on ArrayHash is fine standalone, but if you cast a model attribute to it, Eloquent's own magic attributes, toArray(), and dirty tracking all get confusing. Use them for plain DTOs deserialised from JSON — that's the sweet spot — not as model casts.

Static classes make mocking painful. You cannot inject a fake Strings. Anything with branching logic worth testing should be behind your own adapter (see pattern 10). For pure deterministic calls, test the outcome, not the call.

License is a triple-license with NOASSERTION in Composer's metadata. Nette ships BSD-3-Clause / GPL-2.0-only / GPL-3.0-only. In practice everyone takes BSD-3-Clause, but if your organisation runs automated license gates, this package will trip a NOASSERTION flag and needs a manual allowlist entry. Do that before you introduce it, not after.

Version drift catches people upgrading Laravel. Because nette/utils often arrives transitively, a Laravel upgrade that bumps an intermediate package can drag the major version from 3.x to 4.x — bringing the DateTime strictness BC break and the Reflection::getReturnType() removals with it. Pin the major in composer.json and read the CHANGELOG on every minor bump; releases here ship small but behaviour-changing tweaks (Strings::trim() gained Unicode space and ZWSP handling in 4.0.4 and 4.0.8).

Platform-support patches arrive on the old branch. 4.0.11 (PHP 8.6 support) landed on the 4.0 line, not as a 4.1 release — so if you're on 4.1 and planning a PHP 8.6 upgrade, check the corresponding 4.1.x release before assuming you're covered. Conversely, if you're stuck on 4.0 for a PHP 8.0/8.1 reason, you still get runtime-compatibility fixes on that branch; don't treat 4.0 as abandoned, but do set a composer update --dry-run habit so you notice when a maintenance release lands. Neither branch changed API in this release, so no code changes are needed for existing users.

Finder is not a Storage or Symfony\Finder replacement. It was fully rewritten in 4.0, has a known Windows symlink issue with is_file(), and operates on real paths only. If you're already using Symfony Finder (it's in every Laravel install via symfony/finder), adding nette's Finder gives you two near-identical abstractions for zero benefit.

Deprecation notes are invisible to static analysis. @property-deprecated annotations are a Nette convention; PHPStan and PhpStorm won't flag them. If you subclass SmartObject-based classes, watch the CHANGELOG manually.

When in doubt, read the release notes first. The README is thin and the API docs lag behind new methods (Process, FileSystem::isValidFilename(), Arrays::mapWithKeys(), Iterables::memoize()). release/v4.x.y.md files in the repo are the authoritative source for what's newly available and what's newly strict.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource