Strukt

The application skeleton for the strukt4 package set. It ships almost
nothing of its own: a composer.json that pulls in
strukt/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.

Getting started

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/

Run it

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 facet

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') (or
Core::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.

Working in the monorepo

The skeleton resolves strukt/framework and its siblings through Composer
path 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.

Sessions

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.

Common commands

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.

Documentation

The framework documentation lives in ../framework/docs:
getting started,
routing,
forms and validation,
console,
configuration, and
extensions.

License

MIT. See LICENSE.

Section 1: Framework /home/pitsolu/Desktop/dev/php/strukt/strukt4/struktapp.github.io/bin/../docs/framework.md

Strukt Framework

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.

Install

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.

Quick start

use Strukt\Framework\Application;

$app = Application::make();
$app->register(new AuthModule());
$app->run();

handle() returns a ResponseInterface; run() sends it.

Application scaffolding

The framework publishes vendor/bin/strukt and a development-only
vendor/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.

Modules

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 and annotations

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.

Dependency injection

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.

Configuration

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

Bundles

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.

Console commands

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.

Helpers

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.

Section 2: Router /home/pitsolu/Desktop/dev/php/strukt/strukt4/struktapp.github.io/bin/../docs/router.md

Strukt Router

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.

Install

composer require strukt/router

Requires PHP 8.2+ and the strukt4 contract, form, cache, and event packages as
needed.

Quick start

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');

Routing

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');

Groups

$router->group('/api/v1', function (Router $router): void {
    $router->get('/users', fn () => 'users');
    $router->get('/users/{id:int}', fn (int $id) => "user[{$id}]");
});

Named routes

$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().

Parameters

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.

Middleware

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.

Authentication and permissions

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.

Forms

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);

Responses

An action may return a string (body), an array (JSON), a ResponseInterface, or
null.

$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 an
Allow header for a bad method, 422 for failed form validation, and 401/403 for
the auth gates.

Helpers

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.

HTTP layer

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.

Module prefixes

In a Strukt application, module route prefixes are resolved in this order:

  1. Module class property or method: public string $prefix = '/api/v1' or
    public function prefix(): string.
  2. Router folder Prefix.php class: prefix() method, $prefix property, or
    getPrefix().
  3. Class-level annotation: @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.

Section 3: Commons /home/pitsolu/Desktop/dev/php/strukt/strukt4/struktapp.github.io/bin/../docs/commons.md

Strukt Commons

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.

Install

composer require strukt/commons

Composer loads the Strukt\ namespace and the global helpers. Require the
normal autoloader before using the helpers.

Collections and dot paths

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

Value objects expose their raw value through yield(). Transformations return a
new wrapper so chains do not mutate the original.

DateTime

$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.

Today and date periods

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

String

$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().

Array

arr() returns a chainable wrapper implementing Countable and
IteratorAggregate.

$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', ...]

Token queries

$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 and messages

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);

JSON

$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 raises
JsonException.

Registry, configuration, and helpers

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.

Section 4: Console /home/pitsolu/Desktop/dev/php/strukt/strukt4/struktapp.github.io/bin/../docs/console.md

Strukt Console

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.

Install

composer require strukt/console

Create a console.php in the consuming project:

<?php

return [
    'commands' => __DIR__.'/src/Command',
    'docs' => __DIR__.'/docs/commands',
];

Defining a command

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

Command specification

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.

Runtime contract

interface CommandInterface
{
    public function execute(array $arguments, array $options): int;
}

Return 0 for success. A non-zero value becomes the process exit status.

Help, listing, and docs

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.

Design notes

Section 5: Filesystem /home/pitsolu/Desktop/dev/php/strukt/strukt4/struktapp.github.io/bin/../docs/fs.md

Strukt Fs

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.

Install

composer require strukt/fs

Requires PHP 8.2+ and strukt/base; ZIP operations require ext-zip.

Quick start

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);

Helpers

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:///...

Static Fs API

Strukt\Fs\Fs exposes the complete local filesystem API through static
methods.

Existence and file access

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.

Removal, permissions, and listing

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.

Path and platform helpers

Fs::isWindows();
Fs::dirSep($path);
Fs::ds($path);

Tailing and archives

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 into
topath/archive-name.

PHP file helpers

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.

Rooted Rfs API

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.

Phar helper

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.

Notes

Section 6: Cache /home/pitsolu/Desktop/dev/php/strukt/strukt4/struktapp.github.io/bin/../docs/cache.md

Strukt Cache

Typed cache storage with a process-local memory driver, JSON filesystem
persistence, and an optional Memcached backend behind one fluent API.

Install

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.

Quick start

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();

Drivers

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.

The cache helper

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.

Facade API

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

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.

Filesystem

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.

Memcached

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, and
memcached_password. The PHP memcached extension is only required when this
driver is instantiated.

Configuration

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.

