INDEX

📅 2026/3/13 ✍️ Bullsoft

VSlim

Documentation entry:

VSlim 是一个运行在 vphp 之上的 PHP 微框架扩展,核心目标是给 PHP 用户提供熟悉的 runtime builder 体验:

  • VSlim\App 负责路由注册、分发、middleware、hooks
  • VSlim\Request / VSlim\Response 提供轻量 request/response facade
  • VSlim\Stream\Response 提供扩展原生流式响应类型
  • VSlim\WebSocket\App 提供扩展原生 WebSocket handler
  • VSlim\Mcp\App 提供扩展原生 MCP handler
  • VSlim\View / VSlim\Controller 提供简单 MVC 能力
  • VSlim\Container / VSlim\Config 提供基础依赖注入与 TOML 配置
  • vslim_handle_request(...) / dispatch_envelope(...) 提供和 worker / vhttpd 的集成边界

这份 README 只做两件事:

  1. 给一个最简单、能跑通的 tutorial
  2. 作为整个 docs/ 的索引页

注意:本文档以代码和测试为准,旧文档只作为历史参考。

1 分钟认识 VSlim

最小可用对象是 VSlim\App。你注册路由,然后直接 dispatch:

<?php

$app = new VSlim\App();

$app->get('/hello/:name', function (VSlim\Request $req) {
    return new VSlim\Response(
        200,
        'Hello, ' . $req->param('name'),
        'text/plain; charset=utf-8'
    );
});

$res = $app->dispatch('GET', '/hello/codex');

echo $res->status . PHP_EOL;
echo $res->body . PHP_EOL;

输出:

200
Hello, codex

性能

在MacBook Air M2 + 4 个 PHP 进程的条件下,用 K6 压测,详情如下

核心指标

指标数值评价
吞吐量5,019 req/s优秀
平均响应时间3.1ms非常快
P95 响应时间5.65ms很稳定
最大响应时间29.1ms无尖刺
错误率0%完美
并发用户30 VUs中等压力

总体评价

这个成绩在 MacBook Air M2 + 4 个 PHP 进程的条件下非常亮眼:

  • 5000+ req/s 对于 PHP 来说属于高性能范畴,说明你的框架/服务本身开销很低
  • 3ms 平均延迟极低,结合 0 错误率,说明系统完全没有过载
  • P95 仅 5.65ms,延迟分布非常集中,没有长尾问题
  • 627,497 次 checks 全部通过,业务逻辑正确性 100%
