nextstage-brasil/ns-util
A collection of general-purpose utilities for Laravel and PHP applications from NextStage Brasil, offering handy helpers that cut boilerplate and speed up everyday development tasks.
1. Confirm how you can even install it. The repo lives on GitLab under public-projects, so assume it is not on Packagist until you prove otherwise. Try the plain install first:
composer require nextstage-brasil/ns-util
If that fails, add a VCS repository entry (and make sure CI has read credentials for the GitLab host):
{
"repositories": [
{ "type": "vcs", "url": "https://gitlab.com/nextstage1/public-projects/ns-util.git" }
],
"require": {
"nextstage-brasil/ns-util": "dev-main#<commit-sha>"
}
}
Pin a commit SHA, not dev-main. Commit composer.lock.
2. Read composer.json before you read any PHP. Four fields decide everything:
autoload.psr-4 — the namespace you'll actually type.autoload.files — if this is present, the package registers global functions. Those are unremovable, uncatchable-on-collision, and the single biggest reason to wrap the package rather than call it directly.require — the illuminate/* and php constraints. If they stop at Laravel 9/PHP 8.0, budget for a fork now.license — no license file is a legal gate, not a nitpick.3. Inventory the surface. There's no README worth trusting, so generate the API list yourself:
# what classes exist
find vendor/nextstage-brasil/ns-util/src -name '*.php'
# what's publicly callable
grep -rn "public function\|public static function" vendor/nextstage-brasil/ns-util/src
# global functions, if any
grep -rn "^function \|^ function " vendor/nextstage-brasil/ns-util/src
Write that list down. Utility packages are typically 80% surface you will never call, and the 20% you do call is your entire risk surface.
4. Run whatever tests exist before trusting anything.
cd vendor/nextstage-brasil/ns-util && composer install && ./vendor/bin/phpunit
No tests, or tests that don't cover the methods you want? Then treat every helper as unverified and write characterization tests against known vectors yourself (see Gotchas).
5. First real use case. The value proposition is Brazilian localization: CPF/CNPJ formatting and checksum validation, CEP formatting, currency (R$ 1.234,56), and dd/mm/yyyy dates. That is the only category here where the package can plausibly beat 30 lines of your own code, because document checksums and currency placement have fiddly edge cases. Everything else in the package — string, array, number helpers — is already Str::, Arr::, Number::, and Carbon.
So the first call site should be something like a Blade/API formatter, not a domain rule:
// app/Support/Br.php — your adapter, not a direct package call
namespace App\Support;
class Br
{
public static function cpf(?string $value): ?string
{
return $value ? \NextStage\Util\...::formatCpf($value) : null; // verify FQCN
}
}
Then use App\Support\Br::cpf($user->cpf) everywhere. One file to change if you drop or fork the package.
Never let nextstage-brasil/ns-util classes appear outside app/Support/. Helpers have no architectural boundary — they diffuse into controllers, jobs, models, and Blade — and that ambient coupling is what makes a low-value dependency expensive to remove. A one-class adapter keeps the exit a single-file change.
final class Br
{
public static function money(int $cents, bool $symbol = true): string { /* delegate */ }
public static function date(?CarbonInterface $d): ?string { /* delegate */ }
public static function cpfIsValid(string $cpf): bool { /* delegate */ }
}
Formatting and validation-for-display belong in resources, Blade components, and API transformers. Keep raw values in the database:
'12345678909'), never int — leading zeros die.float. R$ 19,90 is not representable in binary floating point, and no formatter can rescue a value that was already wrong at write time.config('app.timezone') = 'UTC') and convert to America/Sao_Paulo only at render. A helper that assumes São Paulo on the way in is a data-corruption bug waiting for a second timezone.// app/Http/Resources/OrderResource.php
'formatted_total' => Br::money($this->total_cents),
'created_at_br' => Br::date($this->created_at),
If the package ships validation helpers, wire them through a custom rule rather than calling them from controllers. That way the failure message, the translation, and the field name stay yours:
class CpfRule implements ValidationRule
{
public function validate(string $attribute, mixed $value, Closure $fail): void
{
if (! Br::cpfIsValid((string) $value)) {
$fail('O :attribute informado não é um CPF válido.');
}
}
}
Always normalize before validating — strip ./-/spaces — because a helper that rejects masked input while your form posts 123.456.789-09 will look like a false negative to users.
{{-- resources/views/components/money.blade.php --}}
@props(['cents'])
<span>{{ \App\Support\Br::money($props->cents) }}</span>
Centralizing keeps formatting consistent across products and means an eventual swap is one Blade file.
If the package can generate valid documents (not just validate), use it in factories — this is genuinely high-leverage and low-risk, because bad seed data is only ever a local annoyance:
'cpf' => fn () => Br::generateCpf(), // verify this exists
Never use generated documents in production data or in tests that assert on specific document values.
Treat every helper as potentially impure. If a helper resolves from the container (app(), config(), DB::) it is not a free pure function, and calling it 10k times in an import command is a hidden per-call cost. Hoist config lookups and formatters out of loops:
$fmt = Br::money(...); // one resolution
foreach ($rows as $row) { $out[] = $fmt($row->cents); }
app/Support graveyards.composer outdated-triggered re-audit against the next Laravel major.Global functions are a trapdoor, not a convenience. If autoload.files is set, those function names live in the global namespace forever. A Laravel upgrade, a PHP minor that adds a function with the same name, or a second utility package declaring str_limit() gives you a fatal error at autoload time — not an exception you can catch, not a degraded feature. Check for autoload.files before adoption, and if it exists, wrap immediately and prefer the class-based API even if it's uglier.
Unstated or stale version constraints are the real cost. A utilities package has no runtime footprint but a permanent compatibility commitment. If require says illuminate/support: ^8.0|^9.0, you are adopting a fork on your next Laravel major, not a dependency. Check this first; it's the fastest disqualifier.
Silent drift is worse than breakage. A formatting helper that returns the wrong string after a version bump throws nothing, logs nothing, and shows up as a customer complaint. This is why characterization tests matter — pin the package, then assert on its actual output for the cases you use:
it('formats BRL like the package does today', function () {
expect(Br::money(123456))->toBe('R$ 1.234,56');
});
If upstream changes behavior, your suite fails instead of your invoices.
Do not trust document checksums without vectors. Test CPF/CNPJ validation against a fixed list of known-valid and known-invalid numbers, including: repeated digits (111.111.111-11 — passes many naive algorithms, is invalid), masked vs. unmasked input, and strings with leading zeros. Note that Brazil's alphanumeric CNPJ format rolls out over 2026 — if your roadmap extends past that, check whether the package handles it. If the package's only real justification is document validation, budget the day to write the checksum yourself and remove the dependency.
Money helpers assume the value is already correct. A formatter cannot fix a float that was rounded on write, and number_format($x, 2, ',', '.') will happily render a value that lost a cent three layers ago. Keep cents as integers; use the helper for display only.
Locale and timezone are usually hardcoded. Agency utility packages frequently hardcode America/Sao_Paulo, pt_BR, or R$ rather than reading config('app.locale'). Grep the source for 'R$', 'America/', and 'pt_BR'. If you have any non-Brazilian traffic or tenants, a hardcoded locale in a shared helper is a compliance-flavored bug in a formatting wrapper.
Watch for hidden I/O in "pure" helpers. Formatting functions that quietly hit the DB, an HTTP CEP lookup (ViaCEP-style), or config() are the classic agency-package smell. An HTTP call inside a Blade formatter is an N+1 waiting to happen and is invisible in profiling because it looks like a pure function. Grep for Http::, GuzzleHttp, DB::, Cache::, app( in src/.
Loose constraints can break unrelated packages. A utility package with "nesbot/carbon": "*" or a dev-dependency accidentally in require can force-resolve against your pinned Laravel and break the whole graph. Run composer why / inspect the lock diff after install.
Debugging workflow when there's no README:
php -r "require 'vendor/autoload.php'; var_dump(get_class_methods(\NextStage\Util\ClassName::class));"
Then build a 10-line scratch script that exercises each method you intend to use with real inputs from your app, and eyeball the output. Half a day, once, saves a week of mysteries later.
Extension points are minimal by design — which is fine. A service provider and possibly a facade are all you should expect; no migrations, no queue jobs, no publishable config (verify with php artisan vendor:publish --provider=...). If a config file is publishable, check whether it ships sane defaults or empty strings, and whether the package works when it's never published.
If any of the above reads as too much friction: that's the signal. Write the five functions you need into app/Support/Br.php with attribution, skip the dependency, and keep your illuminate/* constraints and your global namespace to yourself.
How can I help you explore Laravel packages today?