l0n3ly/laravel-dynamic-helpers
Laravel package for managing custom helper classes dynamically. Generate helpers with php artisan make:helper, organize them in nested folders, and call them as global functions like moneyHelper() with auto-registration and singleton caching.
Install and confirm the constraints first. The package requires PHP >= 8.1 and Laravel 11+ (Laravel 10 was dropped in 1.3.0; 13 is supported as of 1.3.0/2.0.0).
composer require l0n3ly/laravel-dynamic-helpers
The service provider is auto-discovered. Note the docs disagree on the next step: the README says nothing else is needed, while the v2.0.0 release notes tell you to run php artisan vendor:publish --tag=dynamic-helpers. Try the publish command once; if the tag doesn't exist you'll get a clear error and can move on. There is no config file documented, so treat publishing as optional.
Verify the boot artefacts before writing any real logic. On the first application boot, v2.0.0 generates _ide_helper.php and .phpstorm.meta.php in your project root and adds them to .gitignore. Load any route once, then check:
ls -la _ide_helper.php .phpstorm.meta.php
git status # .gitignore should now mention both
If those files don't appear, stop and debug the boot path before you invest in helpers — nothing else about the package works without boot-time discovery.
Generate your first helper and use it immediately.
php artisan make:helper MoneyHelper
That creates app/Helpers/MoneyHelper.php extending L0n3ly\LaravelDynamicHelpers\Helper. Wire it into one real call site so you learn the naming contract early:
// app/Http/Controllers/OrderController.php
$minor = moneyHelper()->toMinor($request->amount); // "MoneyHelper" -> moneyHelper()
The three access forms are all live for the same singleton:
moneyHelper()->format(1000); // global function (eval'd at boot)
MoneyHelper::format(1000); // __callStatic forwarding
helpers()->moneyHelper()->format(1000); // proxy
Where to look first when something is wrong: app/Helpers for the files, the base Helper class in vendor/l0n3ly/laravel-dynamic-helpers/src for the __call/__callStatic forwarding, and the service provider's boot() method — that's where directory scanning, eval(), and IDE-file generation all happen. Read that method once; it explains nearly every gotcha below.
Keep helpers stateless and single-purpose. The intended day-to-day use is formatting, permissions, money conversion, thin HTTP wrappers — the stuff you'd otherwise cram into a helpers.php file. Because instances are cached as singletons, anything you store on a helper persists for the request, so avoid per-user or per-tenant state on the helper object itself; pass it in as an argument instead.
Nested helpers for organisation, flattened names at the call site. Subdirectories map to concatenated camelCase, keeping the Helper suffix:
php artisan make:helper Store/Tenant # app/Helpers/Store/TenantHelper.php
php artisan make:helper Payment/Stripe # app/Helpers/Payment/StripeHelper.php
storeTenantHelper()->current();
paymentStripeHelper()->charge($amount);
StoreTenantHelper::current(); // static form uses the class name
helpers()->storeTenantHelper(); // proxy form
Adopt one convention across the team — most codebases will settle on the global function form in controllers/Blade and the class reference in tests — and enforce it in review, because all three work and mixed styles are confusing.
Use the static form in places where a function call reads poorly, e.g. inside static methods, config closures, or when you want the class name visible for grep-ability:
protected static function booted(): void
{
static::creating(fn ($order) => $order->total_minor = MoneyHelper::toMinor($order->total));
}
Callable helpers are the cleanest pattern for single-operation helpers, since they avoid inventing a method name:
class CalculatorHelper extends Helper
{
public function __invoke($a, $b) { return $a + $b; }
}
calculatorHelper(5, 10); // 15
Resolve dependencies inside methods, not the constructor. Helpers are instantiated by the package, not injected, so rely on the container at call time:
class ApiHelper extends Helper
{
public function get(string $url)
{
return app(\Illuminate\Support\Facades\Http::class)->get($url);
}
}
Prefer facades (Http::get(...), auth()->user()) as the README examples do — they're the least surprising inside a helper.
Blade is the highest-value integration point. Helpers read better than @php blocks:
@if(permissionHelper()->can('view-reports'))
{{ moneyHelper()->format($total) }}
@endif
Test the class, not the global function. Because helpers are ordinary classes, write normal unit tests against new MoneyHelper or app(MoneyHelper::class). Reserve one smoke test for the dynamic layer (see Gotchas) so that a broken boot — not a broken method — fails loudly.
Retire existing app/helpers.php files incrementally. Move one function per PR into a helper class, keeping the function name identical where possible (money_format() → moneyHelper()->format()) so call sites convert mechanically. Do not keep both: a hand-written function with the same name as a generated one is a fatal redeclare.
Generated functions are eval'd — collisions are fatal. If a real moneyHelper() exists anywhere (a leftover helpers.php, another package, a test stub), you get Cannot redeclare function. Check with function_exists() before defining anything of your own, and audit composer.json's files autoload for conflicts when adopting the package.
Boot work happens on every request, including writes. v2.0.0 regenerates _ide_helper.php and .phpstorm.meta.php "at every boot cycle". In local dev that means a directory scan plus two filesystem writes per request; with Octane, queue workers, or a read-only/containerised production filesystem it can mean warnings, permission errors, or churn. Look for a guard in the provider and, if none exists, ensure the project root is writable in dev only and that production has the files committed or the writes suppressed. Watch for a measurable boot-time regression on large app/Helpers trees.
Directory discovery depends on autoloadability. A helper is only registered if its class can be resolved. New files under app/Helpers are picked up by PSR-4 without a dump, but renamed namespaces, a non-standard app path, or an optimised/authoritative classmap in production can leave a helper invisible. Symptom: Call to undefined function storeTenantHelper(). Fix: composer dump-autoload and clear cached config.
Flattened names collide silently. Store/TenantHelper and a hypothetical StoreTenantHelper both map to storeTenantHelper(). Likewise beware case-only differences (Store/Tenant vs store/tenant) if any developer is on a case-insensitive filesystem. Namespace your top-level directories and keep them short.
Magic methods defeat static analysis. MoneyHelper::format() goes through __callStatic and is not a real static method; helpers()->xHelper() goes through __call. PHPStan/Psalm will not understand either without the generated stub files. If you run static analysis, either add _ide_helper.php to its scanned paths or standardise on the global-function form (which the .phpstorm.meta.php file covers) and set checkMissingCallableSignature/universalObjectCratesClasses accordingly. Also: PhpStorm only picks up .phpstorm.meta.php from the project root — don't let anyone move it.
Typos fail at runtime, not compile time. There's no registry to validate against; moneyHelpr() is a fatal error in production. Mitigate with a single test that asserts the functions you depend on exist:
public function test_helpers_are_registered(): void
{
$this->assertTrue(function_exists('moneyHelper'));
$this->assertSame(MoneyHelper::class, get_class(moneyHelper()));
}
Singleton caching is a feature and a trap. Same instance per request is great for memoisation, dangerous if a helper caches user-specific data — under Octane a long-lived worker may reuse instances across requests. Keep helpers pure functions; if you must cache, key by auth()->id() and be explicit about it.
Command-name normalisation is forgiving, class names are not. make:helper money-helper, money_helper, and MoneyHelper all yield MoneyHelper. But the generated function name derives from the file path, so reorganising folders is a breaking change to every call site — grep before you move a helper.
Upgrading from 1.x: helpers:ide no longer exists in 2.0.0, and the provider now generates real functions. Any deploy script or CI step calling php artisan helpers:ide will fail. Also re-check that your generated IDE files are gitignored, since v2 writes them automatically and stale committed copies confuse editors.
Vendor risk is real. One star, zero dependents, a single maintainer, and a 2.0.0 that rewrote registration around eval() and boot-time file writes. Pin the version (^2.0 with a committed lockfile), keep the helper classes free of anything exotic so a swap is cheap, and have a fallback in mind: the same ergonomics are achievable with a app/Helpers/*.php set of classes plus a ~15-line helpers.php in composer.json's files autoload. The package's genuine value is the make:helper generator and the IDE stubs, not the runtime machinery — weigh that against the boot-time side effects.
How can I help you explore Laravel packages today?