svc/util-bundle
A PHP utility bundle that collects shared helpers and common services in one place, helping reduce boilerplate and keep reusable functionality consistent across projects. svc/util-bundle is aimed at simpler maintenance, faster setup, and cleaner Laravel/PHP applications.
First, the framework reality check. Despite the "util" name, svc/util-bundle is a Symfony bundle — the Bundle suffix and Sven-Ve\Svc namespace give it away. There is no Laravel service provider, no facade, no config/util.php to publish, and no auto-discovery. In a Laravel app you have two realistic paths:
composer require svc/util-bundle and instantiate the helper classes directly (autoloading works; the Symfony DI wiring is inert).app/Support/Util/. At 3 stars, 0 dependents, and a single maintainer, this is usually the better call.Minimal steps (inspect-first, for the port path):
composer require svc/util-bundle --prefer-source # pull source so you can read it
Then, before writing a single line of app code:
# Enumerate the public surface — this is what you're actually adopting
find vendor/svc/util-bundle/src -name '*.php' -type f | sort
Open the network, environment, and mail helper classes line by line. You are deciding whether to port or depend; the source is the only documentation.
First use case that survives contact with a Laravel app: the network helper's client-IP resolution. Laravel's Request::ip() respects TrustProxies but is coarse for multi-hop CDNs and gives you little visibility into why it picked an address. A small diagnostic endpoint or admin "who am I" panel that surfaces the resolved IP plus the header precedence used is a genuinely useful debugging tool:
// app/Http/Controllers/Diagnostics/ClientController.php
public function show(Request $request)
{
return response()->json([
'laravel_ip' => $request->ip(),
'forwarded_for' => $request->header('X-Forwarded-For'),
'real_ip' => $request->header('X-Real-IP'),
'cf_connecting' => $request->header('CF-Connecting-IP'),
]);
}
Note that this sample is all Laravel primitives — it works without the package. That's the point: run the spike, and if the package's helper doesn't beat three lines of $request->header(), port or skip it.
Wrap, never call directly. Regardless of which helpers survive your spike, do not scatter static calls across controllers and jobs. Put a first-party interface in front of them so the dependency is swappable and you can test against your contract rather than the package's behavior.
// app/Support/Util/NetworkInfo.php
namespace App\Support\Util;
interface NetworkInfo
{
public function clientIp(\Illuminate\Http\Request $request): string;
}
// app/Support/Util/BundleNetworkInfo.php
class BundleNetworkInfo implements NetworkInfo
{
public function clientIp(\Illuminate\Http\Request $request): string
{
// Ported or wrapped resolution — the point is the class boundary.
return $this->resolve($request);
}
}
// app/Providers/AppServiceProvider.php
$this->app->bind(\App\Support\Util\NetworkInfo::class, \App\Support\Util\BundleNetworkInfo::class);
Environment helpers: config-time only. If the package offers env detection (isProduction(), getEnv(), etc.), call it inside config files only, never from a controller or job. Laravel's config:cache flattens env() at deploy time; any per-request env() call hits the filesystem, breaks under Octane's long-lived workers (stale values across requests), and produces the classic "works in staging, wrong in prod after deploy" bug. The correct pattern is:
// config/app.php or a dedicated config/util.php
'env_name' => (string) env('APP_ENV', 'production'),
'debug' => (bool) env('APP_DEBUG', false),
Then read config('app.env_name') everywhere else. Never App\Support\Util\Env::get() per request.
Mail helper: route it through Laravel's Mail facade. If you decide the package's mail transport config is worth keeping, use it only to build a mailer, never to send directly. Anything that instantiates a Swift_Mailer/Symfony\Component\Mailer\Mailer directly bypasses the Mailer contract, which means:
Mail::fake() in tests will not intercept your sends — you get silent delivery attempts during test suites;MessageSending, MessageSent) don't fire, so logging and monitoring go blind;Correct usage pattern:
// Set config from the helper at boot, then use Laravel's Mail normally.
// config/mail.php reads env(), and the package (if kept) informs those values.
Mail::to($user)->send(new WelcomeMail($user)); // always this, never the package's send()
Porting pattern for the environment helper. If you go the vendoring route, keep the class in app/Support/Util/ and give it a test that asserts your expected behavior, not the package's:
public function testEnvironmentDetectionHandlesConfigCache()
{
putenv('APP_ENV=production');
$this->assertTrue(app(Env::class)->isProduction());
// Simulate config:cache flattening
config(['app.env_name' => 'staging']);
$this->assertSame('staging', config('app.env_name'));
}
The pattern that matters is: the adapter's tests define your contract, not the package's implementation. That's what lets you swap or fork without a rewrite.
It is not a Laravel package — stop expecting Laravel ergonomics. No auto-discovery, no Util::network()->ip(), no publishable config. The Symfony bundle class is dead weight in Laravel and will confuse anyone reading composer.json later. If you adopt, document in your README why a Symfony bundle is a dependency of a Laravel app — otherwise someone will remove it as "unused" during a dependency audit and break the build.
The env() / config:cache trap is the number-one production incident source. Symfony-style env helpers that read $_ENV or getenv() per call will:
null after config:cache if the helper is used outside a config file.Grep your codebase before adopting for any direct use of getenv(), $_ENV, or $_SERVER — if the package normalizes those, it will normalize the same broken way in every entry point, and you lose per-context control. In Laravel the correct fix is config('…'), full stop.
Network helper: verify timeouts and header precedence. The package's value proposition (proxy parsing) is also its risk surface. Before relying on it:
file_get_contents with no timeout will hang a queue worker on a flaky network call.X-Forwarded-For order varies between Cloudflare, AWS ALB, nginx, and Traefik — pick a precedence, write it in a comment, and test it against a mocked request with all headers set.Mail helper: three specific test-coverage holes to check.
# 1. Does the helper call Mailer directly?
grep -R "new .*Mailer\|Swift_Mailer\|Symfony\Component\Mailer" vendor/svc/util-bundle/src
# 2. Does it leak credentials into exceptions?
grep -R "password\|secret\|token" vendor/svc/util-bundle/src
Mail::fake() doesn't intercept, your test suite is actually sending mail (or trying to, and silently failing to localhost:25 — which is worse because it hides the fact that tests never verify mail).Extension points are near zero — assume fork.
app()['svc.util'] singleton to decorate.The pragmatic play: composer require, read the two or three classes you care about, copy them into app/Support/Util/, add a @see comment pointing at the upstream repo, and composer remove. You keep the convenience, drop the supply-chain surface, and remove the "who maintains this?" question from your next incident review.
Debugging technique — when something looks environmental, it's usually config cache, not the package. Standard triage:
php artisan config:clear
php artisan config:cache
php artisan tinker --execute "dump(config('app.env'), env('APP_ENV'))"
If env() and config() disagree in tinker, your config was built before the env var was set — a deploy ordering bug, not a package bug. Fix the deploy; don't patch the helper.
The maintenance reality. Single maintainer, 3 stars, zero dependents, no support policy, no CI matrix advertised. Any engineer can read the entire API in under an hour (ramp-up cost is low), but the due diligence — checking the test suite, verifying timeout enforcement, auditing the mail path, wrapping it behind an adapter — is disproportionate to a package whose total value is likely a few dozen lines of convenience. If your PoC cannot articulate a gap versus $request->ip(), config(), and Mail, the correct answer is to skip the dependency. The port is under a day and gives you a first-party class with tests that fail your way, not someone else's.
How can I help you explore Laravel packages today?