k6 run k6_demo.js

         /\      Grafana   /‾‾/  
    /\  /  \     |\  __   /  /   
   /  \/    \    | |/ /  /   ‾‾\ 
  /          \   |   (  |  ()  |
 / __________ \  |_|\_\  \_____/ 


     execution: local
        script: k6_demo.js
        output: -

     scenarios: (100.00%) 1 scenario, 30 max VUs, 55s max duration (incl. graceful stop):
              * mixed_routes: Up to 30 looping VUs for 50s over 3 stages (gracefulRampDown: 5s, gracefulStop: 30s)



 THRESHOLDS 

    checks
 'rate>0.99' rate=100.00%

    http_req_duration
 'p(95)<800' p(95)=5.65ms
 'p(99)<1500' p(99)=8.61ms

    http_req_failed
 'rate<0.01' rate=0.00%

    server_error_rate
 'rate<0.005' rate=0.00%


 TOTAL RESULTS 

    checks_total.......: 627497  12549.903605/s
    checks_succeeded...: 100.00% 627497 out of 627497
    checks_failed......: 0.00%   0 out of 627497

 api users no 5xx
 api users status 200
 api users json
 unauthorized status 401
 hello status 200
 hello has body
 hello request-id header
 health status 200
 health body ok
 forms status 200
 forms json ok

    CUSTOM
    server_error_rate..............: 0.00%  0 out of 250976

    HTTP
    http_req_duration..............: avg=3.1ms  min=130µs    med=3.05ms max=29.1ms  p(90)=5.15ms p(95)=5.65ms
      { expected_response:true }...: avg=3.1ms  min=130µs    med=3.05ms max=29.1ms  p(90)=5.15ms p(95)=5.65ms
    http_req_failed................: 0.00%  0 out of 250976
    http_reqs......................: 250976 5019.505443/s

    EXECUTION
    iteration_duration.............: avg=3.12ms min=145.12µs med=3.08ms max=29.13ms p(90)=5.17ms p(95)=5.67ms
    iterations.....................: 250976 5019.505443/s
    vus............................: 1      min=0           max=29
    vus_max........................: 30     min=30          max=30

    NETWORK
    data_received..................: 52 MB  1.0 MB/s
    data_sent......................: 24 MB  479 kB/s




running (50.0s), 00/30 VUs, 250976 complete and 0 interrupted iterations
mixed_routes [======================================] 00/30 VUs  50s

Tutorials

Tutorial 1:最简单的路由

<?php

$app = new VSlim\App();

$app->get('/ping', function () {
    return 'pong';
});

$res = $app->dispatch('GET', '/ping');
echo $res->status . PHP_EOL; // 200
echo $res->body . PHP_EOL;   // pong

这里可以先记住 3 个事实:

  • handler 可以返回 string
  • string 会被自动归一化成 200 text/plain
  • dispatch() 直接返回 VSlim\Response

Tutorial 2:读取路径参数、query 和 body

<?php

$app = new VSlim\App();

$app->post('/users/:id', function (VSlim\Request $req) {
    return [
        'status' => 200,
        'content_type' => 'application/json; charset=utf-8',
        'body' => json_encode([
            'id' => $req->param('id'),
            'trace' => $req->query('trace_id'),
            'name' => $req->input('name'),
        ], JSON_UNESCAPED_UNICODE),
    ];
});

$res = $app->dispatch_body(
    'POST',
    '/users/7?trace_id=demo',
    'name=neo'
);

echo $res->body . PHP_EOL;

这里用到了:

  • param() 读取路由参数
  • query() 读取 query string
  • input() 读取“query + parsed body 合并后的输入”
  • handler 也可以返回数组,VSlim 会归一化成 Response

Tutorial 3:middleware、before、after

<?php

$app = new VSlim\App();

$app->before(function (VSlim\Request $req) {
    if ($req->path === '/healthz') {
        return 'ok-from-before';
    }
    return null;
});

$app->middleware(function (VSlim\Request $req, callable $next) {
    if ($req->query('token') !== 'demo') {
        return new VSlim\Response(403, 'forbidden', 'text/plain; charset=utf-8');
    }
    return $next($req);
});

$app->after(function (VSlim\Request $req, VSlim\Response $res) {
    $res->set_header('x-demo', 'yes');
    return $res;
});

$app->get('/hello/:name', function (VSlim\Request $req) {
    return 'hello:' . $req->param('name');
});

$res = $app->dispatch('GET', '/hello/codex?token=demo');
echo $res->status . PHP_EOL;
echo $res->body . PHP_EOL;
echo $res->header('x-demo') . PHP_EOL;

行为规则:

  • before() 返回非空值时会短路,不再进入 route
  • middleware() 必须返回一个有效响应,或者调用 $next($req)
  • after() 可以原地修改 Response,也可以返回一个新的 Response

Tutorial 4:group 和命名路由

<?php

$app = new VSlim\App();
$app->set_base_path('/demo');

$api = $app->group('/api');

$api->get_named('users.show', '/users/:id', function (VSlim\Request $req) {
    return 'user:' . $req->param('id');
});

echo $app->url_for('users.show', ['id' => '42']) . PHP_EOL;
echo $app->url_for_abs('users.show', ['id' => '42'], 'https', 'example.local') . PHP_EOL;

$redirect = $app->redirect_to('users.show', ['id' => '42']);
echo $redirect->status . PHP_EOL;
echo $redirect->header('location') . PHP_EOL;

输出类似:

/demo/api/users/42
https://example.local/demo/api/users/42
302
/demo/api/users/42

说明:

  • set_base_path() 会影响 url_for*() 生成的 URL
  • redirect_to() 使用路由名生成 location
  • group('/api') 会把该组下的路由统一加前缀

Tutorial 5:resource / singleton

如果你想快速得到 REST 风格路由,可以直接注册 controller:

<?php

final class UserController
{
    public function index(VSlim\Request $req): string { return 'index'; }
    public function show(VSlim\Request $req): string { return 'show:' . $req->param('id'); }
    public function store(VSlim\Request $req): string { return 'store'; }
    public function update(VSlim\Request $req): string { return 'update:' . $req->param('id'); }
    public function destroy(VSlim\Request $req): string { return 'destroy:' . $req->param('id'); }
    public function create(VSlim\Request $req): string { return 'create'; }
    public function edit(VSlim\Request $req): string { return 'edit:' . $req->param('id'); }
}

$app = new VSlim\App();
$app->container()->set(UserController::class, new UserController());
$app->resource('/users', UserController::class);

这会注册:

  • GET /users -> index
  • GET /users/create -> create
  • POST /users -> store
  • GET /users/:id -> show
  • GET /users/:id/edit -> edit
  • PUT/PATCH /users/:id -> update
  • DELETE /users/:id -> destroy

如果你不需要 create/edit 这种页面路由,使用 api_resource()

Tutorial 6:最简单的 View

<?php

$app = new VSlim\App();
$app->set_view_base_path(__DIR__ . '/views');
$app->set_assets_prefix('/assets');

$res = $app->view('home.html', [
    'title' => 'VSlim Demo',
    'name' => 'neo',
]);

echo $res->content_type . PHP_EOL;
echo $res->body . PHP_EOL;

模板里可以直接用:

<h1>{{ title }}</h1>
<p>{{ name }}</p>
<script src="{{asset:app.js}}"></script>

Tutorial 7:worker / envelope 集成

当 VSlim 运行在 PHP worker 后面时,可以直接消费 envelope:

<?php

$app = new VSlim\App();
$app->get('/hello/:name', function (VSlim\Request $req) {
    return 'hello:' . $req->param('name');
});

$res = $app->dispatch_envelope([
    'method' => 'GET',
    'path' => '/hello/codex?trace_id=demo',
    'body' => '',
    'scheme' => 'https',
    'host' => 'example.local',
    'headers' => [
        'x-request-id' => 'req-1',
    ],
]);

echo $res->status . PHP_EOL;
echo $res->body . PHP_EOL;
echo $res->header('x-request-id') . PHP_EOL;

如果你更想要简单 map,可以使用:

$map = $app->dispatch_envelope_map($envelope);

Tutorial 8:最简单的流式响应

<?php

$app = new VSlim\App();

$app->get('/stream/text', function () {
    return VSlim\Stream\Response::text((function (): iterable {
        yield "hello\n";
        yield "stream\n";
    })());
});

$app->get('/stream/sse', function () {
    return VSlim\Stream\Response::sse((function (): iterable {
        yield ['event' => 'token', 'data' => '{"token":"hello"}'];
        yield ['event' => 'done', 'data' => '{"done":true}'];
    })());
});

如果你要接 Ollama,则直接:

<?php

$app->map(['GET', 'POST'], '/ollama/text', function (VSlim\Request $req) {
    return VSlim\Stream\Factory::ollama_text($req);
});

$app->map(['GET', 'POST'], '/ollama/sse', function (VSlim\Request $req) {
    return VSlim\Stream\Factory::ollama_sse($req);
});

Tutorial 9:原生 MCP handler

<?php

$app = new VSlim\App();
$app->get('/', static fn () => ['name' => 'vslim-native-mcp-demo']);

$app->mcp()
    ->server_info(['name' => 'vslim-native-mcp-demo', 'version' => '0.1.0'])
    ->capabilities([
        'logging' => [],
        'sampling' => [],
    ])
    ->tool(
        'echo',
        'Echo text',
        [
            'type' => 'object',
            'properties' => [
                'text' => ['type' => 'string'],
            ],
            'required' => ['text'],
        ],
        static function (array $arguments): array {
            return [
                'content' => [
                    ['type' => 'text', 'text' => (string) ($arguments['text'] ?? '')],
                ],
                'isError' => false,
            ];
        }
    );

return $app;

这里的关键点是:

  • MCP 不强绑进 VSlim\App
  • vslim.so 已经提供了原生 VSlim\Mcp\App
  • 直接用 $app->mcp() 就能把 MCP handler 挂到 VSlim\App
  • bootstrap 现在直接返回一个 $app 即可

文档索引

核心组件

集成与运行时

现成示例

组件总览

flowchart LR
    A["VSlim\\App"] --> B["VSlim\\Request"]
    A --> C["VSlim\\Response"]
    A --> D["VSlim\\RouteGroup"]
    A --> E["VSlim\\Container"]
    A --> F["VSlim\\Config"]
    A --> G["VSlim\\View"]
    H["VSlim\\Controller"] --> A
    H --> G
    I["Worker / vhttpd / PSR-7"] --> A

真理之源

如果文档和实现不一致,以这些文件为准: