# folk-builder

[![Vibe Index](https://img.shields.io/static/v1?label=Vibe+Index&message=4.4&color=2ea1cc&style=flat-square&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0iI2ZmZiI%2BPHBhdGggZD0iTTkgNCBROSAxMyAxOCAxMyBROSAxMyA5IDIyIFE5IDEzIDAgMTMgUTkgMTMgOSA0IFoiLz48cGF0aCBkPSJNMTkgMSBRMTkgNiAyNCA2IFExOSA2IDE5IDExIFExOSA2IDE0IDYgUTE5IDYgMTkgMSBaIi8%2BPHBhdGggZD0iTTIwIDE0IFEyMCAxOCAyNCAxOCBRMjAgMTggMjAgMjIgUTIwIDE4IDE2IDE4IFEyMCAxOCAyMCAxNCBaIi8%2BPC9zdmc%2B)](https://github.com/roxblnfk/action-vibe-index)

Собирает PHP-расширение `folk.so` из Rust-кода. Folk — это application server для PHP, аналог Swoole/RoadRunner/FrankenPHP, но реализованный как нативное PHP-расширение на Rust.

## Что это

`folk-builder` — CLI-утилита. Она генерирует Cargo-проект с выбранными плагинами и компилирует его в `folk.so` (cdylib). Результат — файл расширения, который PHP загружает через `extension=folk.so`.

## Установка folk-builder

### Через cargo (нужен Rust)

```bash
# Установить Rust если нет:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# Установить folk-builder:
cargo install folk-builder
```

### Из исходников

```bash
git clone https://github.com/Folk-Project/folk-builder
cd folk-builder
cargo build --release
./target/release/folk-builder --help
```

## Требования для сборки folk.so

- **Rust 1.88+** — `rustup update`
- **PHP 8.2+** с `php-config` в PATH
- **clang/libclang** — для bindgen (генерация FFI-биндингов к PHP)
- **pkg-config** — для поиска PHP headers

На Ubuntu/Debian:
```bash
apt install php8.4-dev pkg-config libclang-dev clang
```

На macOS (Homebrew):
```bash
brew install php@8.4
```

### Многопоточность (ZTS)

Для запуска нескольких worker-потоков (`count > 1` в folk.toml) PHP должен быть собран с Thread Safety (ZTS):

- **Docker**: используйте образ `php:8.4-zts` — ZTS уже включён
- **Ubuntu**: `apt install php8.4-zts` (если есть в репо)
- **Из исходников**: `./configure --enable-zts && make && make install`
- **Проверить**: `php -r "echo PHP_ZTS;"` — должно вывести `1`

С NTS (обычным) PHP Folk работает, но только с одним worker-потоком.

## Сборка расширения

### 1. Создать конфиг сборки

Файл `folk.build.toml`:

```toml
[build]
output = "folk"                    # Имя расширения (-> folk.so)

[[plugin]]
crate_name = "folk-plugin-http"    # HTTP-сервер (axum + hyper + tokio)
version = "0.2"
config_key = "http"
```

### 2. Собрать

```bash
folk-builder build --config folk.build.toml --output-dir .
```

Результат: файл `folk.so` в текущей директории.

### 3. Проверить

```bash
php -d extension=./folk.so -r "echo folk_version();"
# folk-ext 0.2.0
```

## Запуск Folk-сервера

### Конфиг сервера

Файл `folk.toml`:

```toml
[workers]
script = "server.php"    # PHP-скрипт воркера
count = 4                # Кол-во worker-потоков (нужен ZTS)
max_jobs = 100000        # Рестарт после N запросов

[http]
listen = "0.0.0.0:8080"

[log]
filter = "warn"          # trace, debug, info, warn, error
```

### Простой HTTP-сервер (без фреймворка)

```php
<?php
// server.php
require __DIR__ . '/vendor/autoload.php';

use Folk\Sdk\Worker\WorkerLoop;
use Folk\Sdk\Http\HttpModeHandler;
use Folk\Sdk\Http\HttpRequest;
use Folk\Sdk\Http\HttpResponse;

class MyHandler implements HttpModeHandler
{
    public function handle(HttpRequest $request): HttpResponse
    {
        return new HttpResponse(
            status: 200,
            headers: ['Content-Type' => 'application/json'],
            body: json_encode(['status' => 'ok', 'uri' => $request->uri]),
        );
    }
}

// ZTS worker threads уже инициализированы — пропускаем start().
if (!folk_is_worker_thread()) {
    $server = new Folk\Server('folk.toml');
    $server->start();
}

$loop = new WorkerLoop();
$loop->registerHttpHandler(new MyHandler());
$loop->run();
```

Запуск:

```bash
composer require folk/sdk
php -d extension=./folk.so server.php
```

### С Laravel

```bash
composer require folk/laravel
```

Файл `server.php`:

```php
<?php
require __DIR__ . '/vendor/autoload.php';

if (!folk_is_worker_thread()) {
    $server = new Folk\Server('folk.toml');
    $server->start();
}

// Laravel bootstrap.
$app = require __DIR__ . '/bootstrap/app.php';
$app->make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap();

// WorkerLoop с Laravel handler (регистрируется через ServiceProvider).
$loop = new \Folk\Sdk\Worker\WorkerLoop();
if (isset($GLOBALS['folk_worker_boot_hook'])) {
    ($GLOBALS['folk_worker_boot_hook'])($loop);
}
$loop->run();
```

```bash
php -d extension=./folk.so server.php
# Сервер слушает на :8080
```

## Docker (рекомендуемый способ)

Не нужно ставить Rust и PHP ZTS локально — всё собирается в Docker.

### Продакшен (всё в образе)

```dockerfile
# === Сборка folk.so ===
FROM php:8.4-zts AS builder

# Rust
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
    | sh -s -- -y --default-toolchain 1.88.0
ENV PATH="/root/.cargo/bin:${PATH}"

# Системные зависимости для сборки
RUN apt-get update && apt-get install -y \
    pkg-config libclang-dev clang protobuf-compiler \
    && rm -rf /var/lib/apt/lists/*

# Собираем folk-builder и folk.so
WORKDIR /build
RUN cargo install folk-builder

COPY folk.build.toml folk.build.toml
RUN folk-builder build --config folk.build.toml --output-dir /build/

# === Runtime ===
FROM php:8.4-zts

RUN apt-get update && apt-get install -y \
    unzip curl sqlite3 \
    && pecl install redis \
    && docker-php-ext-enable redis \
    && rm -rf /var/lib/apt/lists/*

# folk.so
COPY --from=builder /build/folk.so /usr/local/lib/php/extensions/folk.so
RUN echo "extension=/usr/local/lib/php/extensions/folk.so" > /usr/local/etc/php/conf.d/folk.ini

# Приложение
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
WORKDIR /app
COPY . .
RUN composer install --no-dev --optimize-autoloader

EXPOSE 8080
CMD ["php", "server.php"]
```

### Локальная разработка (код через volume)

Для разработки код не копируется в образ — монтируется через volume. Образ содержит только runtime + folk.so.

**`docker/app.Dockerfile`:**

```dockerfile
# Stage 1: Build folk.so
FROM php:8.4-zts AS builder

RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain 1.88.0
ENV PATH="/root/.cargo/bin:${PATH}"

RUN apt-get update && apt-get install -y \
    pkg-config libclang-dev clang protobuf-compiler \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /build
RUN cargo install folk-builder

COPY folk.build.toml folk.build.toml
RUN folk-builder build --config folk.build.toml --output-dir /build/

# Stage 2: PHP ZTS runtime
FROM php:8.4-zts

RUN apt-get update && apt-get install -y \
    unzip curl sqlite3 \
    && pecl install redis \
    && docker-php-ext-enable redis \
    && rm -rf /var/lib/apt/lists/*

COPY --from=composer:2 /usr/bin/composer /usr/bin/composer

COPY --from=builder /build/folk.so /usr/local/lib/php/extensions/folk.so
RUN echo "extension=/usr/local/lib/php/extensions/folk.so" > /usr/local/etc/php/conf.d/folk.ini

WORKDIR /app
EXPOSE 8080
CMD ["php", "server.php"]
```

**`compose.yaml`:**

```yaml
services:
  app:
    build:
      context: .
      dockerfile: docker/app.Dockerfile
    ports:
      - "8080:8080"
      - "9090:9090"
    working_dir: /app
    environment:
      - APP_ENV=local
      - APP_DEBUG=true
      - DB_CONNECTION=sqlite
      - REDIS_HOST=redis
      - CACHE_STORE=redis
      - SESSION_DRIVER=redis
      - QUEUE_CONNECTION=redis
      - RUST_LOG=info
    volumes:
      - ./:/app
    depends_on:
      - redis

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
```

**Запуск:**

```bash
# Собрать образ (folk.so компилируется один раз)
docker compose build

# Установить PHP-зависимости (выполняется внутри контейнера)
docker compose run --rm app composer require folk/laravel

# Запустить
docker compose up
```

Код приложения (`app/`, `routes/`, `config/`, `resources/`) монтируется через volume — изменения применяются при рестарте воркера (по `max_jobs` или `docker compose restart app`).

**`folk.build.toml`** (все плагины):

```toml
[build]
output = "folk"

[[plugin]]
crate_name = "folk-plugin-http"
version = "0.2"
config_key = "http"

[[plugin]]
crate_name = "folk-plugin-jobs"
version = "0.2"
config_key = "jobs"

[[plugin]]
crate_name = "folk-plugin-grpc"
version = "0.2"
config_key = "grpc"

[[plugin]]
crate_name = "folk-plugin-metrics"
version = "0.2"
config_key = "metrics"

[[plugin]]
crate_name = "folk-plugin-process"
version = "0.2"
config_key = "process"
```

**`folk.toml`:**

```toml
[server]
shutdown_timeout = "10s"

[workers]
script = "server.php"
count = 4
max_jobs = 100000

[http]
listen = "0.0.0.0:8080"

[jobs]
driver = "memory"

[[jobs.queues]]
name = "default"
concurrency = 2

[metrics]
listen = "0.0.0.0:9090"

[log]
filter = "info"
format = "text"
```

## Конфиг сборки — справочник

### `[build]`

| Поле | Тип | Обязательно | Описание |
|------|-----|-------------|----------|
| `output` | string | да | Имя расширения (без `.so`) |
| `folk_ext_path` | string | нет | Путь к локальному `folk-ext` (для разработки) |
| `folk_api_path` | string | нет | Путь к локальному `folk-api` (для разработки) |

### `[[plugin]]`

| Поле | Тип | Обязательно | Описание |
|------|-----|-------------|----------|
| `crate_name` | string | да | Имя Rust-крейта |
| `version` | string | нет | Версия с crates.io (по умолчанию `"0.2"`) |
| `path` | string | нет | Локальный путь (вместо crates.io) |
| `git` | string | нет | Git URL (вместо crates.io) |
| `config_key` | string | да | Ключ секции в `folk.toml` |

### Доступные плагины

| Плагин | Крейт | Версия | Описание |
|--------|-------|--------|----------|
| HTTP | `folk-plugin-http` | 0.2 | HTTP/1.1 сервер (axum) |
| gRPC | `folk-plugin-grpc` | 0.2 | gRPC сервер (tonic) |
| Jobs | `folk-plugin-jobs` | 0.2 | Очередь фоновых задач (memory/redis) |
| Metrics | `folk-plugin-metrics` | 0.2 | Prometheus `/metrics` и `/health` |
| Process | `folk-plugin-process` | 0.2 | Supervisor сайдкар-процессов |

## Как это работает

```
PHP загружает folk.so
  |
  +-- Main thread (worker #1)
  |     Rust вызывает PHP handler напрямую через call_user_function
  |     Данные передаются как PHP-массивы (zval), без JSON
  |
  +-- folk-tokio thread
  |     axum HTTP-сервер, пул воркеров, реестр плагинов
  |
  +-- ZTS workers #2..N (если count > 1 и PHP собран с ZTS)
        Каждый — отдельный PHP-контекст со своим Zend Engine
```

## Производительность

Docker, Mac (OrbStack), 4 ZTS workers:

| Нагрузка | Req/sec |
|----------|---------|
| Raw JSON (`{"status":"ok"}`) | ~55,000 |
| Laravel Livewire starter | ~4,200 |

Стабильно держит 500 concurrent connections без socket errors.

## Лицензия

MIT