Lifecycle

  1. Create or open a namespace.
  2. Read and mutate with exists(), get(), put(), and remove().
  3. Call save() for filesystem or Memcached storage.
  4. Reopen the namespace to confirm persisted values.

Memory changes are visible immediately; filesystem and Memcached changes remain
local to the driver until save().

Section 7: Base /home/pitsolu/Desktop/dev/php/strukt/strukt4/struktapp.github.io/bin/../docs/base.md

Strukt Base

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.

Install

composer require strukt/base

Requires PHP 8.2+, psr/container, and symfony/var-dumper. Composer loads
src/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();

Global helpers

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

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 throw
InvalidArgumentException.

Environment values

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.

Reflection

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 with
ref($object) or Ref::createFrom($object). Use Ref::resetCache() in
long-running workers when classes are reloaded.

Helper registry

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();

Container

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.

Errors and debugging

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);
Section 8: Active Record /home/pitsolu/Desktop/dev/php/strukt/strukt4/struktapp.github.io/bin/../docs/ar.md

Strukt Ar

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.

Install

composer require strukt/ar

Requires PHP 8.2+ and a PDO driver for your database.

Connections

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.

Connections from environment files

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 with
env(), 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).

Query builder

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 a
where() 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.

Active record

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();

Table names and keys

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, casts, and timestamps

fillable guards mass assignment through create(), fill(), and update().
Individual columns always accept direct assignment.

casts map values entering and leaving the database. Dates become
DateTimeImmutable 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;

Lifecycle

$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.

Relationships

Describe a relationship as a method delegating to hasMany(), hasOne(), or
belongsTo(). 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.

Generating models

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.

Creating the database from models

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 through
IF NOT EXISTS. Types map to SQLite storage classes; MySQL and PostgreSQL use
their native SQL types.

Managed entities

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.

Strukt Orm

/home/pitsolu/Desktop/dev/php/strukt/strukt4/struktapp.github.io/bin/../docs/orm.md
Section 9: Object Relational Mapper

Strukt Orm

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.

Install

composer require strukt/orm

Requires PHP 8.2+; strukt/ar is pulled in automatically.

Manager

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.

Identity map and unit of work

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

Repositories

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.

Working with both layers

Installing strukt/orm brings strukt/ar in as a dependency, so both layers use
the same models, tables, and connection. Three behaviours follow:

  1. 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.

  2. 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.

  3. Interleaved Ar writes ride the commit's transaction. An Ar save() between
    persist() 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 through
orm(), use strukt/ar directly for one-off or bulk work, and call
orm()->clear() after Ar writes that touch rows also tracked by the manager.
Do not save() or delete() entities you intend to commit().

Tests

The suite runs against an in-memory SQLite connection using the same schema as
strukt/ar and covers the identity map, unit of work, repositories, and result
cache.

Section 10: Key /home/pitsolu/Desktop/dev/php/strukt/strukt4/struktapp.github.io/bin/../docs/key.md

Strukt Key

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.

Install

composer require strukt/key

The package installs vendor/bin/kcli.

CLI

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"

Hashing

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

Symmetric encryption

use Strukt\Codec;

$codec = Codec::make('a-strong-key'); // fresh random IV per message
$wrapped = $codec->encode($plain);    // iv-hex:base64
$codec->decode($wrapped);

JWTs

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

CSRF

use Strukt\Csrf;

$token = Csrf::make(['user' => 42], 'csrf-key', 300)->encode();
Csrf::valid($token, 'csrf-key'); // true

Key pairs, signing, and ciphering

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'));

Envelopes and recipients

use Strukt\Ssl\Envelope;

[$envKeys, $sealed, $iv] = Envelope::withAlgo()
    ->useCerts([$certPath])
    ->closeWithIv($message);

Envelope::withAlgo($iv)
    ->usePrivKey($privateKey)
    ->open($envKeys[0], $sealed);

Certificates and CSRs

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();

Global helpers

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.

Notes

Section 11: Contract /home/pitsolu/Desktop/dev/php/strukt/strukt4/struktapp.github.io/bin/../docs/contract.md

Strukt Contract

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.

Install

composer require strukt/contract

Contracts

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:

Reference implementations

Strukt\User, Strukt\Session, and Strukt\Auth are dependency-free
implementations. They are the default wiring, not the only option.

User

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

Session

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

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.

Middleware

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.

HTTP, cache, and process contracts

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.

Section 12: Event /home/pitsolu/Desktop/dev/php/strukt/strukt4/struktapp.github.io/bin/../docs/event.md

Strukt Event

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.

Install

composer require strukt/event

Requires PHP 8.2+ and strukt/base. Composer loads src/helpers.php and
installs the global event() and cmd() helpers.

Global 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.

Event API

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.

Command API

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.

Loop

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.

Notes

Section 13: Form /home/pitsolu/Desktop/dev/php/strukt/strukt4/struktapp.github.io/bin/../docs/form.md

Strukt Form

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.

Install

