The application skeleton for the strukt4 package set. It ships almost
nothing of its own: a composer.json that pulls instrukt/framework and a post-create-project-cmd hook that
scaffolds a runnable application through the framework console. Everything
under app/, cfg/, templates/, lib/, plus index.php and .env, is
generated and git-ignored.
composer create-project strukt/strukt --prefer-dist
The install hook prompts for an application name, then runs two commands for
you:
vendor/bin/strukt app:make # prompts for the name; scaffolds app/src/<Name>/..., cfg/*.ini and templates/
vendor/bin/strukt app:reload # lib/App/Loader.php, index.php, .env
The result is a ready-to-run project:
.
├── app/
│ └── src/
│ └── {{app}}/
│ ├── User.php
│ └── AuthModule/
│ ├── {{app}}AuthModule.php # the module (alias "au")
│ ├── Controller/User.php # scaffolded, see "Controller facet"
│ ├── Form/User.php
│ ├── Router/Index.php
│ └── Tests/UserTest.php
├── cfg/ # generated application config
│ └── app.ini
├── templates/ # editable scaffolding templates
├── lib/
│ └── App/
│ └── Loader.php # generated, do not edit
├── index.php # generated front controller
├── .env # generated environment
├── composer.json
└── vendor/
vendor/bin/strukt app:exec
# Serving on http://127.0.0.1:8080
Visit http://127.0.0.1:8080/ and http://127.0.0.1:8080/hello/ada — the
generated AuthModule ships with a welcome and a hello route.
Controller/User.php is scaffolded as a stub and is not routed by the generated
app — routes only come from Router/ classes. It exists to illustrate the
controller facet, whose actions other facets reach through the module alias:
// inside a router
return $this->get('au.ctr.User')->find($id);
The registrar indexes it as nr.au.ctr.User, and core('au.ctr.User') (orCore::get()) resolves it through the facet provider. Real action classes can
either live here and be called this way, or sit directly in a router — the RBAC
routes in this skeleton take the latter approach for brevity.
The skeleton resolves strukt/framework and its siblings through Composerpath repositories (../framework, ../base, …), so it installs against the
working copies next to it:
composer install
vendor/bin/strukt app:make {{app}}
vendor/bin/strukt app:reload
bin/clear resets the project back to its seed state by deleting every
generated artifact.
The generated lib/App/Provider/Session.php binds the router's native$_SESSION store — all a single-server app needs:
use Strukt\Contract\Http\SessionInterface;
use Strukt\Http\Session\Native;
router()->session(static fn (): SessionInterface => new Native());
router()->middleware('sess');
To share sessions across processes or servers, swap in the router's
cache-backed store, which writes through to a strukt/cache driver so any
worker can resume the session:
use Strukt\Http\Session\Cache;
router()->session(static fn (): SessionInterface => new Cache(memc('sess')));
memc() is the cache package's memcached helper. Give each client its own
namespace (memc('sess_' . $sid)) to keep sessions isolated. See the router's
HTTP layer and
authentication docs for the full set of
stores.
| Command | Purpose |
|---|---|
app:make [<name>] |
Scaffold an application (app/src/<Name>, cfg/app.ini). Prompts for the name when omitted. |
app:reload |
Regenerate lib/App/Loader.php, index.php, .env. |
app:exec |
Serve the application with the PHP built-in server. |
module:make <app> <module> <alias> |
Add a module to an application. |
route:make [<module>] [<name>] [<route>] |
Scaffold or extend a router. |
route:ls |
List registered routes. |
shell:exec |
Open the interactive console. |
The framework documentation lives in ../framework/docs:
getting started,
routing,
forms and validation,
console,
configuration, and
extensions.
MIT. See LICENSE.
Annotation-driven application kernel, facades, and console for the strukt4
bundle set. This is the package that unifies the Strukt components under one
application lifecycle.
Most applications use it through the strukt/strukt skeleton.
composer require strukt/framework
Requires PHP 8.2+ and a strukt4 powered project using base, commons, contract,
router, cache, event, form, rules, fs, generator, and console.
use Strukt\Framework\Application;
$app = Application::make();
$app->register(new AuthModule());
$app->run();
handle() returns a ResponseInterface; run() sends it.
The framework publishes vendor/bin/strukt and a development-onlyvendor/bin/xhttp binary.
vendor/bin/strukt list
vendor/bin/strukt help module:make
vendor/bin/strukt cache:clear
vendor/bin/strukt app:make payroll
vendor/bin/strukt app:reload
vendor/bin/strukt app:exec
# http://127.0.0.1:8080
app:make scaffolds app/src/<Name> and the configuration. app:reload
generates lib/App/Loader.php, index.php, and .env without overwriting
existing files. app:exec runs the PHP built-in server with index.php as the
router script.
Application namespaces resolve through Strukt\Framework\Autoload, so no
Composer autoload edits or composer dump-autoload are required.
A module groups application facets beneath one namespace.
use Strukt\Framework\Contract\Module;
final class AuthModule extends Module
{
protected string $alias = 'auth';
}
Facets are indexed in the nr registry as nr.<alias>.<facet>.<name> and can
be resolved through core():
core('auth.ctr.Login', ['request' => request()]);
Routers carry method annotations that Application turns into routes on the
Strukt router.
use Strukt\Framework\Contract\Router;
final class Login extends Router
{
/**
* @Route(/auth/login)
* @Method(POST)
* @Form(Authentication)
* @Middleware(sess)
*/
public function login(\Strukt\Http\Request $request)
{
//
}
}
Forms validate through strukt/form and strukt/rules; a failed form
submission produces a 422 response with field errors.
The framework ships a small PSR-11 container, backed by strukt/base.
use Strukt\Container\Container;
use Strukt\Framework\Contract\Kernel;
$kernel = container()->get(Kernel::class);
$auth = container()->make(\App\Auth\Login::class);
container()->set(
\App\Mailer::class,
fn (Container $c) => new \App\Mailer($c->get(\App\Transport::class)),
);
Framework collaborators resolve through the container:
| Identifier | Resolves to |
|---|---|
Kernel |
RouterAdapter over the shared router. |
Registrar |
Module discovery, backed by ModuleStore. |
Configuration |
Cached bundle settings. |
ModuleStore |
The shared reg() registry. |
BundlesRepositoryInterface |
The static Strukt\Bundle\Bundles. |
Console commands are created through ContainerCommandFactory, so commands may
declare constructor dependencies.
Framework cfg/*.ini sections merge into the shared config registry, only when
the section is not already set:
| Section | Purpose |
|---|---|
cmd |
Console command groups. |
module |
Module folders (folders.ctr, folders.rtr, ...). |
cache |
Cache flags such as disable. |
repo |
Installed bundle repositories (core). |
Environment values used by the kernel:
| Key | Default |
|---|---|
root_dir |
Project root. |
rel_appsrc |
app/src |
rel_app_lib |
lib/App |
rel_mod_ini |
cfg/module.ini |
rel_loader_sgf |
Built-in loader template override. |
serve_host |
127.0.0.1 |
serve_port |
8080 |
serve_ctx |
. |
is_dev |
true |
The framework uses bundles, not packages. Bundle metadata is available
through bundle() and repos().
repos(); // available bundles
repos('installed'); // installed bundle names
repos('published'); // published bundle names
bundle('core')->get('settings');
bundle('core', 'App:Idx')->get('settings');
bundle('core')->get('requirements');
The bundle contract lives under Strukt\Framework\Contract\Bundle.
| Command | Description |
|---|---|
app:make |
Generate application scaffolding. |
app:reload |
Generate the application module loader. |
app:exec |
Run the application on the PHP built-in server. |
cache:clear |
Clear the cache. |
cache:make |
Rebuild the application configuration cache. |
cache:reset |
Reset the configuration cache. |
middleware:make |
Create middleware scaffolding. |
provider:make |
Create provider scaffolding. |
validator:make |
Create validator scaffolding. |
module:make |
Generate an application module. |
route:make |
Generate or extend a module router. |
route:ls |
List registered routes. |
shell:exec |
Start an interactive PsySH session. |
sys:ls |
List providers and middlewares. |
Bundle publisher commands:
| Command | Description |
|---|---|
bundle:make |
Create a bundle scaffold. |
bundle:add |
Add files or folders to bundle.json. |
bundle:export |
Export a bundle. |
bundle:ls |
List bundles and their status. |
bundle:info |
Show information on a bundle. |
| Helper | Purpose |
|---|---|
app() |
Resolve from the shared container. |
container() |
Return or install the shared container. |
core() |
Resolve a module facet by alias. |
bundle() |
Read bundle metadata. |
repos() |
List configured bundles. |
config() |
Read or write configuration values. |
router(), request(), response() |
HTTP helpers from strukt/router. |
cache() |
Cache facade from strukt/cache. |
event(), cmd() |
Event helpers from strukt/event. |
The old package-oriented API (package(), package:*) has been replaced by
bundles (bundle(), bundle:*). repos() keeps its name for compatibility and
now lists bundles.
A typed HTTP router with parameter matching, a middleware pipeline, reflection
resolved actions, and form validation.
Routes are declared against patterns such as /user/{id:int}, matched by a
compiled regex with typed parameter extraction, guarded by middleware, and
dispatched by injecting the request, response, services, or a validated form.
composer require strukt/router
Requires PHP 8.2+ and the strukt4 contract, form, cache, and event packages as
needed.
use Strukt\Router\Router;
use Strukt\Contract\Http\RequestInterface;
$router = new Router();
$router->get('/', fn () => 'home');
$router->get('/user/{id:int}', fn (int $id) => "user[{$id}]");
$router->post('/login', fn (
RequestInterface $request,
Strukt\Contract\FormInterface $form,
) => $form->value('email'))->form(LoginForm::class);
$response = $router->kernel()->handle($request);
$response->send();
An application normally uses the shared instance instead:
router()->get('/', fn () => 'home');
Use one verb method per route, or any() for every common method.
$router->get('/list', fn () => 'list');
$router->post('/save', fn () => 'saved');
$router->put('/item/{id:int}', fn (int $id) => "put {$id}");
$router->patch('/item/{id:int}', fn (int $id) => "patch {$id}");
$router->delete('/item/{id:int}', fn (int $id) => 'deleted');
$router->options('/item', fn () => 'options');
$router->head('/item', fn () => 'head');
$router->any('/hook', fn () => 'any method');
$router->group('/api/v1', function (Router $router): void {
$router->get('/users', fn () => 'users');
$router->get('/users/{id:int}', fn (int $id) => "user[{$id}]");
});
$router->get('/user/{id:int}', fn (int $id) => "user[{$id}]")
->name('user.show');
$router->path('user.show', ['id' => 7]); // /user/7
Route options mutate the registered route fluently: name(), middleware(),permissions(), requiresAuth(), form(), and defaults().
| Pattern | Matches | Type |
|---|---|---|
{name} |
any segment | string |
{id:int} |
digits | int |
{price:float} |
decimal | float |
{on:bool} |
true / false |
bool |
{word:alpha} |
letters | string |
{key:alphanum} |
letters, digits, _, - |
string |
{slug} |
slug words | string |
{on:date} |
YYYY-MM-DD |
string |
{id:uuid} |
UUID | string |
{rest:*} |
remaining segments | string |
$router->get('/user/{id:int}', fn (int $id) => "user[{$id}]");
$router->get('/blog/{year:int}/{?month:int}', fn (int $year, ?int $month = null) => '');
$router->get('/files/{rest:*}', fn (string $rest) => $rest);
Matched values are cast. An unknown type or missing required placeholder throws.
Reverse generation through uri() or Router::path() respects the same rules
and route defaults.
Global middleware runs before every route; route middleware runs after that.
Definitions may be a registered name, class, object, or callable.
$router->middleware(TraceMiddleware::class);
$router->get('/admin', fn () => 'admin')->middleware('auth', 'authz');
Built-in names:
| Name | Class | Purpose |
|---|---|---|
sess |
Middleware\Session |
Attach the session store to the request. |
auth |
Middleware\Authentication |
Resolve and bind the current user. |
authz |
Middleware\Authorization |
Enforce required permissions. |
Middleware resolves through constructor injection. A middleware class declares
the services it needs, and the router supplies them from the per-request service
map.
use Strukt\Contract\Http\SessionInterface;
use Strukt\Contract\UserInterface;
$router->session(fn (): SessionInterface => new Strukt\Http\Session\Native());
$router->verify(fn (SessionInterface $session): ?UserInterface =>
$session->get('email')
? new Strukt\User(email: $session->get('email'))
: null
);
$router->permissions(fn (?UserInterface $user): array =>
$user?->getEmail() === 'admin@example.com' ? ['admin'] : []
);
$router->get('/dashboard', fn (RequestInterface $request) =>
$request->user()?->getEmail() ?? 'guest'
)->requiresAuth();
$router->get('/admin', fn () => 'admin')
->requiresAuth()
->permissions(['admin']);
A missing user on an authenticated route stops with 401. A user without the
required permission receives 403.
Attach a strukt/form class with form(). The router submits the request values
and validates before the action runs. A failing validation answers with 422 and
the errors as JSON.
$router->post('/login', fn (
Strukt\Contract\FormInterface $form,
) => 'hi '.$form->value('email'))->form(LoginForm::class);
An action may return a string (body), an array (JSON), a ResponseInterface, ornull.
$router->get('/home', fn () => response()->redirect('/home'));
$router->get('/data', fn () => response()->json(['ok' => true]));
$router->get('/file', fn () => download('/tmp/report.pdf', 'report.pdf'));
The kernel maps exceptions to HTTP errors: 404 for an unknown path, 405 with anAllow header for a bad method, 422 for failed form validation, and 401/403 for
the auth gates.
| Helper | Purpose |
|---|---|
router() |
The shared Router instance. |
request($input, $method, $uri, $headers) |
Build a Request. |
response($code, $headers) |
A fluent response builder. |
route($path, $method, $input) |
Dispatch and return the response. |
matcher($patterns) |
A standalone UrlMatcher. |
redirect($url, $status = 302) |
A redirect response. |
download($path, $filename) |
A file download. |
The package ships its own Strukt\Http stack implementing the strukt/contract
interfaces: Request with createFromGlobals(), Response, ResponseBuilder,Download, and UploadedFile.
Session stores live under Strukt\Http\Session:
$router->session(fn (): SessionInterface => new Strukt\Http\Session\Native());
$router->session(fn (): SessionInterface => new Strukt\Http\Session\Cache(memc('sess')));
Native persists in $_SESSION. Cache persists through a strukt/cache
driver, so several processes or servers can share a session.
In a Strukt application, module route prefixes are resolved in this order:
public string $prefix = '/api/v1' orpublic function prefix(): string.Prefix.php class: prefix() method, $prefix property, orgetPrefix().@Prefix('/api/v1').use Strukt\Framework\Contract\Module;
class PayrollAuthModule implements Module
{
public string $prefix = '/api/v1';
}
Prefix joining follows Router::group() semantics, with the trailing slash
preserved for the root.
Chainable arrays and strings, dot-separated collections, date ranges, token
queries, registries, message stacks, JSON helpers, and common global functions.
Requires PHP 8.2+ and strukt/base. Optional integrations such as Ramsey UUID
and Symfony's English inflector are used when installed; standalone fallbacks
remain available.
composer require strukt/commons
Composer loads the Strukt\ namespace and the global helpers. Require the
normal autoloader before using the helpers.
collect() creates a mutable collection with dot-path reads and writes.
$contact = collect([]);
$contact->set('mobile', '+2540770123456');
$contact->set('work-phone', '+2540202345678');
$user = collect([]);
$user->set('contacts', $contact->yield());
echo $user->get('contacts.mobile');
$user->exists('contacts.mobile'); // true
$user->remove('contacts.work-phone');
$user->keys('contacts'); // ['mobile']
The dot(), attach(), and detach() helpers work directly on arrays.
$data = [
'user' => [
'firstname' => 'Gene',
'db' => [
'config' => ['username' => 'root'],
],
],
];
echo dot('user.db.config.username', $data); // root
attach('user.db.config.host', $data, 'localhost');
detach('user.db.config.username', $data);
Value objects expose their raw value through yield(). Transformations return a
new wrapper so chains do not mutate the original.
$start = when();
$end = when('+30 days');
$random = $start->rand($end);
$end->gt($start); // true
$start->btwn($start, $end);
$start->gte($end);
$start->lte($end);
$start->equals($end);
$start->same(new DateTime());
$start->clone('+1 day');
$start->reset(); // 00:00:00
$start->last(); // 23:59:59
echo $start;
same() compares calendar dates; equals() compares the complete instant.
period() controls an inclusive date range and can simulate the current day.
$period = period();
$period->create(when('1900-01-01'), when('1963-12-31'));
$period->reset(when('1960-03-23'));
$fakeToday = today();
echo $fakeToday->format('Y-m-d'); // 1960-03-23
$fakeToday->hasPeriod(); // true
\Strukt\Today::withDate(when('1959-01-01'))->isValid();
$period->reset(); // restore the real today
$value = str('Strukt Commons');
echo $value->toSnake(); // strukt_commons
echo $value->toCamel(); // StruktCommons
$value->startsWith('Strukt'); // true
$value->endsWith('Commons'); // true
$value->contains('Commons'); // true
$value->empty(); // false
$value->replace('Commons', 'Library');
$value->replaceFirst('S', 'X');
$value->replaceLast('s', 'z');
str('[commons]')->btwn('[', ']'); // commons
str('log')->pad('.')->right(3); // log...
str('admin@example.com')->is()->email(); // true
Other operations include prepend(), concat(), len(), count(),split(), slice(), first(), last(), part(), repeat(), toUpper(),toLower(), replaceAt(), and newline().
arr() returns a chainable wrapper implementing Countable andIteratorAggregate.
$numbers = arr([1, 2, 2, 3]);
$numbers->sum(); // 8
$numbers->product(); // 12
$numbers->uniq()->values()->yield(); // [1, 2, 3]
$numbers->distinct()->yield(); // [1 => 1, 2 => 2, 3 => 1]
Compose transformations:
$rows = arr([
['name' => 'Ada', 'score' => 92],
['name' => 'Grace', 'score' => 88],
]);
$names = $rows->column('name')->yield();
$ordered = $rows->order()->desc('score')->yield();
$flat = arr(['user' => ['name' => 'Ada']])->level()->yield();
$upper = arr(['ada', 'grace'])
->map(fn (int|string $key, string $value): string => strtoupper($value))
->yield();
Predicates and selectors:
arr(['ada', 'grace'])->isof()->strings(); // true
arr([1, 1])->are()->all(1); // true
arr(['name' => 'Ada'])->is()->map(); // true
arr([1, 2, 3])->only([1, 3])->yield(); // [1, 3]
The wrapper also provides push(), enqueue(), prequeue(), remove(),merge(), enjoin(), slice(), reverse(), flip(), rehash(), filter(),each(), diff(), cross(), join(), contains(), has(), skip(),jump(), stopAt(), and pointer helpers.
Nested arrays can be flattened with optional key prefixes:
arr([
'user' => [
'name' => 'Ada',
'roles' => ['admin', 'author'],
],
])->level()->prefix('payload')->yield();
// ['payload.user.name' => 'Ada', 'payload.user.roles.0' => 'admin', ...]
$query = token('page:2|tag:php,composer');
$query->has('page'); // true
$query->get('tag'); // ['php', 'composer']
$query->set('sort', 'name')->remove('page');
echo $query->yield(); // tag:php,composer|sort:name
echo tokenize(['page' => 2, 'tag' => ['php', 'composer']]);
stack() records messages in a bounded process-local stack. The default limit
is ten messages.
\Strukt\Stack::clear();
$message = stack('User was not found');
$message->add('A retry was scheduled');
$messages = \Strukt\Stack::get();
$messages->yield();
$filtered = \Strukt\Stack::get('/retry/');
$filtered->yield();
\Strukt\Stack::limit(20);
$payload = json(['name' => 'Peter', 'active' => true]);
echo $payload->pp(); // pretty JSON
$encoded = $payload->encode();
$decoded = json($encoded)->decode();
json($encoded)->valid(); // true
Encoding and decoding use JSON_THROW_ON_ERROR, so malformed data raisesJsonException.
reg('user.name', 'Ada');
echo reg('user.name');
echo config('app.name');
echo format('date', when('2024-01-01'));
echo plural('story'); // stories
echo singular('stories'); // story
echo uuid();
provider('provider.core');
Low-level alias(), is_map(), notnull(), negate(), and raise() come
from strukt/base. form() and form_check() come from strukt/form;number(), matrix(), and pipeline() come from strukt/math.
Documentation-first PHP CLI commands. A structured PHPDoc block on a command
class becomes routing, argument and option parsing, validation, --help output,
command listing, and generated Markdown documentation.
composer require strukt/console
Create a console.php in the consuming project:
<?php
return [
'commands' => __DIR__.'/src/Command',
'docs' => __DIR__.'/docs/commands',
];
Implement Strukt\Console\Runtime\CommandInterface and describe the command
with the @command DSL.
<?php
declare(strict_types=1);
namespace App\Command;
use Strukt\Console\Runtime\CommandInterface;
/**
* @command user:create
* @description Creates a new application user.
* @usage user:create <email> [options]
*
* @argument email
* @type string
* @required
* @description The user's email address.
*
* @option name
* @type string
* @default Anonymous
* @description User's display name.
*
* @option admin
* @flag
* @description Create the user as an administrator.
*
* @option password
* @type string
* @required
* @description Initial password.
*/
final class CreateUser implements CommandInterface
{
public function execute(array $arguments, array $options): int
{
$email = $arguments['email'];
$name = $options['name'] ?? 'Anonymous';
$isAdmin = $options['admin'] ?? false;
return 0;
}
}
Run it:
vendor/bin/console user:create john@example.com --name="John Doe" --admin --password=secret
| Tag | Purpose |
|---|---|
@command |
Required public command name. |
@description |
Short command description. |
@usage |
Optional explicit usage line. |
@argument name |
Declare a positional argument. |
@option name |
Declare a value option or flag. |
@type |
Argument/option type: string, int, integer, float, bool, boolean. |
@required |
The argument or option must be supplied. |
@default |
Default value. |
@enum |
Comma-separated allowed values. |
@flag |
Declare a boolean switch. |
@short |
Single-character option alias. |
Use one tag per line and plain spaces. The DSL is explicit: arbitrary prose is
not guessed or executed.
interface CommandInterface
{
public function execute(array $arguments, array $options): int;
}
Return 0 for success. A non-zero value becomes the process exit status.
vendor/bin/console
vendor/bin/console list
vendor/bin/console user:create --help
vendor/bin/console docs
docs writes Markdown generated from the same command definitions. The command
block is the single source for CLI behaviour, help, validation, and generated
documentation.
Fast local filesystem, rooted path, and ZIP archive helpers. The package
provides a static Fs API, a rooted Rfs facade, and procedural helpers for
common operations.
composer require strukt/fs
Requires PHP 8.2+ and strukt/base; ZIP operations require ext-zip.
require __DIR__.'/vendor/autoload.php';
$files = fs(__DIR__);
$files->touchWrite('storage/app.log', "ready\n");
echo $files->cat('storage/app.log');
$entries = $files->lsr('storage');
$recent = $files->tail('storage/app.log', 20);
| Helper | Purpose |
|---|---|
fs() |
Return an Fs instance, or a rooted Rfs when a directory is supplied. |
tail() |
Read the final lines of a file. |
ds() |
Normalize slashes to the current OS separator. |
path_exists() |
Check for an existing file or directory. |
phar() |
Detect and adapt paths inside a Phar archive. |
local() |
Convert an existing path to a file:// URI. |
$absolute = fs();
$project = fs(__DIR__);
$project->touchWrite('var/cache/data.txt', 'cached');
tail('README.md');
ds('src/Strukt/Contract');
path_exists('src/Strukt/Contract');
phar()->active();
phar('src/Strukt/Contract')->adapt();
local('README.md'); // file:///...
Strukt\Fs\Fs exposes the complete local filesystem API through static
methods.
use Strukt\Fs\Fs;
Fs::isDir($dir);
Fs::isFile($file);
Fs::isPath($path);
Fs::cat($file);
Fs::touch($file);
Fs::touchWrite($file, $contents);
Fs::rename($from, $to);
Fs::overwrite($file, $contents);
Fs::appendWrite($file, $contents);
touch() creates only a new file. touchWrite() creates missing parent
directories, creates a new file, and writes all contents; it returns false if
the file already exists. overwrite() and appendWrite() require an existing
file and return the number of bytes written. rename() backs up an existing
destination before replacing it.
Fs::rm($file);
Fs::rmdir($dir);
Fs::mkdir($dir, 0755, true);
Fs::isWritable($file);
Fs::isReadable($file);
Fs::copyRecur($source, $dest);
Fs::cpr($source, $dest);
Fs::listFiles($path);
Fs::ls($path);
Fs::listFilesRecur($path);
Fs::lsr($path);
rmdir() removes a complete directory tree, including hidden entries.mkdir() is idempotent. Recursive listings include hidden files and return paths
for every regular file below the directory.
Fs::isWindows();
Fs::dirSep($path);
Fs::ds($path);
Fs::tail($filepath, 20);
Fs::zip($path, $zipfile = null);
Fs::addZipList($zipfile, $files);
Fs::unzip($zipfile, $topath = './');
Fs::lsz($zippath);
zip() archives every file below a directory; when no zip file is supplied it
creates a timestamped archive in the current working directory. unzip()
rejects absolute and parent-traversal entries before extracting intotopath/archive-name.
Fs::ini($path, sections: true);
Fs::req($path);
ini() parses an INI file with PHP's typed scanner. req() includes a PHP file
and returns its included value.
Strukt\Fs\Rfs fixes a root once and interprets every relative path below it.
use Strukt\Fs\Rfs;
$root = new Rfs(__DIR__);
$root->path('storage/app.log'); // absolute path below the root
$root->touchWrite('storage/app.log', "ready\n");
$root->cat('storage/app.log');
$root->lsr('storage');
$root->zip('storage');
Rfs exposes the same isDir(), isFile(), isPath(), cat(), touch(),touchWrite(), rename(), overwrite(), appendWrite(), rm(), rmdir(),mkdir(), isWritable(), isReadable(), copyRecur(), cpr(),listFiles(), ls(), listFilesRecur(), lsr(), tail(), zip(),addZipList(), unzip(), lsz(), ini(), and req() methods.
Rfs is a path convenience facade, not a security sandbox. Validate untrusted path
input in the application.
use Strukt\Phar;
Phar::adapt('src/Strukt/Contract');
Phar::isPhar();
adapt() resolves a path against the active archive when available, otherwise
against the current working directory. isPhar() reports whether the current
code is running from a Phar archive.
tail() reads only the required suffix in chunks.Typed cache storage with a process-local memory driver, JSON filesystem
persistence, and an optional Memcached backend behind one fluent API.
composer require strukt/cache
Requires PHP 8.2+. The package uses strukt/base for helpers and environment
values, strukt/commons for aliases and configuration, and strukt/fs for
filesystem operations.
The cache() helper opens an in-memory namespace by default.
cache('settings.theme', 'dark');
echo cache('settings.theme'); // dark
Use the fluent facade for explicit control:
use Strukt\Cache\Cache;
use Strukt\Cache\Driver\Memory;
$cache = Cache::make('settings', Memory::class);
$cache->put('theme', 'dark');
echo $cache->get('theme');
Persistent caches must be saved:
use Strukt\Cache\Cache;
use Strukt\Cache\Driver\Fs;
$cache = new Cache('settings', Fs::class);
$cache->put('theme', 'dark')->save();
| Driver | Class | Storage |
|---|---|---|
| Memory | Strukt\Cache\Driver\Memory |
Current PHP process. |
| Filesystem | Strukt\Cache\Driver\Fs |
One JSON file per namespace. |
| Memcached | Strukt\Cache\Driver\MemCached |
A Memcached server. |
All drivers implement Strukt\Contract\CacheDriverInterface.
Short driver names are mem/memory, fs, and memc/memcached.
cache(string $filename, mixed $value = null, string $driver = Memory::class): mixed
With no dot in the name, cache() returns a Cache facade:
$cache = cache('settings');
$cache->put('theme', 'dark');
With a dot, the first segment is the namespace and the remainder is a dotted
key:
cache('settings.theme', 'dark', 'fs');
$theme = cache('settings.theme', null, 'fs');
A null value means read. Use the facade when null must be stored
explicitly. memc('sessions') opens a Memcached namespace.
use Strukt\Cache\Cache;
$cache = new Cache('settings'); // defaults to the filesystem driver
$cache = Cache::make('settings', 'mem'); // late-static constructor
$cache->put('user.name', 'Ada');
$cache->get('user.name');
$cache->exists('user.name');
$cache->remove('user.name');
$cache->empty();
$cache->driver(); // CacheDriverInterface
$cache->save();
Keys must have non-empty segments. Empty keys, leading/trailing dots, and
consecutive dots throw InvalidArgumentException. A stored null still counts
as existing.
Memory values are shared by namespace name within the current process.
use Strukt\Cache\Driver\Memory;
$first = new Memory('runtime');
$first->put('user.id', 42);
$second = new Memory('runtime');
echo $second->get('user.id'); // 42
Memory::reset(); // clear every process-local namespace
save() is a no-op for memory.
The filesystem driver stores a complete namespace as pretty JSON and writes
atomically through a temporary file.
use Strukt\Cache\Driver\Fs;
$cache = new Fs('settings');
$cache->put('theme', 'dark')->save();
$cache = new Fs('settings', sys_get_temp_dir().'/my-cache');
$cache->path(); // the JSON file path
The default directory is the application's .cache folder. Names cannot contain
directory separators. Invalid JSON is rejected when the driver opens the file;
encoding and write failures surface as exceptions.
use Strukt\Cache\Driver\MemCached;
$cache = new MemCached('settings', [
'host' => '127.0.0.1',
'port' => 11211,
'ttl' => 3600,
]);
$cache->put('theme', 'dark')->save();
$cache->isSuccess();
Options may instead come from the environment keys memcached_host,memcached_port, memcached_ttl, memcached_username, andmemcached_password. The PHP memcached extension is only required when this
driver is instantiated.
config('cache.default', 'fs'); // short name or driver class
config('cache.namespace', 'app'); // default namespace
config('cache.disable', true); // force memory storage
Custom drivers can be bound on the shared container:
app()->set('cache.redis', fn () => RedisDriver::class);
$cache = new Cache('sessions', 'redis');
The default service is Memory with namespace default.
exists(), get(), put(), and remove().save() for filesystem or Memcached storage.Memory changes are visible immediately; filesystem and Memcached changes remain
local to the driver until save().
Typed runtime foundations for the Strukt package set: process-local aliases and
environment values, cached reflection wrappers, conditional helper registration,
a PSR-11 container, and Symfony-backed debugging helpers.
This is the lowest-level Strukt package. Most applications get it through the
framework, but it can be used on its own.
composer require strukt/base
Requires PHP 8.2+, psr/container, and symfony/var-dumper. Composer loadssrc/helpers.php; the helpers are available as soon as the normal autoloader is
required.
require __DIR__.'/vendor/autoload.php';
alias('user', App\User::class);
env('APP_ENV', 'production');
$user = ref(App\User::class)->makeArgs(['Ada'])->getInstance();
| Helper | Purpose |
|---|---|
helper() |
Select a helper package or inspect the helper registry. |
helper_add() |
Register a global helper name for the selected package. |
is_map() |
True for a non-empty associative array. |
alias() / env() |
Read and write process-local aliases and typed values. |
ref() |
Create a cached class, object, method, property, or function reference. |
app() |
Resolve from the shared container, or return the container. |
raise() |
Throw Strukt\Raise and record the error. |
timezone(), negate(), notnull() |
Small runtime helpers. |
dd() / dump() |
Symfony VarDumper output; dd() exits. |
Aliases map short names to classes, commands, or events.
alias('user', App\User::class); // App\User::class
alias('user'); // App\User::class
alias(); // all alias => target pairs
alias('user*'); // aliases matching a prefix
Alias::exists('user'); // true
Alias::ls('user'); // matching alias names
Alias::reset(); // clear the process-local store
Registering an existing alias throws LogicException; empty names throwInvalidArgumentException.
env() stores typed values in memory. It does not modify PHP's process
environment.
env('APP_ENV', 'production');
env('DEBUG', false);
echo env('APP_ENV'); // production
Load a dotenv-style file with Env::withFile():
use Strukt\Env;
Env::withFile(); // .env
Env::withFile('.env-dev');
Env::has('APP_ENV');
Env::get('APP_ENV');
Env::set('APP_ENV', 'local');
Env::reset();
Blank lines, # and // comments, an optional export prefix, quoted values,
and values containing = are supported. Missing keys throw Strukt\Raise.
ref() caches ReflectionClass instances for the lifetime of the process.
$r = ref(App\User::class);
$r->ref(); // ReflectionClass
$r->makeArgs(['Ada']); // construct with an array of arguments
$r->make('Ada'); // construct with variadic arguments
$r->noMake(); // skip the constructor
$r->prop('name')->get();
$r->prop('name')->set('Ada');
$r->method('getName')->invoke();
ref('array_sum')->invoke([1, 2, 3]); // 6
Construction may also come from an existing instance withref($object) or Ref::createFrom($object). Use Ref::resetCache() in
long-running workers when classes are reloaded.
Helpers are conditionally declared so packages can share one process.
helper(); // package names
helper('base'); // helper names registered by base
helper_add('custom_helper'); // true when the name can be defined
Helper::create('my-package')->register('my_helper');
Helper::reset();
The shared PSR-11 container is exposed through app(). It supports shared
bindings, per-resolve factories, prebuilt instances, aliases, autowiring, and
named constructor overrides.
use Strukt\Container\Container;
$container = app(); // or Container::getInstance()
$container->set(Mailer::class, fn (Container $c) => new Mailer($c->get(Transport::class)));
$container->factory(Report::class, fn () => new Report());
$container->instance(Config::class, $config);
$container->alias('mailer', Mailer::class);
app(Mailer::class); // shared binding
app(User::class, ['id' => 7]); // autowired with override
$container->make(User::class); // autowire directly
Missing identifiers throw Strukt\Container\NotFoundException; failed
construction throws Strukt\Container\ContainerException. Both implement the
PSR-11 exception interfaces.
Raise is the runtime exception used by the helpers and records each message.
raise('Something went wrong', 500);
Raise::errors();
Raise::reset();
dd($value); // dump and exit
dump($value);
A small query builder and active record ORM for SQL databases. It sits directly
on PDO, parameterizes every statement, and keeps the moving parts small.
Strukt\Ar covers both data access and active record. Managed entities with an
identity map and unit of work live in strukt/orm.
composer require strukt/ar
Requires PHP 8.2+ and a PDO driver for your database.
Strukt\Ar\Connection wraps a PDO instance. Open one through a named constructor
and register it with the manager.
use Strukt\Ar\Connection;
Connection::sqlite(':memory:')->asDefault();
Connection::sqlite('app.db')->as('archive');
db(); // default connection
db('archive'); // named connection
mysql() and pgsql() accept host, credentials, and connection options.pdo() wraps an existing client.
db()->transaction(function (Connection $connection): void {
$connection->execute(
'UPDATE accounts SET balance = balance - ? WHERE id = ?',
[25, 7],
);
$connection->execute(
'UPDATE accounts SET balance = balance + ? WHERE id = ?',
[25, 9],
);
});
transaction() commits on success and rolls back when the callback throws.
Nested calls share the outer transaction.
DB_CONNECTION=sqlite
DB_DATABASE=app.db
Connection::fromEnv('.env', '.env.local');
Files are read in order, so later files override earlier ones. Keys are read withenv(), then the process environment, then defaults: DB_CONNECTION (sqlite),DB_DATABASE (database.sqlite), DB_HOST (127.0.0.1), DB_USERNAME
(root), DB_PASSWORD (empty), and DB_PORT (3306 for MySQL, 5432 for
PostgreSQL).
table() starts a fluent, parameterized query against one table.
db()->table('users')
->where('active', 1)
->where('age', '>', 21)
->orderBy('name')
->limit(10)
->get();
where() accepts equality shorthand, an explicit operator, or a map of
equalities. Other clauses include orWhere(), whereIn(), whereNotIn(),whereNull(), whereNotNull(), whereBetween(), join(), and leftJoin().
db()->table('posts')
->join('users', 'users.id', 'posts.user_id')
->select(['users.name', 'posts.title'])
->groupBy('users.name')
->get();
Read rows, values, or aggregates:
db()->table('users')->where('active', 1)->count();
db()->table('users')->sum('balance');
db()->table('users')->pluck('name', 'id');
db()->table('users')->where('id', 7)->value('name');
db()->table('users')->where('email', $email)->exists();
insertGetId() returns the inserted id. delete() refuses to run without awhere() clause; update(), increment(), and decrement() touch only
matching rows.
Clauses accumulate on the instance, so clone a base query for variations:
$base = db()->table('users')->where('active', 1);
$admins = (clone $base)->where('admin', 1)->get();
$staff = (clone $base)->where('admin', 0)->get();
toSql() and bindings() expose the compiled statement for debugging.
Extend Strukt\Ar\Model. One instance represents one row.
use Strukt\Ar\Model;
class User extends Model
{
protected static array $fillable = ['name', 'email', 'is_admin'];
public function posts(): array
{
return $this->hasMany(Post::class);
}
}
$sam = User::create(['name' => 'Sam', 'email' => 'sam@strukt.test']);
$sam->name; // Sam
$sam->is_admin = true;
$sam->save();
User::find(1);
User::where('is_admin', 0)->orderBy('name')->get();
User::first();
The table name is derived from the class short name, pluralized and snake-cased:PostComment maps to post_comments.
class User extends Model
{
protected static ?string $table = 'people';
protected static string $keyName = 'uuid';
}
fillable guards mass assignment through create(), fill(), and update().
Individual columns always accept direct assignment.
casts map values entering and leaving the database. Dates becomeDateTimeImmutable on read and return to Y-m-d H:i:s on write; bool, int,float, string, and array round-trip their JSON representation.
Timestamps are maintained when $timestamps is left at its default true.
$sam = User::create([...]);
$sam->created_at;
$sam->updated_at;
$sam = User::findOrFail(7);
$sam->update(['name' => 'Samantha']);
$sam->refresh();
$sam->delete(); // false when the model does not exist yet
$sam->exists();
$sam->key();
$sam->toArray();
$sam->toJson();
save() inserts when the model is new and updates otherwise, touching only
changed columns. Reads always go through the query builder, so nothing is cached
at the query layer.
Describe a relationship as a method delegating to hasMany(), hasOne(), orbelongsTo(). Foreign keys are derived from class names and can be overridden.
Results are computed on first call and remembered for the instance.
class Post extends Model
{
public function author(): ?User
{
return $this->belongsTo(User::class);
}
public function comments(): array
{
return $this->hasMany(Comment::class);
}
}
$post->author()->name;
$user->posts();
$user->posts()[0]->comments();
Model columns are read through __get/__set. A column that is neither an
attribute nor a declared relationship throws OutOfBoundsException, so typos
fail loudly.
model:generate introspects a database and writes Ar\Model subclasses.
php bin/dbcli model:generate --database=app.db --driver=sqlite
php bin/dbcli model:generate --database=app.db --driver=sqlite \
--table=users --namespace=App\Models --dir=src/Models
MySQL and PostgreSQL add host and credentials:
php bin/dbcli model:generate --driver=mysql --database=app \
--host=127.0.0.1 --user=root --password=secret --dir=src/Models
php bin/dbcli model:generate --driver=pgsql --database=app \
--host=127.0.0.1 --user=app --dir=src/Models
Omit --database to read the connection from a dotenv file. --env lists files
and defaults to .env.
php bin/dbcli model:generate --dir=src/Models
php bin/dbcli model:generate --env=.env,.env.local
The generator derives the class name, pins $table, selects a $keyName when
the primary key is not id, disables $timestamps when timestamp columns are
missing, and fills $casts and $fillable.
When installed as a dependency, the command is available as vendor/bin/dbcli.
db:create scans a directory for models that declare $schema and creates their
tables.
class User extends Model
{
protected static array $schema = [
'name' => ['type' => 'string', 'nullable' => false],
'email' => ['type' => 'string', 'nullable' => false, 'unique' => true],
'is_admin' => ['type' => 'bool', 'nullable' => false, 'default' => false],
'birthday' => ['type' => 'datetime', 'nullable' => true],
];
}
php bin/dbcli db:create --database=app.db --driver=sqlite --dir=src
php bin/dbcli db:create --database=app.db --driver=sqlite \
--dir=src --namespace=App\Models
Models without $schema are skipped. The command is idempotent throughIF NOT EXISTS. Types map to SQLite storage classes; MySQL and PostgreSQL use
their native SQL types.
For an identity map, unit of work, and result caching over these same models and
connection, install strukt/orm.
composer require strukt/orm
$sam = orm()->find(User::class, 7);
$sam->name = 'Samantha';
orm()->commit();
See the Orm page for the full contract.
Managed entities over strukt/ar: an identity map, a unit of work, and result
caching layered on the same Strukt\Ar\Model classes and connection.
The objects are the same entities returned by User::find(); the manager adds
managed behavior for the objects routed through it.
Direct (strukt/ar) |
Managed (strukt/orm) |
|
|---|---|---|
| Style | Immediate, explicit writes. | Deferred, batched writes. |
| Identity | One object per call. | One object per row. |
| Writes | save()/delete() run at once. |
persist()/remove() flush on commit(). |
| Reads | Query builder per query. | Query builder plus result cache. |
| Use | Low-level control. | Managed entities on a large workload. |
composer require strukt/orm
Requires PHP 8.2+; strukt/ar is pulled in automatically.
orm() returns the shared manager resolved through the default connection.
use Strukt\Orm\Manager;
Manager::fromEnv('.env', '.env.local');
$sam = orm()->find(User::class, 7);
Manager::for('name') binds a manager to a named connection.Manager::fromEnv() loads the dotenv files through Strukt\Ar\Connection and
then returns the shared manager.
find() consults the identity map before touching the database, so a row always
loads into the same instance.
$sam = orm()->find(User::class, 7);
$sam->name = 'Samantha'; // nothing written yet
$draft = new User();
$draft->fill(['name' => 'Ada', 'email' => 'ada@strukt.test']);
orm()->persist($draft);
orm()->remove($sam);
orm()->commit(); // one transaction: insert, update, delete
An untouched entity is not written: a snapshot taken when it was loaded is
compared at commit time, and only drifted columns are saved.
A failed commit rolls the batch back and rethrows the cause. The manager discards
its tracked work, so reload affected rows with find() afterward.
orm()->rollback(); // discard pending work without touching the database
Peek at the identity map without a query:
$sam = orm()->find(User::class, 7);
orm()->lookup(User::class, 7); // same instance, no round trip
orm()->lookup(User::class, 99); // null
query() returns a Repository that proxies the Ar query builder but hydrates
through the identity map, so query results share instances with find().
$admins = orm()->query(User::class)
->where('is_admin', 1)
->orderBy('name')
->get();
$first = orm()->query(User::class)
->where('email', $email)
->first();
Row results are cached by SQL and bindings, so re-running the same query reuses
the rows. The cache lives in the manager and is emptied by commit(),rollback(), and clear().
Aggregates such as count() and sum() always read live.
Installing strukt/orm brings strukt/ar in as a dependency, so both layers use
the same models, tables, and connection. Three behaviours follow:
Ar writes do not invalidate the manager's result cache. Only commit(),rollback(), and clear() flush it. A direct User::create() or$user->save() bypasses the manager, so a repeated query can serve rows that
predate the write until the next flush. Aggregates still read live.
One row can exist as two objects. Ar find()/first() materialize fresh
instances; only objects routed through orm() are identity-mapped. Editing a
managed entity and then saving it through Ar writes immediately and leaves it
dirty against its snapshot, so commit() issues a redundant update.
Ar-deleting a tracked entity makes commit() try the delete again as a
harmless zero-row change.
Interleaved Ar writes ride the commit's transaction. An Ar save() betweenpersist() and commit() uses the same connection, so a failed commit rolls
it back too.
In practice, pick one path per row or workflow. Route managed rows throughorm(), use strukt/ar directly for one-off or bulk work, and callorm()->clear() after Ar writes that touch rows also tracked by the manager.
Do not save() or delete() entities you intend to commit().
The suite runs against an in-memory SQLite connection using the same schema asstrukt/ar and covers the identity map, unit of work, repositories, and result
cache.
Cryptography, signing, JWT, CSRF, and SSL utilities for PHP 8.2+, built on Strukt
Math, Console, and Fs.
No external crypto services, HTTP calls, or bundled private keys are required.
Keys are generated in memory and written only where requested.
composer require strukt/key
The package installs vendor/bin/kcli.
hash Compute a message digest
bcrypt Hash a password with bcrypt
bcrypt:check Check a password against a bcrypt digest
keys:new Generate an RSA key pair on disk
enc/dec AES encrypt / decrypt
sign/verify Sign / verify data with an RSA key
cert:selfsign Generate a key pair and self-signed certificate
cert:verify Verify a certificate
cert:xpub Extract the public key from a certificate
cert:info Read certificate fields
jwt:encode Mint a JSON web token
jwt:decode Decode and validate a JSON web token
envelope:close Seal a message for a set of recipients
envelope:open Open a sealed message with a private key
csrf Mint a CSRF token
csrf:check Validate a CSRF token
Every applicable command supports --json.
./vendor/bin/kcli hash "hello world"
./vendor/bin/kcli bcrypt "s3cret!" --cost=12
./vendor/bin/kcli bcrypt:check "s3cret!" '$2y$12$...'
./vendor/bin/kcli keys:new --dir keys --name alice
./vendor/bin/kcli sign "the document" --key keys/alice.pem
./vendor/bin/kcli verify "the document" "$SIG" --key keys/alice.pub.pem
./vendor/bin/kcli enc "top secret" "a-strong-key" --json
./vendor/bin/kcli dec "$CIPHERTEXT" "a-strong-key"
./vendor/bin/kcli cert:selfsign --dir keys --name web --common strukt.example --days 365
./vendor/bin/kcli cert:info keys/cert.pem
./vendor/bin/kcli cert:xpub keys/cert.pem --json
./vendor/bin/kcli jwt:encode '{"role":"admin"}' "jwt-secret"
./vendor/bin/kcli jwt:decode "$TOKEN" "jwt-secret"
./vendor/bin/kcli csrf '{"user":42}' "csrf-key"
./vendor/bin/kcli csrf:check "$TOKEN" "csrf-key"
./vendor/bin/kcli envelope:close "letter" "ca.pem,another.pem" --json
./vendor/bin/kcli envelope:open "$ENV_KEY" "$SEALED" "$IV" --key alice.pem
Input can be piped or read from a file with --file:
cat message.txt | vendor/bin/kcli hash
./vendor/bin/kcli enc --file document.txt "a-strong-key"
use Strukt\Hash\Bcrypt;
use Strukt\Hash\Sha;
$hash = (new Bcrypt())->makeHash('p@55w0rd');
(new Bcrypt())->verify('p@55w0rd', $hash); // true
Sha::once256('abc');
Sha::dbl256('abc');
Sha::hmac('key', 'data', 'sha256'); // raw binary
use Strukt\Codec;
$codec = Codec::make('a-strong-key'); // fresh random IV per message
$wrapped = $codec->encode($plain); // iv-hex:base64
$codec->decode($wrapped);
use Strukt\Jwt;
$signer = Jwt::make([
'secret' => 'jwt-secret',
'algo' => 'HS256',
'expire' => 3600,
]);
$token = $signer->encode(['role' => 'admin']);
$claims = $signer->decode($token); // stdClass|null
$signer->valid($token); // bool
use Strukt\Csrf;
$token = Csrf::make(['user' => 42], 'csrf-key', 300)->encode();
Csrf::valid($token, 'csrf-key'); // true
use Strukt\Ssl\KeyPairBuilder;
use Strukt\Ssl\Signer;
use Strukt\Ssl\Cipher;
$pair = KeyPairBuilder::make('keys/', 'alice')->makeKeyPair();
$pair->makePrivateKeyFile();
$pair->makePublicKeyFile();
$signer = new Signer($pair);
$signature = $signer->create('the document');
$signer->check('the document', $signature); // true
$cipher = new Cipher($pair);
$cipher->decrypt($cipher->encrypt('classified'));
use Strukt\Ssl\Envelope;
[$envKeys, $sealed, $iv] = Envelope::withAlgo()
->useCerts([$certPath])
->closeWithIv($message);
Envelope::withAlgo($iv)
->usePrivKey($privateKey)
->open($envKeys[0], $sealed);
use Strukt\Ssl\Request;
use Strukt\Ssl\Certificate\Parser;
use Strukt\Ssl\Csr\UniqueName;
$request = new Request($pair, new UniqueName(['common' => 'strukt.example']));
$request->selfSign(365);
$request->verify();
$parser = new Parser($pem); // or Cert::withPath()->parser()
$parser->getDomain();
$parser->getIssuer();
$parser->isExpired(); // bool
The legacy facade remains:
use Strukt\Ssl\All;
$tool = All::makeKeys();
$tool = All::keyPath('keys/alice.pem');
$tool->toSend($message);
$tool->useCsr(['common' => 'x'])->selfSign();
codec($key, $cipher, $vector?) Codec
bcry($password, $cost?) encode()/verify()
sha256($input) hex digest
sha256dbl($input) double hex digest
csrf($data, $key, $ttl?) encode()/decode()/valid()
jwt($data, $secret, $algo?, $ttl?) token or valid()/yield()
ssl($arg?) All facade
keypair($privateKeyPath?) KeyPair
pubkey($path) PublicKey
Secrets are explicit parameters, never pulled from a global configuration file.
Bcrypt uses native password_hash() and password_verify().Jwt is self-contained and uses hash_hmac with constant-time comparison.Codec carries a fresh IV in each encoded message.DateTime values.Zero-dependency interfaces for HTTP, authentication, forms, service providers,
caching, and processes. This is the foundational Strukt package: it requires
nothing but PHP, so any other package or application can depend on it without
pulling in a runtime.
composer require strukt/contract
| Contract | Purpose |
|---|---|
Strukt\Contract\UserInterface |
An authenticated principal. Nullable throughout; password is write-only. |
Strukt\Contract\AuthInterface |
Resolves the current principal. |
Strukt\Contract\MiddlewareInterface |
One link in the HTTP pipeline. |
Strukt\Contract\FormInterface |
Field definitions, submitted values, and validation state. |
Strukt\Contract\ProviderInterface |
Container bindings contributed by a service provider. |
Strukt\Contract\Http\RequestInterface |
Inbound method, URI, headers, parameters, files, and user. |
Strukt\Contract\Http\ResponseInterface |
Status, headers, body, and send. |
Strukt\Contract\Http\SessionInterface |
Per-client state, including flash data. |
Strukt\Contract\Http\DownloadInterface |
An attachment or inline payload. |
Strukt\Contract\Http\UploadedFileInterface |
A file received through a multipart form. |
Strukt\Contract\CacheDriverInterface |
Shared cache driver operations. |
Strukt\Contract\ProcessInterface |
Shared process resource operations. |
Strukt\Contract\PromptableProcessInterface |
A process that accepts standard input. |
Two rules hold across the HTTP contracts:
ResponseInterface, never a specific response type.Strukt\User, Strukt\Session, and Strukt\Auth are dependency-free
implementations. They are the default wiring, not the only option.
use Strukt\User;
$user = new User(id: '7', username: 'pitsolu', email: 'pitsolu@gmail.com');
$user->getUsername(); // pitsolu
$user->get('id'); // 7
$user->has('email'); // true
$user->toArray();
The password is write-only: it is never returned by a getter, is not addressable
through get(), and never appears in toArray().
$user = new User(password: password_hash('secret', PASSWORD_BCRYPT));
$user->hasPassword(); // true
$user->verifyPassword('secret'); // true
$user->verifyPassword('wrong'); // false
$user->get('password', 'leaked'); // leaked, never the real hash
use Strukt\Session;
$session = new Session();
$session->set('theme', 'dark');
$session->get('theme'); // dark
$session->has('theme'); // true
$session->flash('notice', 'saved');
$session->getFlash('notice'); // saved, consumed on read
A key holding null is still present.
Auth receives its session by injection rather than resolving a global
container.
use Strukt\Auth;
use Strukt\Session;
use Strukt\User;
$auth = new Auth(new Session());
$auth->guest(); // true
$auth->login(new User(id: '7', username: 'pitsolu'));
$auth->check(); // true
$auth->id(); // 7
$auth->logout();
(new Auth($session))->user()?->getUsername();
login() rejects a user with neither an id nor a username.
use Strukt\Contract\Http\RequestInterface;
use Strukt\Contract\Http\ResponseInterface;
use Strukt\Contract\MiddlewareInterface;
final class Authenticate implements MiddlewareInterface
{
public function process(
RequestInterface $request,
ResponseInterface $response,
callable $next,
): ResponseInterface {
if ($request->user() === null) {
return $response->setStatus(401);
}
return $next($request, $response);
}
}
Returning without calling $next short-circuits the chain.
The HTTP interfaces are implemented by strukt/router's Strukt\Http stack:Request, Response, ResponseBuilder, Download, UploadedFile, and the
session stores. The cache contract is implemented by strukt/cache; the process
contracts are implemented by strukt/process.
Applications and packages should type against the contracts rather than the
concrete classes.
Fast callable events and named command queues. This package is the strukt5
line and a drop-in match for the strukt4/event public API.
Internally it fixes the halt-hook leak in Loop, makes run() exception-safe,
and stops treating array callables such as [$object, 'method'] as argument
arrays.
composer require strukt/event
Requires PHP 8.2+ and strukt/base. Composer loads src/helpers.php and
installs the global event() and cmd() helpers.
event(string $name, ?callable $function = null): ?Strukt\Event
cmd(string $name, ?array $args = null): mixed
Register and invoke an event:
$login = event(
'login',
static fn (string $user, string $password): bool =>
$user === 'admin' && $password === 'secret',
);
$accepted = $login?->apply('admin', 'secret')->exec();
Calling event() again with the same name replaces the callback but preserves
its position in the command order.
Execute a registered command:
cmd('login', ['admin', 'secret']);
cmd('greet', ['name' => 'Ada']);
Both return null when the command is unknown.
Strukt\Event retains a Closure and its ReflectionFunction.
use Strukt\Event;
$event = Event::create(fn (string $name): string => "Hello {$name}");
$event->apply('Ada')->exec(); // Hello Ada
$event->applyArgs(['name' => 'Ada'])->exec();
$event->clear();
$event(); // shorthand for exec()
apply() stores positional arguments. applyArgs() accepts positional or named
keys; named keys use PHP named-argument invocation. Each call replaces the
arguments stored by the previous one. Calling exec() without prepared
arguments invokes the callback with no arguments.
Strukt\Cmd is the process-local named command registry.
use Strukt\Cmd;
Cmd::add('greet', static fn (string $name): string => "Hello {$name}");
Cmd::add('welcome', ['name' => 'Ada'], static fn (string $name): string => "Hi {$name}");
Cmd::get('greet'); // callable|null
Cmd::exists('greet'); // true
Cmd::ls(); // ['greet', 'welcome']
Cmd::ls('^g'); // ['greet']
Cmd::doc('greet');
Cmd::exec('welcome'); // uses the stored default argument
Cmd::reset();
Ls() filters with a regular-expression body. doc() returns a compact
description from the callback's doc comment, or the command name when none is
available. Executing an unknown command throws InvalidArgumentException.
Strukt\Loop extends Cmd with an insertion-ordered queue and a repeatable
halt hook.
use Strukt\Loop;
Loop::add('prepare', static function (): void {
// work
});
Loop::halt(static function (): void {
// invoked while paused; call Loop::pause(false) to continue
});
Loop::run();
run() executes commands in insertion order and removes them after they run.
While paused, the halt callback is re-invoked each iteration, which makes the
loop a read-eval-print loop. Calling halt() again replaces the previous
terminal callback instead of leaving a stale command behind.
Options:
Loop::run(['alias' => 'worker']); // filter through the base alias map
Loop::run(['limit' => 1000]); // cap halt iterations
If a command or hook throws, run() removes the terminal command before the
exception escapes, so a failed run leaves no dangling halt command.
Event converts a callable to a Closure and reflection object once.Cmd stores callbacks, defaults, and insertion order in dedicated arrays.Loop re-invokes the halt hook while paused, enabling REPL-style loops.Rule-based form definition, validation, and error reporting, built on the Strukt
rules engine.
A form is described by its rules. There is no docblock parsing, no reflection,
and no request object to hand: give the form an array of submitted values and it
reports which fields failed and why.
composer require strukt/form
Requires PHP 8.2+ and strukt/rules.
Extend Strukt\Form\Form and return rules keyed by field name. A field may use a
pipe-separated string or a list of rule tokens.
use Strukt\Form\Form;
final class LoginForm extends Form
{
public function rules(): array
{
return [
'email' => 'required|email',
'password' => 'required|min_length:8',
];
}
}
public function rules(): array
{
return [
'name' => 'required|string|min_length:2|max_length:50',
'email' => ['required', 'email', 'max_length:120'],
'age' => 'nullable|integer|min:18',
'role' => 'required|in:admin,editor,viewer',
'tags' => 'array|max_count:5',
'bio' => 'nullable|max_length:500',
];
}
The full rule list, messages, and custom rules live in strukt/rules.
$form = new LoginForm();
$form->submit($_POST);
if ($form->validate() === false) {
foreach ($form->errors() as $field => $messages) {
// ['email' => ['Email must be a valid email address.']]
}
}
Use validateWith() to submit and validate in one call:
$form->validateWith([
'email' => 'ada@example.com',
'password' => 'analytical-engine',
]);
$form->validate(); // true
$form->hasErrors(); // false
$form->errors(); // []
$form->errorsFor('email'); // []
| Method | Purpose |
|---|---|
fields() |
Field names in declaration order. |
rules() |
The rules definition. |
value($field, $default = null) |
One submitted value. |
setValue($field, $value) |
Set one value. |
setValues(array $values) |
Merge several values. |
values() |
Every submitted value. |
isSubmitted() |
Whether values have been submitted. |
submit(array $values = []) |
Merge values and mark as submitted. |
validate() |
Run the rules; true when everything passes. |
validateWith(array $values) |
Submit, then validate. |
hasErrors() |
Whether any field failed. |
errors() |
Failure messages keyed by field. |
errorsFor($field) |
Failure messages for one field. |
reset() |
Clear values, errors, and the submitted flag. |
validated(array $values) |
A fresh submitted form with no failures. |
submit(), setValue(), and setValues() accumulate. Call reset() first, or
use validated(), to start over.
$fresh = $form->validated($otherSubmission);
Override a message per rule, or for one field with field.rule. The field
specific override wins. An override is a complete message used as written and may
use :field, :0, :1, :list, and :format.
public function messages(): array
{
return [
'email.required' => 'We need an email address to reach you.',
'terms.required' => 'You must accept the terms to continue.',
'min_length' => ':field is too short, use at least :0 characters.',
];
}
Register a custom rule on a form-specific registry so the rest of the application
is unaffected.
use Strukt\Rules\RuleRegistry;
final class ReservationForm extends Form
{
protected function registry(): RuleRegistry
{
return RuleRegistry::create()->extend(
'after',
fn (string $field, array $params) => new AfterRule($field, $params[0]),
);
}
public function rules(): array
{
return [
'end' => 'required|date|after:start',
'start' => 'required|date',
];
}
}
form(LoginForm::class, ['email' => $email, 'password' => $password]);
form(LoginForm::class, $values, submit: false);
form_check(
['age' => 12],
['age' => 'required|integer|min:18'],
);
form() creates and submits a form instance. form_check() validates a plain
array against a rules definition without a form class.
The router can attach a form with form() on a route. It submits the request
values, validates before the action runs, and returns a 422 JSON payload with
field errors when validation fails.
$router->post('/login', fn (
Strukt\Contract\FormInterface $form,
) => 'hi '.$form->value('email'))->form(LoginForm::class);
Typed PHP class generation, lightweight text templating, and PHPDoc annotation
utilities.
composer require strukt/generator
Requires PHP 8.2+ and depends on strukt/base and strukt/commons.
$html = template(
'<h1>{{title}}</h1>',
['title' => 'Dashboard'],
);
Template syntax:
Hello {{name}}!
Missing values render as an empty string. Null and false values also render as
empty text. Scalars are converted to strings; arrays and objects are JSON encoded
unless an object exposes __toString().
{{begin:people}}
<b>{{name}}</b> {{surname}}
{{end:people}}
The value at people must be an array of arrays. Each item is rendered against
its own values.
{if enabled}
This section is visible.
{/if}
The section is trimmed when enabled and removed when the value is missing or
false.
Templator::create() applies directives in this order:
use Strukt\Templator;
Templator::create($template, $data);
Templator::element($template, $data);
Templator::loop($template, $data);
Templator::condition($template, $data);
Templator::console($template);
Templator::trimBlanks($text);
console() resolves {color name} sections when a console color class andcolor() helper are available.
Strukt\DocBlocker converts between plain annotation text and PHPDoc blocks.
use Strukt\DocBlocker;
$plain = "@Route(/)\n@Method(GET, POST)";
$docblock = (string) new DocBlocker($plain);
DocBlocker::deBlock($docblock);
The helper deblock($input) is also available.
Strukt\BasicBlock parses single-line annotations with parenthesized values.
use Strukt\BasicBlock;
$annotations = BasicBlock::parse(<<<'DOC'
/**
* @Route(/users)
* @Method(GET, POST)
* @Middleware(AuthToken)
* @Middleware(GVerify)
*/
DOC);
// [
// 'Route' => '/users',
// 'Method' => ['GET', 'POST'],
// 'Middleware' => ['AuthToken', 'GVerify'],
// ]
A single argument returns as a string, comma-separated arguments return as a
list, and repeated annotation names are merged into one list. Quoted text and
nested brackets are respected when splitting arguments.
Strukt\ClassBuilder renders a PHP class declaration with inheritance,
interfaces, constructors, methods, and method annotations.
use Strukt\ClassBuilder;
$source = (string) new ClassBuilder([
'ns' => 'App\\Controller',
'class_name' => 'UserController',
'extends' => 'BaseController',
'methods' => [
[
'name' => 'index',
'params' => '',
'body' => "\treturn [];",
'block' => ['return' => 'array'],
],
],
]);
Required keys are ns and class_name. Optional keys are extends,implements, and methods. Missing or empty namespace/class names throwInvalidArgumentException.
Add a constructor fluently:
$builder->addInit(['string $name', 'int $id']);
Method definitions accept name, params, body, and block; the block map
becomes PHPDoc tags. Rendering is repeatable and does not duplicate methods.
Strukt\Annotation\Parser reads @Name, @Name(item), @Name(a, b, c), and@Name(key=value) annotations from a class, its properties, and its methods.
use Strukt\Annotation\Parser;
$notes = (new Parser(new ReflectionClass(Worker::class)))->getNotes();
The result contains a class_name key plus optional class, properties, andmethods keys. Repeated names are merged into one entry whose items list holds
every value.
| Helper | Purpose |
|---|---|
template($template, $data) |
Render values, loops, and conditions. |
huecli($template) |
Resolve optional console color sections. |
deblock($input) |
Remove PHPDoc comment delimiters. |
The helpers are declared conditionally and do not overwrite application
functions.
Immutable numeric and matrix primitives. The package keeps the existingstrukt4/math behaviour and adds aggregation, roots, deltas, vector products,
and explicit transformation pipelines.
composer require strukt/math
Strukt\Number wraps one int or float and returns a new instance from every
operation.
use Strukt\Number;
$sum = Number::sum([1, 2, 2.5]); // 5.5, a Number
$mean = Number::sum($column)->parts(count($column));
$average = number(10)->add(0.2)->parts(3); // 3.4
$num = number(1000)
->add(200) // 1200
->subtract(100) // 1100
->times(2) // 2200
->parts(4); // 550
$num->mod(9);
$num->raise(2);
$num->ratio(1, 3); // two parts in the given ratio
$num->yield();
$num->reset();
echo $num;
parts() throws DivisionByZeroError on a zero divisor. Use partsOr() for a
safe fallback:
number(10)->partsOr(0, 7); // 7
number(10)->partsOr(0); // 0
Number::sum([1, 2, 3]); // 6, int
Number::sum([1, 2.5]); // 3.5, float
Number::sum([]); // 0
Number::sum($iterable); // any iterable
number(0)->max(1)->yield(); // 1
number(7)->max()->yield(); // 7
number(9)->min(5)->yield(); // 5
sum() preserves the input kind: an int only when every input is an int.min() and max() accept raw numbers or Number objects.
number(-4.5)->abs(); // 4.5
number(2.1)->ceil(); // 3, integer
number(2.9)->floor(); // 2, integer
number(20.5111111)->round(2); // 20.51, float
number(8)->log(2); // 3
number(1000)->log10(); // 3
number(1)->exp(); // 2.718281828459045
ceil() and floor() yield integers so they can index a sequence; round()
still yields a float. log() defaults to the natural logarithm and rejects a
base of zero or less.
number(5)->compare(3); // 1
number(5)->compare(5.0); // 0
number(5)->compare(number(3)); // 1
$num->gt(302499);
$num->gte(302500);
$num->lt(302501);
$num->lte(302501);
$num->equals(-302500);
$num->negate();
Floating-point comparisons can use an absolute delta:
number(0.1)->add(0.2)->equals(0.3); // false
number(0.1)->add(0.2)->equalsWithDelta(0.3); // true
number(0.1)->add(0.2)->equalsWithDelta(0.3, 0.0); // false
Other helpers include format(), type(), and Number::random($count, $min,
$max).
Strukt\Matrix is an immutable matrix of Number values. Every operation
returns a new matrix.
use Strukt\Matrix;
$a = Matrix::create([[1, 2], [3, 4]]);
$b = Matrix::create([[1, 1], [1, 1]]);
$a->add($b)->yield(); // [[2, 3], [4, 5]]
$a->subtract($b)->yield(); // [[0, 1], [2, 3]]
$a->scale(2)->yield(); // [[2, 4], [6, 8]]
add() and subtract() require matching shapes and raise Strukt\Raise
otherwise.
$a = matrix([[1, 2, 3], [4, 5, 6], [7, 8, 9]]);
$b = matrix([[11, 22, 33], [44, 55, 66], [77, 88, 99]]);
echo $a->multiply($b);
$a->multiply($b)->yield();
Matrix::dot([1, 2], [3, 4]); // 11
Matrix::dot([1, 0], [0, 1]); // 0
Matrix::cosineSimilarity([1, 2, 3], [1, 2, 3]); // 1.0
Matrix::cosineSimilarity([1, 0], [0, 1]); // 0.0
Entries are paired by position, so input keys are irrelevant. Mismatched lengths
throw InvalidArgumentException; a zero vector makes cosineSimilarity() throwDivisionByZeroError.
Matrix::create([[3, 4]])->norm(); // 5, Euclidean
Matrix::create([[1, -2]])->norm(1); // 3, sum of absolute values
Any other norm order throws InvalidArgumentException.
Pipeline is mutable and eager: next() returns the same object, and each step
runs when it is added.
use Strukt\Pipeline;
pipeline([12, 3, 2])
->next(static fn (int $m, int $x): int => $m * $x)
->next(static fn (int $mx, int $c): int => $mx + $c)
->next(static fn (int $result): int => $result)
->yield(); // 38
Positional pipelines pair values by position. Scoped pipelines bind them by
parameter name:
scoped(['c' => 12, 'm' => 3, 'x' => 2])
->next(static fn (int $m, int $x): int => $m * $x)
->next(static fn (int $mx, int $c): int => $mx + $c)
->next(static fn (int $result): int => $result)
->yield(); // 18
with() replaces a scope value explicitly. A scoped step may not reuse a key
that was already in the scope. A step that throws leaves the pipeline unchanged;
by-reference parameters are rejected.
$pipeline = pipeline([1]);
$pipeline->isPending(); // true
$pipeline->hasResult(); // false
monos() is a deprecated shim. Use pipeline() or scoped().
Monad is immutable and holds exactly one value. of lifts a value, bind
composes a step that may return a monad, map is bind composed with of, andjoin reveals the value.
use Strukt\Monad;
Monad::of(12)
->bind(static fn (int $c): Monad => Monad::of(3)->map(static fn (int $m) => $m * 2))
->bind(static fn (int $mx): Monad => Monad::of($mx + 12))
->join(); // 18
A monad can be shared and joined repeatedly:
$base = Monad::of(2);
$base->map(static fn (int $v): int => $v * 2)->join(); // 4
$base->map(static fn (int $v): int => $v * 3)->join(); // 6
$base->join(); // 2
bind is strict, so errors surface where they occur. map does not flatten a
returned monad; use bind when flattening is required.
Typed, resource-safe process execution for PHP 8.2 and later. The package
supports sequential command batches, line callbacks, captured output,
interactive processes, supervised worker pools, and a managed supervisor CLI.
composer require strukt/process
Requires PHP 8.2+ and strukt/base.
$result = process('printf "hello\n"');
echo $result->outputs();
Stream complete output lines while the command runs:
$result = process(
'printf "first\nsecond\n"',
static function (string $line): void {
echo "received: {$line}";
},
);
process() waits for each command, drains standard output and standard error
concurrently, and returns a Strukt\ProcessResult.
| Helper | Parameters | Returns |
|---|---|---|
process |
command string or batch, optional callback | ProcessResult |
pprocess |
command string | PromptableProcessInterface |
pool |
size, command, cwd, env | ProcessPool |
supervisor |
size, command, restart cap, backoff, cwd, env | ProcessSupervisor |
switchChannels |
bool | Swaps logical output/error channels for new processes. |
$result = process('php -r "echo 2 + 2;"');
echo $result->outputs(); // 4
$process = $result->resource();
Commands run in array order. Each command has its own process resource and
captured output.
$result = process([
'printf "compile\n"',
'printf "test\n"',
]);
echo $result->outputs(0); // compile
echo $result->outputs(1); // test
$first = $result->resource(0);
$second = $result->resource(1);
An empty list returns an empty result. Every command must be a non-empty string;
invalid commands throw InvalidArgumentException.
The callback receives complete logical standard-output lines, including line
endings. A final fragment without a newline is delivered after the process
closes its output stream.
$lines = [];
process('printf "one\ntwo\n"', static function (string $line) use (&$lines): void {
$lines[] = trim($line);
});
// ['one', 'two']
Callbacks may declare no parameters. Standard error is drained but is not sent to
the callback.
Strukt\ProcessResult extends ArrayIterator and may be iterated as a list ofStrukt\Process objects.
$result->resource(int $idx = 0): Process
$result->outputs(int $idx = 0): ?string
$result->errors(int $idx = 0): ?string
resource() throws OutOfBoundsException for a missing index. outputs() anderrors() return null when no entry exists and an empty string when the
command produced no output on that channel.
Both channels are retained.
$result = process('sh -c "printf failure >&2; exit 7"');
echo $result->errors(); // failure
echo $result->resource()->error(); // failure
echo $result->resource()->close(); // 7
Process::run() throws RuntimeException when a command cannot be started.
Invalid constructor handles or empty commands throw InvalidArgumentException.
The default mapping is standard output to output() and standard error toerror().
switchChannels(true);
$result = process('sh -c "printf output; printf error >&2"');
echo $result->outputs(); // error
echo $result->errors(); // output
switchChannels(false);
The setting affects newly created processes and remains until changed.
Use pprocess() when a command expects input.
$prompt = pprocess('read password; printf "%s\n" "$password"');
$prompt->write('secret');
$prompt->closeInput();
echo $prompt->read(); // secret
write() appends PHP_EOL and returns the bytes written. Close standard input
when the child must receive EOF.
$prompt->read();
$prompt->error();
$prompt->readline();
$prompt->isRunning();
$prompt->terminate();
$prompt->close();
Closing an already-closed pipe is safe. Read output before close() if it has
not already been captured.
ProcessPool supervises a fixed set of concurrent workers.
$pool = new ProcessPool(3);
$pool->start(PHP_BINARY.' '.escapeshellarg($workerScript));
while ($pool->running()) {
$pool->drain(static function (int $index, string $line): void {
printf("worker %d: %s", $index, $line);
});
usleep(100000);
}
$pool->stop();
Or start one with the helper:
$pool = pool(3, [PHP_BINARY, $workerScript, '--prefix=demo']);
start() spawns the workers with separate pipes; starting twice throwsRuntimeException. drain() never blocks on a worker and retains standard
error on each worker. stop() terminates and reaps every worker, returning exit
codes keyed by spawn index.
Introspection:
| Method | Behavior |
|---|---|
count |
Configured pool size. |
active |
Workers still running. |
running |
True while any worker runs. |
processes |
Workers in spawn order. |
process($index) |
One worker by spawn index. |
ProcessSupervisor composes a pool with a restart policy. A dead worker is
replaced by a fresh copy of the original command in the same slot.
$supervisor = new ProcessSupervisor(3, restartCap: 5, backoff: 0.5);
$supervisor->onExit(static function (int $index, int $exitCode): void {
printf("worker %d exited with %d\n", $index, $exitCode);
});
$supervisor->start(PHP_BINARY.' '.escapeshellarg($workerScript));
while (true) {
$supervisor->supervise(static function (int $index, string $line): void {
printf("worker %d: %s", $index, $line);
});
usleep(100000);
}
Or use the helper:
$supervisor = supervisor(3, [PHP_BINARY, $workerScript], restartCap: 5);
Policy:
restartCap bounds restarts per slot. -1 never gives up.backoff is the minimum delay before restarting the same slot.onExit() fires whenever a worker is found dead, whether or not it restarts.supervise() drains output first, then revives dead slots. Introspection includesrestarts($index), totalRestarts, and stop().
Strukt\ManagedSupervisor runs the supervision loop as a detached daemon and
persists observable state.
use Strukt\ManagedSupervisor;
$supervisor = new ManagedSupervisor('mail', size: 3, restartCap: 5);
$supervisor->start(PHP_BINARY.' '.escapeshellarg($workerScript));
Artifacts live under ~/.strukt/supervisor by default, orSTRUKT_SUPERVISOR_DIR / the $dir argument:
| Artifact | Purpose |
|---|---|
<name>/supervise.pid |
Daemon pid. |
<name>/state.json |
Snapshot written every supervision pass. |
<name>/output.log |
Combined worker output and supervisor events. |
ManagedSupervisor::status('mail');
ManagedSupervisor::running('mail');
ManagedSupervisor::ps();
ManagedSupervisor::tail('mail', 50);
ManagedSupervisor::kill('mail', 0);
ManagedSupervisor::stop('mail');
php vendor/bin/supervise start mail "php worker.php" --size=2 --dir=/var/run/sv
php vendor/bin/supervise ps
php vendor/bin/supervise status mail
php vendor/bin/supervise kill mail 0
php vendor/bin/supervise logs mail --lines=100
php vendor/bin/supervise stop mail
Add --json for scripting. Run php vendor/bin/supervise list for the command
reference.
Process::run(array $commands, ?callable $callback = null): ProcessResult
$process->wait(callable $callback): void
$process->output(): string
$process->poll(?callable $callback = null): bool
Process::switchChannels(bool $switch = true): void
wait() uses non-blocking pipes and stream_select() so large error output
cannot block while the parent waits on standard output. poll() drains what is
available without blocking and returns true while the process is running.
Strukt\Pprocess extends Strukt\AbstractPromptableProcess and adds write()
through PromptableProcessInterface.
Process::run() closes standard input before waiting, drains both output
channels, dispatches pending output, and reaps the child. Captured strings remain
available through the result and process objects after cleanup.
For manual interactive processes:
write() as needed.closeInput() when no more input will be sent.close() or closeAll().All process objects attempt safe cleanup in __destruct().
A Memcached-backed FIFO message queue with replies. It supports the classic
producer/consumer flow: push a job, pop it, answer with a reply, and wait for the
answer.
The implementation is concurrency-safe: pop() is CAS-guarded, so parallel
workers cannot receive the same message twice.
memcached extension and libmemcached127.0.0.1:11211composer require strukt/queue
The package depends on strukt/base, strukt/console, and strukt/process.
The kqueue binary is installed into vendor/bin/.
use Strukt\Queue;
$queue = new Queue(['prefix' => 'acme']);
$id = $queue->push('jobs', ['user' => 7, 'type' => 'export']);
// in a worker
$job = $queue->pop('jobs', $id);
$queue->reply('jobs', $id, 'exported-42.pdf');
// in the caller
$reply = qwait('jobs', $id, 30);
prefix option.Strukt\Queue\Supervisor.Strukt\Queue is the complete surface.
$queue = new Queue(['prefix' => 'acme']);
$queue->push('jobs', $payload, $ttl = null); // id
$queue->pop('jobs', $id); // value or false
$queue->reply('jobs', $id, $reply, $ttl = null);
$queue->getReply('jobs', $id); // reply or null
$queue->is_empty('jobs');
$queue->is_processed('jobs', $id);
$queue->count('jobs');
$queue->flush('jobs');
$queue->flushAll($prefix = null);
$queue->queues();
$queue->all();
$queue->prefixes();
$queue->client(); // Memcached
$queue->prefix();
The constructor options are prefix, Memcached host, port, ttl,username, and password.
qpush($queue, $item, $options?) Enqueue and return the id.
qpop($queue, &$id, $options?) Dequeue, or return false when empty.
qreply($queue, $id, $reply, $options?) Store a reply.
qwait($queue, $id, $seconds?, $options?) Block until a reply arrives.
qempty($queue, $options?) Whether nothing remains.
qprocessed($queue, $id, $options?) Whether a message was popped.
qwait() gives up after its timeout instead of hanging forever.
kqueue push jobs 'my payload'
kqueue pop jobs
kqueue status jobs
kqueue queues
kqueue queues --prefix jobs
kqueue flush jobs
kqueue flush --all --prefix jobs
kqueue flush --all
kqueue list and kqueue help <command> work locally.
Strukt\Queue\Supervisor keeps a pool of workers alive with restart cap and
backoff. Messages claimed by a dead worker but never finished are returned to the
queue.
qwait().The original queue concept was contributed by Maurizio Giunti
(https://www.mauriziogiunti.it) under the MIT license.
Self-contained, explainable AI/NLP utilities for PHP 8.2+, built on the Strukt
Math primitives.
<center><img src="owl.jpg" width="200" height="200"></center>
No API keys, network services, Python, model server, or database are required.
composer require strukt/owl
The package installs vendor/bin/owl.
sentiment Analyze sentiment
emotion Detect emotions
keywords Extract keywords
summarize Extractive TF-IDF-style summary
language Detect language
similarity Compare two texts
match Find the closest candidate
classify Classify text with Naive Bayes
train Train a Naive Bayes model
spam Detect spam with a built-in heuristic
readability Analyze readability
anomaly Detect numeric anomalies
Every applicable command supports --json.
./vendor/bin/owl sentiment "This product is absolutely fantastic!"
./vendor/bin/owl emotion "I am so happy that we finally won!"
cat article.txt | ./vendor/bin/owl keywords --limit=10
cat article.txt | ./vendor/bin/owl summarize --sentences=5
./vendor/bin/owl language "Habari ya asubuhi, uko aje?"
./vendor/bin/owl similarity "Kenya Airways" "Kenya Airways Limited"
./vendor/bin/owl match "Kenya Airways Ltd" "Kenya Airways" "Kenya Power" "Safaricom"
cat article.txt | ./vendor/bin/owl readability
./vendor/bin/owl anomaly "1,2,3,4,5,6,7,8,9,1000" --method=iqr
Text can be piped or read from a file:
cat article.txt | vendor/bin/owl keywords
vendor/bin/owl sentiment --file message.txt
vendor/bin/owl train sports/*.txt --class=sports --model=model.json
vendor/bin/owl train business/*.txt --class=business --model=model.json
vendor/bin/owl classify article.txt --model=model.json
Training accumulates into the existing model file, so each call can add one
class.
use Strukt\Owl\Sentiment\SentimentAnalyzer;
$result = (new SentimentAnalyzer())->analyze('I absolutely love PHP!');
Main classes:
Strukt\Owl\Emotion\EmotionAnalyzer
Strukt\Owl\Similarity\Similarity
Strukt\Owl\Nlp\Text
Strukt\Owl\Nlp\KeywordExtractor
Strukt\Owl\Nlp\Summarizer
Strukt\Owl\Nlp\Readability
Strukt\Owl\Classifier\NaiveBayesClassifier
Strukt\Owl\Language\LanguageDetector
Strukt\Owl\Spam\SpamDetector
Strukt\Owl\Anomaly\AnomalyDetector
Owl favors deterministic, inspectable algorithms over opaque remote services.
Every numeric step is expressed with Strukt\Math primitives, so scores are
built from typed value objects rather than loose floating-point arithmetic.
It is suitable for CLI tools, PHP applications, offline processing, prototypes,
data cleaning, and lightweight NLP.
Data-driven validation rules, a parser, a registry, and a value validator.
Every built-in rule has a declaration in src/Strukt/Rules/Spec/ describing its
arity, parameter format, failure message, and bail behaviour. Those
specifications compile into one table read by the shared engine, so parsing,
messages, and bail behaviour cannot drift apart.
This package has no runtime dependencies and powers strukt/form.
composer require strukt/rules
Validator takes field values and a rules definition keyed by field name. Each
entry is a pipe-separated string or a list of rule tokens.
use Strukt\Rules\Validator;
$errors = (new Validator())->validate(
['age' => 12],
['age' => 'required|integer|min:18'],
);
// ['age' => ['Age must be at least 18.']]
The standalone helper is equivalent:
$errors = check(
['age' => 12],
['age' => 'required|integer|min:18'],
);
Failures are collected per field in declaration order.
| Rule | Passes when |
|---|---|
required |
Value is not null, '', or []. |
filled |
Same check as required. |
nullable |
Value is not null. |
0 and false count as present. A non-required field with an absent value skips
its remaining rules.
| Rule | Passes when |
|---|---|
string |
Value is a string. |
integer |
An int, or a string of digits. 5.5 and 1e3 fail. |
numeric |
is_numeric. |
boolean |
Bool, or 0/1/true/false/on/off/yes/no. |
array |
Value is an array. |
| Rule | Passes when |
|---|---|
email |
FILTER_VALIDATE_EMAIL. |
url |
FILTER_VALIDATE_URL. |
uuid |
RFC 4122 UUID. |
ip |
FILTER_VALIDATE_IP. |
date |
A real date in the given format, default Y-m-d. |
regex |
The pattern matches. |
alpha |
Letters only, across scripts. |
alpha_num |
Letters and numbers, across scripts. |
date rejects invalid calendar dates rather than rolling them over.
'born_on' => 'date', // Y-m-d
'shipped' => 'date:m/d/Y', // 02/29/2024
regex accepts patterns with or without delimiters and keeps commas inside a
pattern.
'zip' => 'regex:^\d{5}$',
'ssn' => 'regex:/^\d{3}-\d{4}$/',
'csv_id' => 'regex:^\d{3},\d{4}$',
| Rule | Passes when |
|---|---|
min:n |
Number is at least n. |
max:n |
Number is at most n. |
between:min,max |
Number is within the inclusive range. |
length:n |
String is exactly n characters. |
min_length:n |
String is at least n characters. |
max_length:n |
String is at most n characters. |
min_count:n |
Array has at least n items. |
max_count:n |
Array has at most n items. |
min compares numbers, min_length counts characters with mb_strlen, andmin_count counts items.
| Rule | Passes when |
|---|---|
in:a,b,c |
Value is one of the options. |
not_in:a,b,c |
Value is none of the options. |
same:field |
Value equals the named field. |
different:field |
Value differs from the named field. |
in and not_in compare loosely, so a submitted '2' matches an option list
containing 2.
Shape-guarding rules such as required, integer, and array stop the field
when they fail, so dependent checks do not add a second confusing message.
'age' => 'required|integer|min:18'
For age => 'not a number', only Age must be an integer. is reported.
Messages use a readable field label and the rule's wording. email_address
becomes Email address; min_length:8 on name becomesName must be at least 8 characters.
Override a message per rule or per field/rule:
use Strukt\Rules\Messages;
use Strukt\Rules\Validator;
$validator = new Validator(null, new Messages([
'email.required' => 'We need an email address to reach you.',
'min_length' => ':field is too short, use at least :0 characters.',
]));
An override is used as written and may use :field, :0, :1, :list, and:format.
Implement RuleInterface; a rule receives the value and a FieldSource for
reading sibling fields.
use Strukt\Rules\Contract\FieldSource;
use Strukt\Rules\Contract\RuleInterface;
final class AfterRule implements RuleInterface
{
public function __construct(
private readonly string $field,
private readonly string $other,
) {
}
public function passes(mixed $value, FieldSource $fields): bool
{
$other = $fields->value($this->other);
return is_string($value) && is_string($other) && $value > $other;
}
public function message(): string
{
return ucfirst($this->field).' must be later than '.$this->other.'.';
}
public function bails(): bool
{
return false;
}
}
Register globally or pass a private registry:
use Strukt\Rules\RuleRegistry;
use Strukt\Rules\Validator;
RuleRegistry::shared()->extend(
'after',
fn (string $field, array $params) => new AfterRule($field, $params[0]),
);
$validator = new Validator(RuleRegistry::shared());
extend() accepts a rule instance or a factory called with the field name and
parsed parameters. Built-in names cannot be replaced. A misspelled rule name
throws Strukt\Rules\Exception\UnknownRule with the available rules.
The validator has no concept of a form. strukt/form is one consumer; any
rule-based engine can be built on the same foundations. Keep a private registry
when an engine should not share custom rules with the rest of the application.
$registry = RuleRegistry::create();
$registry->extend('port', fn (string $field, array $params) => new PortRule($field, $params));
$engine = new Validator($registry, new Messages([
'required' => ':field is required in the config.',
]));
$errors = $engine->validate($values, $definition);