webman-tech/common-utils
A collection of common utility helpers for the webman PHP framework, providing reusable tools to simplify everyday development tasks. Designed for webman-tech projects, it offers practical shared functions and classes that reduce boilerplate and keep application code cleaner and more consistent.
webman-tech/common-utils is not a feature package — it is a compatibility shim. It gives you one API for the things that differ between Webman, Laravel, and plain CLI: path resolution, the container, the current request/response, sessions, routes, config, env, JSON, and logging. The value only appears once you have code that must run in more than one of those environments (e.g. a library, a shared module, a console command that reuses app code).
Install
composer require webman-tech/common-utils
Note the repo is a read-only subtree split of webman-tech/components-monorepo. Do not open issues or PRs against it; all changes land in the monorepo. Treat the package as vendored, versioned, third-party code — pin a version and read the source when in doubt.
Where to look first (in this order)
src/functions.php — the global helpers (base_path(), runtime_path(), logger(), config()). This is the surface you will actually type 90% of the time.src/Runtime.php + src/RuntimeCustomRegister.php — the detection/registration mechanism everything else hangs off.src/Request.php, src/Response.php, src/Route.php, src/Config.php — the adapters.src/Testing/Factory.php and src/Testing/Test*.php — how you make any of this testable.tests/Unit/CommonUtils/ in the monorepo — the real usage examples.First use case: a console command that reuses app code
In CLI there is no framework bootstrapping, so nothing is auto-detected. You must register a runtime before any helper works:
use WebmanTech\CommonUtils\RuntimeCustomRegister;
RuntimeCustomRegister::register([
'base_path' => '/path/to/project',
'runtime_path' => '/path/to/project/runtime',
]);
// now the helpers resolve
$log = base_path('runtime/logs/app.log');
logger()->info('command started');
From that point on, any code written against Request::getCurrent(), config(), logger(), Response::make() runs unchanged in the command, in Webman, and in Laravel.
1. Write to the abstraction, branch only at the edges
Business/service code should never see a framework class. Keep the runtime check in one place — a bootstrap, a service provider, a Webman Install.php-style entry:
use WebmanTech\CommonUtils\Runtime;
if (Runtime::isWebman()) {
// wire Webman-specific listeners here
} elseif (Runtime::isCli()) {
// CLI-only wiring
}
Everything downstream stays framework-agnostic.
2. Request as an ambient, mutable object
Request::getCurrent() returns a wrapper over the live underlying request. Mutations write through:
$request = Request::getCurrent();
$all = $request->allPostJson();
$userId = $request->getCustomData('userId');
$request->withCustomData('traceId', $traceId); // visible to later code, no plumbing
This is the intended pattern: an auth middleware or trace middleware stamps data onto the request, and controllers read it via getCustomData() — no need to thread a DTO through every signature. It is a deliberate trade-off of explicit dependency-passing for ergonomics.
3. Response as a fluent builder ending in getRaw()
return Response::make()
->withStatus(201)
->withHeaders(['X-Request-Id' => $traceId])
->withBody(Json::encode(['id' => $userId]))
->getRaw();
getRaw() yields the framework-native response object, which is what Webman/Laravel controllers and middleware expect to be returned. Always terminate the chain with it; returning the wrapper itself will not be understood by the framework.
4. Cross-framework middleware via BaseMiddleware
This is the single highest-leverage pattern in the package. One middleware class, usable in both Webman and Laravel:
final class TraceMiddleware extends BaseMiddleware
{
protected function processRequest(Request $request, \Closure $handler): Response
{
$request->withCustomData('traceId', uniqid('trace_', true));
$response = $handler($request);
return $response->withHeaders(['X-Response-Time' => (string) microtime(true)]);
}
}
Because request mutations write through to the real object, you do not re-inject the request into the handler — the contract is $handler($request) returning a Response. Register it through each framework's native middleware config; the class itself is portable.
5. Config with closure defaults
Config::get() accepts a closure as the default, which is how you lazily build objects without eager instantiation:
$client = config('services.foo.client', fn () => new FooClient(config('services.foo.key')));
This is the pattern to use for anything expensive or environment-dependent — a good replacement for singletons in shared libraries. The config() helper routes through Config, so it works in CLI too (once a runtime is registered).
6. JSON: always Json::encode()
use WebmanTech\CommonUtils\Json;
use WebmanTech\CommonUtils\Json\Expression;
$payload = [
'value' => $data, // NaN/INF/non-UTF-8 tolerated
'callback' => new Expression('function () { alert(1); }'), // emitted unquoted
];
$body = Json::encode($payload);
Expression is the escape hatch for inlining JS into a JSON blob for a template. Use it only when you control the string — it bypasses quoting entirely.
7. Env: read once, treat as immutable
use WebmanTech\CommonUtils\EnvAttr;
$debug = EnvAttr::get('APP_DEBUG', false);
EnvAttr auto-loads the env file on first get() and then locks itself read-only. Treat env as configuration frozen at boot — read it at the top of a bootstrap and pass values down, rather than calling EnvAttr::get() deep inside services.
8. Testing: register a fake runtime once
// tests/bootstrap.php
use WebmanTech\CommonUtils\Testing\Factory;
Factory::registerTestRuntime(__DIR__ . '/test-demo');
After that, Request::getCurrent(), Route::getCurrent(), Config, Session, and the container all resolve to Testing\Test* fakes with no change to production code. This is why the abstraction exists — the alternative is mocking framework internals.
Per-test cleanup:
beforeEach(function () {
Request::getCurrent()->clear();
Route::getCurrent()->clear();
});
9. Caches are in-memory only
ArrayCache / NullCache exist to satisfy a cache dependency in tests and short-lived CLI runs. NullCache is the right choice when you want to assert "no caching happened." Neither is a substitute for a real store across requests.
10. Views
View is a plain-PHP template renderer — no Blade/Twig dependency. Use it for mail bodies, CLI output, and small HTML fragments that must render identically in all three runtimes.
UnsupportedRuntime in CLI is the #1 error. Nothing is auto-detected outside a web request. Any script, queue-worker bootstrap, or PHPUnit entry point must call RuntimeCustomRegister::register([...]) before touching a helper. If you see InvalidArgumentException about runtime, this is it — not a bug.
Never type-hint a framework request. public function handle(\Webman\Http\Request $request) makes the class unusable in Laravel and in tests. Type-hint WebmanTech\CommonUtils\Request, or take no argument and use Request::getCurrent().
Never call the raw escape hatch for header reads. $request->getRaw()->header('X-Foo') works in Webman and silently diverges in Laravel. The whole point of the wrapper is uniform semantics — $request->header('X-Foo') is the only correct form. Reserve getRaw() for the final hand-off to the framework.
Request mutation is a side effect. withHeaders() / withCustomData() write through to the live request object. That is convenient, but it also means the same request object handed to two middlewares carries the first one's changes. If you need per-scope data, key it uniquely (traceId, authUser) rather than reusing generic keys.
EnvAttr::set() throws by default. The first get() flips the class into read-only mode. To mutate (tests and bootstrap only):
EnvAttr::changeSupportReadonly(false);
EnvAttr::set('APP_DEBUG', true);
Do not scatter this across a codebase — if you find yourself flipping it in application code, your config is being read too late.
Route::clear() is a test-only API. It wipes the global route table. Calling it from a service provider or a request lifecycle will silently delete your production routes. Guard it behind the test bootstrap and nowhere else.
Test pollution is the second-most-common failure. The Test* fakes are process-global singletons. Without clear() in beforeEach, a request or route from test A leaks into test B, and you get order-dependent failures. Clear both Request and Route every time, even if a test "doesn't touch routing."
json_encode() returning false is a symptom, not a bug. NaN, INF, or invalid UTF-8 (common with GBK/legacy DB data) will fail. Json::encode() handles those cases, and Encoding exists for fixing source strings before they reach the encoder. If you are debugging an empty API response, check for false before checking anything else.
Pin the version and read the source. Zero stars and zero dependents means no community stack traces, no blog posts, and no Stack Overflow answers. The repository is a read-only split, so there is no issue tracker on it either — changes come from webman-tech/components-monorepo. Budget for reading src/ directly; the code is small and the adapters are the definitive documentation.
Extension point is RuntimeCustomRegister, not subclassing. When you need a fourth environment (Swoole standalone, RoadRunner, a bespoke container), register a custom runtime rather than subclassing Request/Response. Adapters are selected by runtime; overriding one in isolation will produce a half-consistent stack.
Middleware contract is Closure(Request): Response. Returning something else (a raw array, a framework response) breaks the abstraction at the boundary and defeats the point of BaseMiddleware. If you need to short-circuit, build the response with Response::make()->...->getRaw() inside the middleware and return the wrapper's downstream result unmodified.
Keep functions.php helpers at the edges. base_path(), runtime_path(), logger(), config() are convenient, but they are global state. In library code, accept a path or a LoggerInterface as a constructor argument and default to the helper — that keeps unit tests trivial even without Factory::registerTestRuntime().
How can I help you explore Laravel packages today?