composer require strukt/form

Requires PHP 8.2+ and strukt/rules.

Defining a form

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.

Submitting and validating

$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');  // []

Form API

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);

Messages

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.',
    ];
}

Form-specific custom rules

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',
        ];
    }
}

Helpers

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.

Integration

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);
Section 14: Generator /home/pitsolu/Desktop/dev/php/strukt/strukt4/struktapp.github.io/bin/../docs/generator.md

Strukt Generator

Typed PHP class generation, lightweight text templating, and PHPDoc annotation
utilities.

Install

composer require strukt/generator

Requires PHP 8.2+ and depends on strukt/base and strukt/commons.

Templating

$html = template(
    '<h1>{{title}}</h1>',
    ['title' => 'Dashboard'],
);

Template syntax:

Values

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().

Lists

{{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.

Conditions

{if enabled}
This section is visible.
{/if}

The section is trimmed when enabled and removed when the value is missing or
false.

Rendering order

Templator::create() applies directives in this order:

  1. list loops;
  2. direct values;
  3. conditions.

Templator API

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 and
color() helper are available.

DocBlocker

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.

BasicBlock

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.

ClassBuilder

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 throw
InvalidArgumentException.

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.

Annotation parser

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, and
methods keys. Repeated names are merged into one entry whose items list holds
every value.

Global helpers

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.

Section 15: Math /home/pitsolu/Desktop/dev/php/strukt/strukt4/struktapp.github.io/bin/../docs/math.md

Strukt Math

Immutable numeric and matrix primitives. The package keeps the existing
strukt4/math behaviour and adds aggregation, roots, deltas, vector products,
and explicit transformation pipelines.

Install

composer require strukt/math

Number

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

Arithmetic

$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

Aggregation and clamping

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.

Sign, rounding, and roots

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.

Comparison

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).

Matrix

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.

Multiplication

$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();

Vector products

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() throw
DivisionByZeroError.

Norms

Matrix::create([[3, 4]])->norm();    // 5, Euclidean
Matrix::create([[1, -2]])->norm(1);  // 3, sum of absolute values

Any other norm order throws InvalidArgumentException.

Transformations

Pipeline

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

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, and
join 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.

Section 16: Process /home/pitsolu/Desktop/dev/php/strukt/strukt4/struktapp.github.io/bin/../docs/process.md

Strukt Process

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.

Install

composer require strukt/process

Requires PHP 8.2+ and strukt/base.

Quick start

$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.

Helpers

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.

Running commands

One command

$result = process('php -r "echo 2 + 2;"');

echo $result->outputs(); // 4
$process = $result->resource();

Multiple commands

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.

Line callbacks

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.

ProcessResult

Strukt\ProcessResult extends ArrayIterator and may be iterated as a list of
Strukt\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() and
errors() return null when no entry exists and an empty string when the
command produced no output on that channel.

Error output and exit codes

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.

Switching channels

The default mapping is standard output to output() and standard error to
error().

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.

Interactive processes

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.

Process pool

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 throws
RuntimeException. 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.

Process supervisor

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:

supervise() drains output first, then revives dead slots. Introspection includes
restarts($index), totalRestarts, and stop().

Managed supervisor

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, or
STRUKT_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');

Supervisor CLI

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 API

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.

Resource lifecycle

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:

  1. Call write() as needed.
  2. Call closeInput() when no more input will be sent.
  3. Read output and error.
  4. Call close() or closeAll().

All process objects attempt safe cleanup in __destruct().

Section 17: Queue /home/pitsolu/Desktop/dev/php/strukt/strukt4/struktapp.github.io/bin/../docs/queue.md

Strukt Queue

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.

Requirements

Install

composer require strukt/queue

The package depends on strukt/base, strukt/console, and strukt/process.
The kqueue binary is installed into vendor/bin/.

Quick start

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);

Features

Queue API

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.

Helpers

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.

CLI

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.

Crash redelivery

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.

Operational notes

Credits

The original queue concept was contributed by Maurizio Giunti
(https://www.mauriziogiunti.it) under the MIT license.

Section 18: Owl /home/pitsolu/Desktop/dev/php/strukt/strukt4/struktapp.github.io/bin/../docs/owl.md

Strukt Owl

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.

Install

composer require strukt/owl

The package installs vendor/bin/owl.

CLI

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

Classifier

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.

PHP API

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

Design

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.

Credits

Section 19: Rules /home/pitsolu/Desktop/dev/php/strukt/strukt4/struktapp.github.io/bin/../docs/rules.md

Strukt Rules

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.

Install

composer require strukt/rules

Validating values

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.

Rules

Presence

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.

Types

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.

Formats

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}$',

Size

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, and
min_count counts items.

Sets and fields

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.

Bailing

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

Messages use a readable field label and the rule's wording. email_address
becomes Email address; min_length:8 on name becomes
Name 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.

Custom rules

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.

Building another engine

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);