leovie/php-method-runner
A lightweight PHP utility that lets you run methods on objects by name, making it easy to invoke dynamic or configurable method calls without verbose reflection code. Ideal for small Laravel or standalone PHP projects.
Install and inspect the source first. The package is tiny and low-traffic, so the most reliable documentation is the code itself:
composer require leovie/php-method-runner:^3.0
Then read README.md, src/, and tests/ before wiring anything into a Laravel app. The tests are effectively the spec.
Locate the runner entry point. The package revolves around taking a method's source code as a string and executing it with supplied arguments. Look for the main runner class in src/ — everything else is likely value objects for arguments, results, or parsing exceptions.
First use case: dynamic rule/formula evaluation. The natural fit is when you want to store small PHP method bodies somewhere (database, config, user-defined rules) and invoke them at runtime. A minimal flow looks like:
// Conceptual — verify exact class/method names in src/
$runner = app(\LeoVie\PhpMethodRunner\MethodRunner::class);
$result = $runner->run($methodSource, ['order' => $order]);
Bind it once. Register the runner as a singleton in your AppServiceProvider (or a package provider) so you're not instantiating it per call:
$this->app->singleton(\LeoVie\PhpMethodRunner\MethodRunner::class);
Build a thin wrapper immediately. Don't call the package directly from controllers or models. A MethodExecutor service in app/Services gives you one place to add validation, logging, and sandboxing later.
Know what v3.0.1 changed before you debug. The runner now suppresses output produced by the executed method (echo, print, var_dump, etc.). Your only channel out of a method is its return value. Design your snippets around returning data, not printing it, from day one.
Service wrapper + DTOs. Expose a single method like execute(string $source, array $args): mixed and wrap exceptions in your own domain exceptions. This decouples your app from the package's internals (important given the project's maturity).
Return values are the contract. Because v3.0.1 silences method output, every rule/formula must communicate by returning a value. If you need a method to return $x and surface diagnostics, return an array:
// Stored source
'$total = $args["order"]->total(); return ["total" => $total, "tier" => $total > 1000 ? "gold" : "silver"];'
Validate the shape of that array in your wrapper before handing it to the rest of the app.
Rules engine. Pair it with a rules table holding source, name, and version columns; cache the parsed/validated source in Redis and run on demand. This turns the package into a lightweight, self-hosted expression engine without a third-party rules library.
Validation via syntax check. Before executing, run php -l in a subprocess (or token_get_all) against a wrapped snippet:
$wrapped = "<?php class __Validate { {$source} }";
file_put_contents($tmp = tempnam(sys_get_temp_dir(), 'm'), $wrapped);
exec("php -l " . escapeshellarg($tmp), $out, $code);
Reject anything that fails — this prevents parse errors from bubbling into eval failures.
Artisan command for dry runs. A php artisan method:run {file} {--args=} command is invaluable for developers authoring rules locally before they hit production. Since output is suppressed, have your command print the returned value (and the caught exception, if any) rather than expecting var_dump from inside the snippet.
Queue isolation. Run untrusted methods in a queued job with a timeout. Pair with symfony/process to execute in a child PHP process when the code isn't fully trusted — this is the only robust "sandbox" in plain PHP.
Testing pattern. Use Pest/PHPUnit to snapshot the source snippets and assert results. Because the runner shares your process, RefreshDatabase etc. still apply — be careful that executed code doesn't mutate shared state. Assert on the returned value, not on captured stdout: expect($out)->toBe('') is now the baseline, and any assertion that previously relied on echo output will silently fail after upgrading.
Integration tip — form requests. A tiny ValidateMethodSource rule that calls the syntax checker runs at the HTTP boundary and keeps invalid code out of the DB.
echo, print, printf, var_dump, print_r and friends inside the executed method no longer reach your output buffer or response body. If you were using echo as a poor-man's debugger, or if a stored rule emitted markup you relied on, that behaviour is gone — and it will fail silently, not loudly. Rewrite those rules to return a value; for ad-hoc debugging, have the snippet write to a known log file instead.headers already sent, or leak data into a Blade view. If you had defensive ob_start()/ob_get_clean() wrappers around calls to this package, you can probably remove them — but test first, since output buffering also affected other code in the same request.disable_functions, open_basedir, memory/time limits) or a separate container. Treat this as the primary risk of the package.composer.json, read src/, and be prepared to fork. Check PHP compatibility against your Laravel version — older-style packages often break on newer PHP notices.use statements; any class it references must be fully qualified or already available via Composer. This is the #1 source of Class not found errors.$this, private properties, or constructor-injected dependencies won't work unless the runner is given a bound object — check whether it supports instance binding before designing around it.void returns come back as null; by-reference parameters may not be honored; type coercion follows normal PHP semantics, so declare(strict_types=1) in your caller does not apply to the eval'd code.try/catch, log the full source alongside the exception, and map errors back to your own rule IDs.MethodExecutor interface is the extension point you should offer the rest of the codebase.How can I help you explore Laravel packages today?