[
  {
    "path": ".clang-format-ignore",
    "content": "frankenphp_arginfo.h\n"
  },
  {
    "path": ".codespellrc",
    "content": "[codespell]\ncheck-hidden =\nskip = .git,docs/*/*,docs,*/go.mod,*/go.sum,./internal/phpheaders/phpheaders.go\n"
  },
  {
    "path": ".dockerignore",
    "content": "/caddy/frankenphp/frankenphp\n/internal/testserver/testserver\n/internal/testcli/testcli\n/dist\n.DS_Store\n.idea/\n.vscode/\n__debug_bin\nfrankenphp.test\ncaddy/frankenphp/Build\n*.log\n"
  },
  {
    "path": ".editorconfig",
    "content": "root = true\n\n[*]\nend_of_line = lf\ninsert_final_newline = true\n\n[*.{sh,Dockerfile}]\nindent_style = tab\ntab_width = 4\n\n[*.{yaml,yml}]\nindent_style = space\ntab_width = 2\n"
  },
  {
    "path": ".gitattributes",
    "content": "* text=auto eol=lf\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yaml",
    "content": "---\nname: Bug Report\ndescription: File a bug report\nlabels: [bug]\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Thanks for taking the time to fill out this bug report!\n\n        Before submitting, please ensure that your issue:\n\n        * Is not [a known issue](https://frankenphp.dev/docs/known-issues/).\n        * Has not [already been reported](https://github.com/php/frankenphp/issues).\n        * Is not caused by a dependency (like Caddy or PHP itself). If the issue is with a dependency, please report it to the upstream project directly.\n  - type: textarea\n    id: what-happened\n    attributes:\n      label: What happened?\n      description: |\n        Tell us what you do, what you get, and what you expected.\n        Provide us with some step-by-step instructions to reproduce the issue.\n    validations:\n      required: true\n  - type: dropdown\n    id: build\n    attributes:\n      label: Build Type\n      description: What build of FrankenPHP do you use?\n      options:\n        - Docker (Debian Trixie)\n        - Docker (Debian Bookworm)\n        - Docker (Alpine)\n        - apk packages\n        - deb packages\n        - RPM packages\n        - Static binary\n        - Custom (tell us more in the description)\n      default: 0\n    validations:\n      required: true\n  - type: dropdown\n    id: worker\n    attributes:\n      label: Worker Mode\n      description: Does the problem happen only when using the worker mode?\n      options:\n        - \"Yes\"\n        - \"No\"\n      default: 0\n    validations:\n      required: true\n  - type: dropdown\n    id: os\n    attributes:\n      label: Operating System\n      description: What operating system are you executing FrankenPHP with?\n      options:\n        - GNU/Linux\n        - macOS\n        - Windows\n        - FreeBSD\n        - Other (tell us more in the description)\n      default: 0\n    validations:\n      required: true\n  - type: dropdown\n    id: arch\n    attributes:\n      label: CPU Architecture\n      description: What CPU architecture are you using?\n      options:\n        - x86_64\n        - Apple Silicon\n        - x86\n        - aarch64\n        - Other (tell us more in the description)\n      default: 0\n  - type: textarea\n    id: php\n    attributes:\n      label: PHP configuration\n      description: |\n        Please copy and paste the output of the `phpinfo()` function -- remember to remove **sensitive information** like passwords, API keys, etc.\n      render: shell\n    validations:\n      required: true\n  - type: textarea\n    id: logs\n    attributes:\n      label: Relevant log output\n      description: |\n        Please copy and paste any relevant log output.\n        This will be automatically formatted into code,\n        so no need for backticks.\n      render: shell\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.yaml",
    "content": "---\nname: Feature Request\ndescription: Suggest an idea for this project\nlabels: [enhancement]\nbody:\n  - type: textarea\n    id: description\n    attributes:\n      label: Describe your feature request\n      value: |\n        **Is your feature request related to a problem? Please describe.**\n        A clear and concise description of what the problem is.\n        Ex. I'm always frustrated when [...]\n\n        **Describe the solution you'd like**\n        A clear and concise description of what you want to happen.\n\n        **Describe alternatives you've considered**\n        A clear and concise description of any alternative solutions\n        or features you've considered.\n"
  },
  {
    "path": ".github/actions/watcher/action.yaml",
    "content": "name: watcher\ndescription: Install e-dant/watcher\nruns:\n  using: composite\n  steps:\n    - name: Determine e-dant/watcher version\n      id: determine-watcher-version\n      run: echo version=\"$(gh release view --repo e-dant/watcher --json tagName --template '{{ .tagName }}')\" >> \"${GITHUB_OUTPUT}\"\n      shell: bash\n      env:\n        GH_TOKEN: ${{ github.token }}\n    - name: Cache e-dant/watcher\n      id: cache-watcher\n      uses: actions/cache@v4\n      with:\n        path: watcher/target\n        key: watcher-${{ runner.os }}-${{ runner.arch }}-${{ steps.determine-watcher-version.outputs.version }}-${{ env.CC && env.CC || 'gcc' }}\n    - if: steps.cache-watcher.outputs.cache-hit != 'true'\n      name: Compile e-dant/watcher\n      run: |\n        mkdir watcher\n        gh release download --repo e-dant/watcher -A tar.gz -O - | tar -xz -C watcher --strip-components 1\n        cd watcher\n        cmake -S . -B build -DCMAKE_BUILD_TYPE=Release\n        cmake --build build\n        sudo cmake --install build --prefix target\n      shell: bash\n      env:\n        GH_TOKEN: ${{ github.token }}\n    - name: Update LD_LIBRARY_PATH\n      run: |\n        sudo sh -c \"echo ${PWD}/watcher/target/lib > /etc/ld.so.conf.d/watcher.conf\"\n        sudo ldconfig\n      shell: bash\n"
  },
  {
    "path": ".github/dependabot.yaml",
    "content": "---\nversion: 2\nupdates:\n  - package-ecosystem: gomod\n    directory: /\n    schedule:\n      interval: weekly\n    commit-message:\n      prefix: chore\n    groups:\n      go-modules:\n        patterns:\n          - \"*\"\n    cooldown:\n      default-days: 7\n  - package-ecosystem: gomod\n    directory: /caddy\n    schedule:\n      interval: weekly\n    commit-message:\n      prefix: chore(caddy)\n    groups:\n      go-modules:\n        patterns:\n          - \"*\"\n    cooldown:\n      default-days: 7\n  - package-ecosystem: github-actions\n    directory: /\n    schedule:\n      interval: weekly\n    commit-message:\n      prefix: ci\n    groups:\n      github-actions:\n        patterns:\n          - \"*\"\n    cooldown:\n      default-days: 7\n"
  },
  {
    "path": ".github/scripts/docker-compute-fingerprints.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\nwrite_output() {\n\tif [[ -n \"${GITHUB_OUTPUT:-}\" ]]; then\n\t\techo \"$1\" >>\"${GITHUB_OUTPUT}\"\n\telse\n\t\techo \"$1\"\n\tfi\n}\n\nget_php_version() {\n\tlocal version=\"$1\"\n\tskopeo inspect \"docker://docker.io/library/php:${version}\" \\\n\t\t--override-os linux \\\n\t\t--override-arch amd64 |\n\t\tjq -r '.Env[] | select(test(\"^PHP_VERSION=\")) | sub(\"^PHP_VERSION=\"; \"\")'\n}\n\nPHP_82_LATEST=\"$(get_php_version 8.2)\"\nPHP_83_LATEST=\"$(get_php_version 8.3)\"\nPHP_84_LATEST=\"$(get_php_version 8.4)\"\nPHP_85_LATEST=\"$(get_php_version 8.5)\"\n\nPHP_VERSION=\"${PHP_82_LATEST},${PHP_83_LATEST},${PHP_84_LATEST},${PHP_85_LATEST}\"\nwrite_output \"php_version=${PHP_VERSION}\"\nwrite_output \"php82_version=${PHP_82_LATEST//./-}\"\nwrite_output \"php83_version=${PHP_83_LATEST//./-}\"\nwrite_output \"php84_version=${PHP_84_LATEST//./-}\"\nwrite_output \"php85_version=${PHP_85_LATEST//./-}\"\n\nif [[ \"${GITHUB_EVENT_NAME:-}\" == \"schedule\" ]]; then\n\tFRANKENPHP_LATEST_TAG=\"$(gh release view --repo php/frankenphp --json tagName --jq '.tagName')\"\n\tgit checkout \"${FRANKENPHP_LATEST_TAG}\"\nfi\n\nMETADATA=\"$(PHP_VERSION=\"${PHP_VERSION}\" docker buildx bake --print | jq -c)\"\n\nBASE_IMAGES=()\nwhile IFS= read -r image; do\n\tBASE_IMAGES+=(\"${image}\")\ndone < <(jq -r '\n\t.target[]?.contexts? | to_entries[]?\n\t| select(.value | startswith(\"docker-image://\"))\n\t| .value\n\t| sub(\"^docker-image://\"; \"\")\n' <<<\"${METADATA}\" | sort -u)\n\nBASE_IMAGE_DIGESTS=()\nfor image in \"${BASE_IMAGES[@]}\"; do\n\tif [[ \"${image}\" == */* ]]; then\n\t\tref=\"docker://docker.io/${image}\"\n\telse\n\t\tref=\"docker://docker.io/library/${image}\"\n\tfi\n\tdigest=\"$(skopeo inspect \"${ref}\" \\\n\t\t--override-os linux \\\n\t\t--override-arch amd64 \\\n\t\t--format '{{.Digest}}')\"\n\tBASE_IMAGE_DIGESTS+=(\"${image}@${digest}\")\ndone\n\nBASE_FINGERPRINT=\"$(printf '%s\\n' \"${BASE_IMAGE_DIGESTS[@]}\" | sort | sha256sum | awk '{print $1}')\"\nwrite_output \"base_fingerprint=${BASE_FINGERPRINT}\"\n\nif [[ \"${GITHUB_EVENT_NAME:-}\" != \"schedule\" ]]; then\n\twrite_output \"skip=false\"\n\texit 0\nfi\n\nFRANKENPHP_LATEST_TAG_NO_PREFIX=\"${FRANKENPHP_LATEST_TAG#v}\"\nEXISTING_FINGERPRINT=$(\n\tskopeo inspect \"docker://docker.io/dunglas/frankenphp:${FRANKENPHP_LATEST_TAG_NO_PREFIX}\" \\\n\t\t--override-os linux \\\n\t\t--override-arch amd64 |\n\t\tjq -r '.Labels[\"dev.frankenphp.base.fingerprint\"] // empty'\n)\n\nif [[ -n \"${EXISTING_FINGERPRINT}\" ]] && [[ \"${EXISTING_FINGERPRINT}\" == \"${BASE_FINGERPRINT}\" ]]; then\n\twrite_output \"skip=true\"\n\texit 0\nfi\n\nwrite_output \"ref=${FRANKENPHP_LATEST_TAG}\"\nwrite_output \"skip=false\"\n"
  },
  {
    "path": ".github/scripts/docker-verify-fingerprints.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\nPHP_VERSION=\"${PHP_VERSION:-}\"\nGO_VERSION=\"${GO_VERSION:-}\"\nUSE_LATEST_PHP=\"${USE_LATEST_PHP:-0}\"\n\nif [[ -z \"${GO_VERSION}\" ]]; then\n\tGO_VERSION=\"$(awk -F'\"' '/variable \"GO_VERSION\"/ {f=1} f && /default/ {print $2; exit}' docker-bake.hcl)\"\n\tGO_VERSION=\"${GO_VERSION:-1.26}\"\nfi\n\nif [[ -z \"${PHP_VERSION}\" ]]; then\n\tPHP_VERSION=\"$(awk -F'\"' '/variable \"PHP_VERSION\"/ {f=1} f && /default/ {print $2; exit}' docker-bake.hcl)\"\n\tPHP_VERSION=\"${PHP_VERSION:-8.2,8.3,8.4,8.5}\"\nfi\n\nif [[ \"${USE_LATEST_PHP}\" == \"1\" ]]; then\n\tPHP_82_LATEST=$(skopeo inspect docker://docker.io/library/php:8.2 --override-os linux --override-arch amd64 | jq -r '.Env[] | select(test(\"^PHP_VERSION=\")) | sub(\"^PHP_VERSION=\"; \"\")')\n\tPHP_83_LATEST=$(skopeo inspect docker://docker.io/library/php:8.3 --override-os linux --override-arch amd64 | jq -r '.Env[] | select(test(\"^PHP_VERSION=\")) | sub(\"^PHP_VERSION=\"; \"\")')\n\tPHP_84_LATEST=$(skopeo inspect docker://docker.io/library/php:8.4 --override-os linux --override-arch amd64 | jq -r '.Env[] | select(test(\"^PHP_VERSION=\")) | sub(\"^PHP_VERSION=\"; \"\")')\n\tPHP_85_LATEST=$(skopeo inspect docker://docker.io/library/php:8.5 --override-os linux --override-arch amd64 | jq -r '.Env[] | select(test(\"^PHP_VERSION=\")) | sub(\"^PHP_VERSION=\"; \"\")')\n\tPHP_VERSION=\"${PHP_82_LATEST},${PHP_83_LATEST},${PHP_84_LATEST},${PHP_85_LATEST}\"\nfi\n\nOS_LIST=()\nwhile IFS= read -r os; do\n\tOS_LIST+=(\"${os}\")\ndone < <(\n\tpython3 - <<'PY'\nimport re\n\nwith open(\"docker-bake.hcl\", \"r\", encoding=\"utf-8\") as f:\n\tdata = f.read()\n\n# Find the first \"os = [ ... ]\" block and extract quoted values\nm = re.search(r'os\\s*=\\s*\\[(.*?)\\]', data, re.S)\nif not m:\n\traise SystemExit(1)\n\nvals = re.findall(r'\"([^\"]+)\"', m.group(1))\nfor v in vals:\n\tprint(v)\nPY\n)\n\nIFS=',' read -r -a PHP_VERSIONS <<<\"${PHP_VERSION}\"\n\nBASE_IMAGES=()\nfor os in \"${OS_LIST[@]}\"; do\n\tBASE_IMAGES+=(\"golang:${GO_VERSION}-${os}\")\n\tfor pv in \"${PHP_VERSIONS[@]}\"; do\n\t\tBASE_IMAGES+=(\"php:${pv}-zts-${os}\")\n\tdone\ndone\n\nmapfile -t BASE_IMAGES < <(printf '%s\\n' \"${BASE_IMAGES[@]}\" | sort -u)\n\nBASE_IMAGE_DIGESTS=()\nfor image in \"${BASE_IMAGES[@]}\"; do\n\tif [[ \"${image}\" == */* ]]; then\n\t\tref=\"docker://docker.io/${image}\"\n\telse\n\t\tref=\"docker://docker.io/library/${image}\"\n\tfi\n\tdigest=\"$(skopeo inspect \"${ref}\" --override-os linux --override-arch amd64 --format '{{.Digest}}')\"\n\tBASE_IMAGE_DIGESTS+=(\"${image}@${digest}\")\ndone\n\nhash_cmd=\"sha256sum\"\nif ! command -v \"${hash_cmd}\" >/dev/null 2>&1; then\n\thash_cmd=\"shasum -a 256\"\nfi\n\nfingerprint=\"$(printf '%s\\n' \"${BASE_IMAGE_DIGESTS[@]}\" | sort | ${hash_cmd} | awk '{print $1}')\"\n\necho \"PHP_VERSION=${PHP_VERSION}\"\necho \"GO_VERSION=${GO_VERSION}\"\necho \"OS_LIST=${OS_LIST[*]}\"\necho \"Base images:\"\nprintf '  %s\\n' \"${BASE_IMAGES[@]}\"\necho \"Fingerprint: ${fingerprint}\"\n"
  },
  {
    "path": ".github/workflows/docker.yaml",
    "content": "---\nname: Build Docker Images\nconcurrency:\n  cancel-in-progress: true\n  group: ${{ github.workflow }}-${{ github.ref }}\non:\n  pull_request:\n    branches:\n      - main\n    paths:\n      - \"docker-bake.hcl\"\n      - \".github/workflows/docker.yaml\"\n      - \"**cgo.go\"\n      - \"**Dockerfile\"\n      - \"**.c\"\n      - \"**.h\"\n      - \"**.sh\"\n      - \"**.stub.php\"\n  push:\n    branches:\n      - main\n    tags:\n      - v*.*.*\n  workflow_dispatch:\n    inputs:\n      #checkov:skip=CKV_GHA_7\n      version:\n        description: \"FrankenPHP version\"\n        required: false\n        type: string\n  schedule:\n    - cron: \"0 4 * * *\"\npermissions:\n  contents: read\nenv:\n  IMAGE_NAME: ${{ (github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && inputs.version) || startsWith(github.ref, 'refs/tags/')) && 'dunglas/frankenphp' || 'dunglas/frankenphp-dev' }}\njobs:\n  prepare:\n    runs-on: ubuntu-24.04\n    outputs:\n      # Push if it's a scheduled job, a tag, or if we're committing to the main branch\n      push: ${{ (github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && inputs.version) || startsWith(github.ref, 'refs/tags/') || (github.ref == 'refs/heads/main' && github.event_name != 'pull_request')) && true || false }}\n      variants: ${{ steps.matrix.outputs.variants }}\n      platforms: ${{ steps.matrix.outputs.platforms }}\n      metadata: ${{ steps.matrix.outputs.metadata }}\n      php_version: ${{ steps.check.outputs.php_version }}\n      php82_version: ${{ steps.check.outputs.php82_version }}\n      php83_version: ${{ steps.check.outputs.php83_version }}\n      php84_version: ${{ steps.check.outputs.php84_version }}\n      php85_version: ${{ steps.check.outputs.php85_version }}\n      skip: ${{ steps.check.outputs.skip }}\n      ref: ${{ steps.check.outputs.ref || (github.event_name == 'workflow_dispatch' && inputs.version) || '' }}\n      base_fingerprint: ${{ steps.check.outputs.base_fingerprint }}\n    steps:\n      - uses: actions/checkout@v6\n        with:\n          fetch-depth: 0\n          persist-credentials: false\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v4\n      - name: Check PHP versions and base image fingerprint\n        id: check\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        run: ./.github/scripts/docker-compute-fingerprints.sh\n      - name: Create variants matrix\n        if: ${{ !fromJson(steps.check.outputs.skip) }}\n        id: matrix\n        shell: bash\n        run: |\n          set -e\n          METADATA=\"$(docker buildx bake --print | jq -c)\"\n          {\n            echo metadata=\"${METADATA}\"\n            echo variants=\"$(jq -c '.group.default.targets|map(sub(\"runner-|builder-\"; \"\"))|unique' <<< \"${METADATA}\")\"\n            echo platforms=\"$(jq -c 'first(.target[]) | .platforms' <<< \"${METADATA}\")\"\n          } >> \"${GITHUB_OUTPUT}\"\n        env:\n          SHA: ${{ github.sha }}\n          VERSION: ${{ (github.ref_type == 'tag' && github.ref_name) || steps.check.outputs.ref || 'dev' }}\n          PHP_VERSION: ${{ steps.check.outputs.php_version }}\n  build:\n    runs-on: ${{ startsWith(matrix.platform, 'linux/arm') && 'ubuntu-24.04-arm' || 'ubuntu-24.04' }}\n    needs:\n      - prepare\n    if: ${{ !fromJson(needs.prepare.outputs.skip) }}\n    strategy:\n      fail-fast: false\n      matrix:\n        variant: ${{ fromJson(needs.prepare.outputs.variants) }}\n        platform: ${{ fromJson(needs.prepare.outputs.platforms) }}\n        include:\n          - race: \"\"\n          - platform: linux/amd64\n            race: \"-race\" # The Go race detector is only supported on amd64\n        exclude:\n          # arm/v6 is only available for Alpine: https://github.com/docker-library/golang/issues/502\n          - variant: php-${{ needs.prepare.outputs.php82_version }}-trixie\n            platform: linux/arm/v6\n          - variant: php-${{ needs.prepare.outputs.php83_version }}-trixie\n            platform: linux/arm/v6\n          - variant: php-${{ needs.prepare.outputs.php84_version }}-trixie\n            platform: linux/arm/v6\n          - variant: php-${{ needs.prepare.outputs.php85_version }}-trixie\n            platform: linux/arm/v6\n          - variant: php-${{ needs.prepare.outputs.php82_version }}-bookworm\n            platform: linux/arm/v6\n          - variant: php-${{ needs.prepare.outputs.php83_version }}-bookworm\n            platform: linux/arm/v6\n          - variant: php-${{ needs.prepare.outputs.php84_version }}-bookworm\n            platform: linux/arm/v6\n          - variant: php-${{ needs.prepare.outputs.php85_version }}-bookworm\n            platform: linux/arm/v6\n    steps:\n      - name: Prepare\n        id: prepare\n        run: echo \"sanitized_platform=${PLATFORM//\\//-}\" >> \"${GITHUB_OUTPUT}\"\n        env:\n          PLATFORM: ${{ matrix.platform }}\n      - uses: actions/checkout@v6\n        with:\n          ref: ${{ needs.prepare.outputs.ref }}\n          persist-credentials: false\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v4\n        with:\n          platforms: ${{ matrix.platform }}\n      - name: Login to DockerHub\n        uses: docker/login-action@v4\n        if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository\n        with:\n          username: ${{ secrets.REGISTRY_USERNAME }}\n          password: ${{ secrets.REGISTRY_PASSWORD }}\n      - name: Build\n        id: build\n        uses: docker/bake-action@v7\n        with:\n          pull: true\n          load: ${{ !fromJson(needs.prepare.outputs.push) }}\n          source: .\n          targets: |\n            builder-${{ matrix.variant }}\n            runner-${{ matrix.variant }}\n          # Remove tags to prevent \"can't push tagged ref [...] by digest\" error\n          set: |\n            ${{ (github.event_name == 'pull_request') && '*.args.NO_COMPRESS=1' || '' }}\n            *.tags=\n            *.platform=${{ matrix.platform }}\n            ${{ fromJson(needs.prepare.outputs.push) && '' || format('builder-{0}.cache-from=type=gha,scope=builder-{0}-{1}-{2}', matrix.variant, needs.prepare.outputs.ref || github.ref, matrix.platform) }}\n            ${{ fromJson(needs.prepare.outputs.push) && '' || format('builder-{0}.cache-from=type=gha,scope=refs/heads/main-builder-{0}-{1}', matrix.variant, matrix.platform) }}\n            ${{ fromJson(needs.prepare.outputs.push) && '' || format('builder-{0}.cache-to=type=gha,scope=builder-{0}-{1}-{2},ignore-error=true', matrix.variant, needs.prepare.outputs.ref || github.ref, matrix.platform) }}\n            ${{ fromJson(needs.prepare.outputs.push) && '' || format('runner-{0}.cache-from=type=gha,scope=runner-{0}-{1}-{2}', matrix.variant, needs.prepare.outputs.ref || github.ref, matrix.platform) }}\n            ${{ fromJson(needs.prepare.outputs.push) && '' || format('runner-{0}.cache-from=type=gha,scope=refs/heads/main-runner-{0}-{1}', matrix.variant, matrix.platform) }}\n            ${{ fromJson(needs.prepare.outputs.push) && '' || format('runner-{0}.cache-to=type=gha,scope=runner-{0}-{1}-{2},ignore-error=true', matrix.variant, needs.prepare.outputs.ref || github.ref, matrix.platform) }}\n            ${{ fromJson(needs.prepare.outputs.push) && format('*.output=type=image,name={0},push-by-digest=true,name-canonical=true,push=true', env.IMAGE_NAME) || '' }}\n        env:\n          SHA: ${{ github.sha }}\n          VERSION: ${{ (github.ref_type == 'tag' && github.ref_name) || needs.prepare.outputs.ref || 'dev' }}\n          PHP_VERSION: ${{ needs.prepare.outputs.php_version }}\n          BASE_FINGERPRINT: ${{ needs.prepare.outputs.base_fingerprint }}\n      - # Workaround for https://github.com/actions/runner/pull/2477#issuecomment-1501003600\n        name: Export metadata\n        if: fromJson(needs.prepare.outputs.push)\n        run: |\n          mkdir -p /tmp/metadata/builder /tmp/metadata/runner\n\n          builderDigest=$(jq -r \".\\\"builder-${VARIANT}\\\".\\\"containerimage.digest\\\"\" <<< \"${METADATA}\")\n          touch \"/tmp/metadata/builder/${builderDigest#sha256:}\"\n\n          runnerDigest=$(jq -r \".\\\"runner-${VARIANT}\\\".\\\"containerimage.digest\\\"\" <<< \"${METADATA}\")\n          touch \"/tmp/metadata/runner/${runnerDigest#sha256:}\"\n        env:\n          METADATA: ${{ steps.build.outputs.metadata }}\n          VARIANT: ${{ matrix.variant }}\n      - name: Upload builder metadata\n        if: fromJson(needs.prepare.outputs.push)\n        uses: actions/upload-artifact@v7\n        with:\n          name: metadata-builder-${{ matrix.variant }}-${{ steps.prepare.outputs.sanitized_platform }}\n          path: /tmp/metadata/builder/*\n          if-no-files-found: error\n          retention-days: 1\n      - name: Upload runner metadata\n        if: fromJson(needs.prepare.outputs.push)\n        uses: actions/upload-artifact@v7\n        with:\n          name: metadata-runner-${{ matrix.variant }}-${{ steps.prepare.outputs.sanitized_platform }}\n          path: /tmp/metadata/runner/*\n          if-no-files-found: error\n          retention-days: 1\n      - name: Run tests\n        if: ${{ !fromJson(needs.prepare.outputs.push) }}\n        run: |\n          # TODO: remove \"containerimage.config.digest\" fallback once all runners use buildx v0.18+\n          # which replaced it with \"containerimage.digest\" and \"containerimage.descriptor\"\n          docker run --platform=\"${PLATFORM}\" --rm \\\n            \"$(jq -r \".\\\"builder-${VARIANT}\\\" | .\\\"containerimage.config.digest\\\" // .\\\"containerimage.digest\\\"\" <<< \"${METADATA}\")\" \\\n            sh -c \"./go.sh test ${RACE} -v $(./go.sh list ./... | grep -v github.com/dunglas/frankenphp/internal/testext | grep -v github.com/dunglas/frankenphp/internal/extgen | tr '\\n' ' ') && cd caddy && ../go.sh test ${RACE} -v ./...\"\n        env:\n          METADATA: ${{ steps.build.outputs.metadata }}\n          PLATFORM: ${{ matrix.platform }}\n          VARIANT: ${{ matrix.variant }}\n          RACE: ${{ matrix.race }}\n\n  # Adapted from https://docs.docker.com/build/ci/github-actions/multi-platform/\n  push:\n    runs-on: ubuntu-24.04\n    needs:\n      - prepare\n      - build\n    if: fromJson(needs.prepare.outputs.push)\n    strategy:\n      fail-fast: false\n      matrix:\n        variant: ${{ fromJson(needs.prepare.outputs.variants) }}\n        target: [\"builder\", \"runner\"]\n    steps:\n      - name: Download metadata\n        uses: actions/download-artifact@v8\n        with:\n          pattern: metadata-${{ matrix.target }}-${{ matrix.variant }}-*\n          path: /tmp/metadata\n          merge-multiple: true\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v4\n      - name: Login to DockerHub\n        uses: docker/login-action@v4\n        if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository\n        with:\n          username: ${{ secrets.REGISTRY_USERNAME }}\n          password: ${{ secrets.REGISTRY_PASSWORD }}\n      - name: Create manifest list and push\n        working-directory: /tmp/metadata\n        run: |\n          set -x\n          # shellcheck disable=SC2046,SC2086\n          docker buildx imagetools create $(jq -cr \".target.\\\"${TARGET}-${VARIANT}\\\".tags | map(\\\"-t \\\" + .) | join(\\\" \\\")\" <<< ${METADATA}) \\\n            $(printf \"${IMAGE_NAME}@sha256:%s \" *)\n        env:\n          METADATA: ${{ needs.prepare.outputs.metadata }}\n          TARGET: ${{ matrix.target }}\n          VARIANT: ${{ matrix.variant }}\n      - name: Inspect image\n        run: |\n          # shellcheck disable=SC2046,SC2086\n          docker buildx imagetools inspect $(jq -cr \".target.\\\"${TARGET}-${VARIANT}\\\".tags | first\" <<< ${METADATA})\n        env:\n          METADATA: ${{ needs.prepare.outputs.metadata }}\n          TARGET: ${{ matrix.target }}\n          VARIANT: ${{ matrix.variant }}\n"
  },
  {
    "path": ".github/workflows/docs.yaml",
    "content": "---\nname: Deploy Docs\non:\n  push:\n    branches:\n      - main\n    paths:\n      - \"docs/**\"\n      - \"README.md\"\n      - \"CONTRIBUTING.md\"\n      - \"install.ps1\"\n      - \"install.sh\"\npermissions: {}\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\njobs:\n  deploy:\n    runs-on: ubuntu-slim\n    steps:\n      - name: Trigger website deployment\n        env:\n          GH_TOKEN: ${{ secrets.WEBSITE_DEPLOY_TOKEN }}\n        run: gh api repos/dunglas/frankenphp-website/actions/workflows/hugo.yaml/dispatches -f ref=main\n"
  },
  {
    "path": ".github/workflows/lint.yaml",
    "content": "---\nname: Lint Code Base\nconcurrency:\n  cancel-in-progress: true\n  group: ${{ github.workflow }}-${{ github.ref }}\non:\n  pull_request:\n    branches:\n      - main\n  push:\n    branches:\n      - main\npermissions:\n  contents: read\n  packages: read\n  statuses: write\njobs:\n  build:\n    name: Lint Code Base\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout Code\n        uses: actions/checkout@v6\n        with:\n          fetch-depth: 0\n          persist-credentials: false\n      - name: Lint Code Base\n        uses: super-linter/super-linter/slim@v8\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          LINTER_RULES_PATH: /\n          MARKDOWN_CONFIG_FILE: .markdown-lint.yaml\n          FILTER_REGEX_EXCLUDE: docs/(cn|es|fr|ja|pt-br|ru|tr)/\n          VALIDATE_CPP: false\n          VALIDATE_JSCPD: false\n          VALIDATE_GO: false\n          VALIDATE_GO_MODULES: false\n          VALIDATE_PHP_PHPCS: false\n          VALIDATE_PHP_PHPSTAN: false\n          VALIDATE_PHP_PSALM: false\n          VALIDATE_TERRAGRUNT: false\n          VALIDATE_DOCKERFILE_HADOLINT: false\n          VALIDATE_TRIVY: false\n          # Prettier, Biome and StandardJS are incompatible\n          VALIDATE_JAVASCRIPT_PRETTIER: false\n          VALIDATE_TYPESCRIPT_PRETTIER: false\n          VALIDATE_BIOME_FORMAT: false\n          VALIDATE_BIOME_LINT: false\n          # Conflicts with MARKDOWN\n          VALIDATE_MARKDOWN_PRETTIER: false\n          # To re-enable when https://github.com/super-linter/super-linter/issues/7466 will be closed\n          VALIDATE_SPELL_CODESPELL: false\n"
  },
  {
    "path": ".github/workflows/sanitizers.yaml",
    "content": "---\nname: Sanitizers\nconcurrency:\n  cancel-in-progress: true\n  group: ${{ github.workflow }}-${{ github.ref }}\non:\n  pull_request:\n    branches:\n      - main\n    paths-ignore:\n      - \"docs/**\"\n  push:\n    branches:\n      - main\n    paths-ignore:\n      - \"docs/**\"\npermissions:\n  contents: read\nenv:\n  GOTOOLCHAIN: local\njobs:\n  # Adapted from https://github.com/beberlei/hdrhistogram-php\n  sanitizers:\n    name: ${{ matrix.sanitizer }}\n    runs-on: ubuntu-latest\n    strategy:\n      fail-fast: false\n      matrix:\n        sanitizer: [\"asan\", \"msan\"]\n    env:\n      CFLAGS: -g -O0 -fsanitize=${{ matrix.sanitizer == 'asan' && 'address' || 'memory' }} -DZEND_TRACK_ARENA_ALLOC\n      LDFLAGS: -fsanitize=${{ matrix.sanitizer == 'asan' && 'address' || 'memory' }}\n      CC: clang\n      CXX: clang++\n      USE_ZEND_ALLOC: 0\n      LIBRARY_PATH: ${{ github.workspace }}/php/target/lib:${{ github.workspace }}/watcher/target/lib\n      LD_LIBRARY_PATH: ${{ github.workspace }}/php/target/lib\n      # PHP doesn't free some memory on purpose, we have to disable leaks detection: https://go.dev/doc/go1.26#go-command\n      ASAN_OPTIONS: detect_leaks=0\n    steps:\n      - name: Remove local PHP\n        run: sudo apt-get remove --purge --autoremove 'php*' 'libmemcached*'\n      - uses: actions/checkout@v6\n        with:\n          persist-credentials: false\n      - uses: actions/setup-go@v6\n        with:\n          go-version: \"1.26\"\n          cache-dependency-path: |\n            go.sum \n            caddy/go.sum\n      - name: Determine PHP version\n        id: determine-php-version\n        run: |\n          curl -fsSL 'https://www.php.net/releases/index.php?json&max=1&version=8.5' -o version.json 2>/dev/null || curl -fsSL 'https://phpmirror.static-php.dev/releases/index.php?json&max=1&version=8.5' -o version.json\n          echo version=\"$(jq -r 'keys[0]' version.json)\" >> \"$GITHUB_OUTPUT\"\n          echo archive=\"$(jq -r '.[] .source[] | select(.filename |endswith(\".xz\")) | \"https://www.php.net/distributions/\" + .filename' version.json)\" >> \"$GITHUB_OUTPUT\"\n      - name: Cache PHP\n        id: cache-php\n        uses: actions/cache@v5\n        with:\n          path: php/target\n          key: php-sanitizers-${{ matrix.sanitizer }}-${{ runner.arch }}-${{ steps.determine-php-version.outputs.version }}\n      - if: steps.cache-php.outputs.cache-hit != 'true'\n        name: Compile PHP\n        run: |\n          mkdir php/\n          MIRROR_URL=${URL/https:\\/\\/www.php.net/https:\\/\\/phpmirror.static-php.dev}\n          (curl -fsSL \"${URL}\" || curl -fsSL \"${MIRROR_URL}\") | tar -Jx -C php --strip-components=1\n          cd php/\n          ./configure \\\n            CFLAGS=\"$CFLAGS\" \\\n            LDFLAGS=\"$LDFLAGS\" \\\n            --enable-debug \\\n            --enable-embed \\\n            --enable-zts \\\n            --enable-option-checking=fatal \\\n            --disable-zend-signals \\\n            --without-sqlite3 \\\n            --without-pdo-sqlite \\\n            --without-libxml \\\n            --disable-dom \\\n            --disable-simplexml \\\n            --disable-xml \\\n            --disable-xmlreader \\\n            --disable-xmlwriter \\\n            --without-pcre-jit \\\n            --disable-opcache-jit \\\n            --disable-cli \\\n            --disable-cgi \\\n            --disable-phpdbg \\\n            --without-pear \\\n            --disable-mbregex \\\n            --enable-werror \\\n            ${{ matrix.sanitizer == 'msan' && '--enable-memory-sanitizer' || '' }} \\\n            --prefix=\"$(pwd)/target/\"\n          make -j\"$(getconf _NPROCESSORS_ONLN)\"\n          make install\n        env:\n          URL: ${{ steps.determine-php-version.outputs.archive }}\n      - name: Add PHP to the PATH\n        run: echo \"$(pwd)/php/target/bin\" >> \"$GITHUB_PATH\"\n      - name: Install e-dant/watcher\n        uses: ./.github/actions/watcher\n      - name: Set Set CGO flags\n        run: |\n          {\n            echo \"CGO_CFLAGS=$CFLAGS -I${PWD}/watcher/target/include $(php-config --includes)\"\n            echo \"CGO_LDFLAGS=$LDFLAGS $(php-config --ldflags) $(php-config --libs)\"\n          } >> \"$GITHUB_ENV\"\n      - name: Compile tests\n        run: go test ${{ matrix.sanitizer == 'msan' && '-tags=nowatcher' || '' }} -${{ matrix.sanitizer }} -v -x -c\n      - name: Run tests\n        run: ./frankenphp.test -test.v\n"
  },
  {
    "path": ".github/workflows/static.yaml",
    "content": "---\nname: Build binary releases\nconcurrency:\n  cancel-in-progress: true\n  group: ${{ github.workflow }}-${{ github.ref }}\n\non:\n  pull_request:\n    branches:\n      - main\n    paths:\n      - \"docker-bake.hcl\"\n      - \".github/workflows/static.yaml\"\n      - \"**cgo.go\"\n      - \"**Dockerfile\"\n      - \"**.c\"\n      - \"**.h\"\n      - \"**.sh\"\n      - \"**.stub.php\"\n  push:\n    branches:\n      - main\n    tags:\n      - v*.*.*\n  workflow_dispatch:\n    inputs:\n      #checkov:skip=CKV_GHA_7\n      version:\n        description: \"FrankenPHP version\"\n        required: false\n        type: string\n  schedule:\n    - cron: \"0 0 * * *\"\n\npermissions:\n  contents: read\n\nenv:\n  IMAGE_NAME: ${{ (github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && inputs.version) || startsWith(github.ref, 'refs/tags/')) && 'dunglas/frankenphp' || 'dunglas/frankenphp-dev' }}\n  SPC_OPT_BUILD_ARGS: --debug\n  GOTOOLCHAIN: local\n\njobs:\n  prepare:\n    runs-on: ubuntu-24.04\n    outputs:\n      push: ${{ toJson((steps.check.outputs.ref || (github.event_name == 'workflow_dispatch' && inputs.version) || startsWith(github.ref, 'refs/tags/') || (github.ref == 'refs/heads/main' && github.event_name != 'pull_request')) && true || false) }}\n      platforms: ${{ steps.matrix.outputs.platforms }}\n      metadata: ${{ steps.matrix.outputs.metadata }}\n      gnu_metadata: ${{ steps.matrix.outputs.gnu_metadata }}\n      ref: ${{ steps.check.outputs.ref }}\n    steps:\n      - name: Get version\n        id: check\n        if: github.event_name == 'schedule'\n        run: |\n          ref=\"${REF}\"\n          if [[ -z \"${ref}\" ]]; then\n            ref=\"$(gh release view --repo dunglas/frankenphp --json tagName --jq '.tagName')\"\n          fi\n\n          echo \"ref=${ref}\" >> \"${GITHUB_OUTPUT}\"\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          REF: ${{ (github.ref_type == 'tag' && github.ref_name) || (github.event_name == 'workflow_dispatch' && inputs.version) || '' }}\n      - uses: actions/checkout@v6\n        with:\n          ref: ${{ steps.check.outputs.ref }}\n          persist-credentials: false\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v4\n      - name: Create platforms matrix\n        id: matrix\n        run: |\n          METADATA=\"$(docker buildx bake --print static-builder-musl | jq -c)\"\n          GNU_METADATA=\"$(docker buildx bake --print static-builder-gnu | jq -c)\"\n          {\n            echo metadata=\"${METADATA}\"\n            echo platforms=\"$(jq -c 'first(.target[]) | .platforms' <<< \"${METADATA}\")\"\n            echo gnu_metadata=\"${GNU_METADATA}\"\n          } >> \"${GITHUB_OUTPUT}\"\n        env:\n          SHA: ${{ github.sha }}\n          VERSION: ${{ steps.check.outputs.ref || 'dev' }}\n\n  build-linux-musl:\n    permissions:\n      contents: write\n      id-token: write\n      attestations: write\n    strategy:\n      fail-fast: false\n      matrix:\n        platform: ${{ fromJson(needs.prepare.outputs.platforms) }}\n        debug: [false]\n        mimalloc: [false]\n        include:\n          - platform: linux/amd64\n          - platform: linux/amd64\n            debug: true\n          - platform: linux/amd64\n            mimalloc: true\n    name: Build ${{ matrix.platform }} static musl binary${{ matrix.debug && ' (debug)' || '' }}${{ matrix.mimalloc && ' (mimalloc)' || '' }}\n    runs-on: ${{ startsWith(matrix.platform, 'linux/arm') && 'ubuntu-24.04-arm' || 'ubuntu-24.04' }}\n    needs: [prepare]\n    steps:\n      - name: Prepare\n        id: prepare\n        run: echo \"sanitized_platform=${PLATFORM//\\//-}\" >> \"${GITHUB_OUTPUT}\"\n        env:\n          PLATFORM: ${{ matrix.platform }}\n      - uses: actions/checkout@v6\n        with:\n          ref: ${{ needs.prepare.outputs.ref }}\n          persist-credentials: false\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v4\n        with:\n          platforms: ${{ matrix.platform }}\n      - name: Login to DockerHub\n        uses: docker/login-action@v4\n        if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository\n        with:\n          username: ${{ secrets.REGISTRY_USERNAME }}\n          password: ${{ secrets.REGISTRY_PASSWORD }}\n      - name: Set VERSION\n        run: |\n          if [ \"${GITHUB_REF_TYPE}\" == \"tag\" ]; then\n            export VERSION=${GITHUB_REF_NAME:1}\n          elif [ \"${GITHUB_EVENT_NAME}\" == \"schedule\" ]; then\n            export VERSION=\"${REF}\"\n          else\n            export VERSION=${GITHUB_SHA}\n          fi\n\n          echo \"VERSION=${VERSION}\" >> \"${GITHUB_ENV}\"\n        env:\n          REF: ${{ needs.prepare.outputs.ref }}\n      - name: Build\n        id: build\n        uses: docker/bake-action@v7\n        with:\n          pull: true\n          load: ${{ !fromJson(needs.prepare.outputs.push) || matrix.debug || matrix.mimalloc }}\n          source: .\n          targets: static-builder-musl\n          set: |\n            ${{ matrix.debug && 'static-builder-musl.args.DEBUG_SYMBOLS=1' || '' }}\n            ${{ matrix.mimalloc && 'static-builder-musl.args.MIMALLOC=1' || '' }}\n            ${{ (github.event_name == 'pull_request' || matrix.platform == 'linux/arm64') && 'static-builder-musl.args.NO_COMPRESS=1' || '' }}\n            *.tags=\n            *.platform=${{ matrix.platform }}\n            ${{ fromJson(needs.prepare.outputs.push) && '' || format('*.cache-from=type=gha,scope={0}-static-builder-musl{1}{2}', needs.prepare.outputs.ref || github.ref, matrix.debug && '-debug' || '', matrix.mimalloc && '-mimalloc' || '') }}\n            ${{ fromJson(needs.prepare.outputs.push) && '' || format('*.cache-from=type=gha,scope=refs/heads/main-static-builder-musl{0}{1}', matrix.debug && '-debug' || '', matrix.mimalloc && '-mimalloc' || '') }}\n            ${{ fromJson(needs.prepare.outputs.push) && '' || format('*.cache-to=type=gha,scope={0}-static-builder-musl{1}{2},ignore-error=true', needs.prepare.outputs.ref || github.ref, matrix.debug && '-debug' || '', matrix.mimalloc && '-mimalloc' || '') }}\n            ${{ (fromJson(needs.prepare.outputs.push) && !matrix.debug && !matrix.mimalloc) && format('*.output=type=image,name={0},push-by-digest=true,name-canonical=true,push=true', env.IMAGE_NAME) || '' }}\n        env:\n          SHA: ${{ github.sha }}\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n      - # Workaround for https://github.com/actions/runner/pull/2477#issuecomment-1501003600\n        name: Export metadata\n        if: fromJson(needs.prepare.outputs.push) && !matrix.debug && !matrix.mimalloc\n        run: |\n          mkdir -p /tmp/metadata\n\n          # shellcheck disable=SC2086\n          digest=$(jq -r '.\"static-builder-musl\".\"containerimage.digest\"' <<< ${METADATA})\n          touch \"/tmp/metadata/${digest#sha256:}\"\n        env:\n          METADATA: ${{ steps.build.outputs.metadata }}\n      - name: Upload metadata\n        if: fromJson(needs.prepare.outputs.push) && !matrix.debug && !matrix.mimalloc\n        uses: actions/upload-artifact@v7\n        with:\n          name: metadata-static-builder-musl-${{ steps.prepare.outputs.sanitized_platform }}\n          path: /tmp/metadata/*\n          if-no-files-found: error\n          retention-days: 1\n      - name: Copy binary\n        run: |\n          # shellcheck disable=SC2034\n          # TODO: remove \"containerimage.config.digest\" fallback once all runners use buildx v0.18+\n          # which replaced it with \"containerimage.digest\" and \"containerimage.descriptor\"\n          digest=$(jq -r '.\"static-builder-musl\" | ${{ (fromJson(needs.prepare.outputs.push) && !matrix.debug && !matrix.mimalloc) && '.\"containerimage.digest\"' || '(.\"containerimage.config.digest\" // .\"containerimage.digest\")' }}' <<< \"${METADATA}\")\n          docker create --platform=\"${PLATFORM}\" --name static-builder-musl \"${{ (fromJson(needs.prepare.outputs.push) && !matrix.debug && !matrix.mimalloc) && '${IMAGE_NAME}@${digest}' || '${digest}' }}\"\n          docker cp \"static-builder-musl:/go/src/app/dist/${BINARY}\" \"${BINARY}${{ matrix.debug && '-debug' || '' }}${{ matrix.mimalloc && '-mimalloc' || '' }}\"\n        env:\n          METADATA: ${{ steps.build.outputs.metadata }}\n          BINARY: frankenphp-linux-${{ matrix.platform == 'linux/amd64' && 'x86_64' || 'aarch64' }}\n          PLATFORM: ${{ matrix.platform }}\n      - name: Upload artifact\n        if: ${{ !fromJson(needs.prepare.outputs.push) }}\n        uses: actions/upload-artifact@v7\n        with:\n          name: frankenphp-linux-${{ matrix.platform == 'linux/amd64' && 'x86_64' || 'aarch64' }}${{ matrix.debug && '-debug' || '' }}${{ matrix.mimalloc && '-mimalloc' || '' }}\n          path: frankenphp-linux-${{ matrix.platform == 'linux/amd64' && 'x86_64' || 'aarch64' }}${{ matrix.debug && '-debug' || '' }}${{ matrix.mimalloc && '-mimalloc' || '' }}\n          compression-level: 0\n      - name: Upload assets\n        if: fromJson(needs.prepare.outputs.push) && (needs.prepare.outputs.ref || github.ref_type == 'tag')\n        run: gh release upload \"${REF}\" frankenphp-linux-${{ matrix.platform == 'linux/amd64' && 'x86_64' || 'aarch64' }}${{ matrix.debug && '-debug' || '' }}${{ matrix.mimalloc && '-mimalloc' || '' }} --repo dunglas/frankenphp --clobber\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          REF: ${{ (github.ref_type == 'tag' && github.ref_name) || needs.prepare.outputs.ref }}\n      - if: fromJson(needs.prepare.outputs.push) && (needs.prepare.outputs.ref || github.ref_type == 'tag')\n        uses: actions/attest-build-provenance@v4\n        with:\n          subject-path: ${{ github.workspace }}/frankenphp-linux-*\n      - name: Run sanity checks\n        run: |\n          \"${BINARY}\" version\n          \"${BINARY}\" build-info\n          \"${BINARY}\" list-modules | grep frankenphp\n          \"${BINARY}\" list-modules | grep http.encoders.br\n          \"${BINARY}\" list-modules | grep http.handlers.mercure\n          \"${BINARY}\" list-modules | grep http.handlers.mercure\n          \"${BINARY}\" list-modules | grep http.handlers.vulcain\n          \"${BINARY}\" php-cli -r \"echo 'Sanity check passed';\"\n        env:\n          BINARY: ./frankenphp-linux-${{ matrix.platform == 'linux/amd64' && 'x86_64' || 'aarch64' }}${{ matrix.debug && '-debug' || '' }}${{ matrix.mimalloc && '-mimalloc' || '' }}\n\n  build-linux-gnu:\n    permissions:\n      contents: write\n      id-token: write\n      attestations: write\n    strategy:\n      fail-fast: false\n      matrix:\n        platform: ${{ fromJson(needs.prepare.outputs.platforms) }}\n    name: Build ${{ matrix.platform }} static GNU binary\n    runs-on: ${{ startsWith(matrix.platform, 'linux/arm') && 'ubuntu-24.04-arm' || 'ubuntu-24.04' }}\n    needs: [prepare]\n    steps:\n      # Inspired by https://gist.github.com/antiphishfish/1e3fbc3f64ef6f1ab2f47457d2da5d9d and https://github.com/apache/flink/blob/master/tools/azure-pipelines/free_disk_space.sh\n      - name: Free disk space\n        run: |\n          set -xe\n          sudo rm -rf /usr/share/dotnet\n          sudo rm -rf /usr/share/swift\n          sudo rm -rf /usr/local/lib/android\n          sudo rm -rf /opt/ghc\n          sudo rm -rf /usr/local/.ghcup\n          sudo rm -rf \"/usr/local/share/boost\"\n          sudo rm -rf \"$AGENT_TOOLSDIRECTORY\"\n          sudo rm -rf /opt/hostedtoolcache/\n          sudo rm -rf /usr/local/graalvm/\n          sudo rm -rf /usr/local/share/powershell\n          sudo rm -rf /usr/local/share/chromium\n          sudo rm -rf /usr/local/lib/node_modules\n          sudo docker image prune --all --force\n\n          APT_PARAMS='sudo apt -y -qq -o=Dpkg::Use-Pty=0'\n          $APT_PARAMS remove -y '^dotnet-.*'\n          $APT_PARAMS remove -y '^llvm-.*'\n          $APT_PARAMS remove -y '^php.*'\n          $APT_PARAMS remove -y '^mongodb-.*'\n          $APT_PARAMS remove -y '^mysql-.*'\n          $APT_PARAMS remove -y azure-cli firefox powershell mono-devel libgl1-mesa-dri\n          $APT_PARAMS autoremove --purge -y\n          $APT_PARAMS autoclean\n          $APT_PARAMS clean\n      - name: Prepare\n        id: prepare\n        run: echo \"sanitized_platform=${PLATFORM//\\//-}\" >> \"${GITHUB_OUTPUT}\"\n        env:\n          PLATFORM: ${{ matrix.platform }}\n      - uses: actions/checkout@v6\n        with:\n          ref: ${{ needs.prepare.outputs.ref }}\n          persist-credentials: false\n      - name: Set VERSION\n        run: |\n          if [ \"${GITHUB_REF_TYPE}\" == \"tag\" ]; then\n            export VERSION=${GITHUB_REF_NAME:1}\n          elif [ \"${GITHUB_EVENT_NAME}\" == \"schedule\" ]; then\n            export VERSION=\"${REF}\"\n          else\n            export VERSION=${GITHUB_SHA}\n          fi\n\n          echo \"VERSION=${VERSION}\" >> \"${GITHUB_ENV}\"\n        env:\n          REF: ${{ needs.prepare.outputs.ref }}\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v4\n        with:\n          platforms: ${{ matrix.platform }}\n      - name: Login to DockerHub\n        uses: docker/login-action@v4\n        if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository\n        with:\n          username: ${{ secrets.REGISTRY_USERNAME }}\n          password: ${{ secrets.REGISTRY_PASSWORD }}\n      - name: Build\n        id: build\n        uses: docker/bake-action@v7\n        with:\n          pull: true\n          load: ${{ !fromJson(needs.prepare.outputs.push) }}\n          source: .\n          targets: static-builder-gnu\n          set: |\n            ${{ (github.event_name == 'pull_request' || matrix.platform == 'linux/arm64') && 'static-builder-gnu.args.NO_COMPRESS=1' || '' }}\n            *.tags=\n            *.platform=${{ matrix.platform }}\n            ${{ fromJson(needs.prepare.outputs.push) && '' || format('*.cache-from=type=gha,scope={0}-static-builder-gnu', needs.prepare.outputs.ref || github.ref) }}\n            ${{ fromJson(needs.prepare.outputs.push) && '' || '*.cache-from=type=gha,scope=refs/heads/main-static-builder-gnu' }}\n            ${{ fromJson(needs.prepare.outputs.push) && '' || format('*.cache-to=type=gha,scope={0}-static-builder-gnu,ignore-error=true', needs.prepare.outputs.ref || github.ref) }}\n            ${{ fromJson(needs.prepare.outputs.push) && format('*.output=type=image,name={0},push-by-digest=true,name-canonical=true,push=true', env.IMAGE_NAME) || '' }}\n        env:\n          SHA: ${{ github.sha }}\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n      - # Workaround for https://github.com/actions/runner/pull/2477#issuecomment-1501003600\n        name: Export metadata\n        if: fromJson(needs.prepare.outputs.push)\n        run: |\n          mkdir -p /tmp/metadata-gnu\n\n          # shellcheck disable=SC2086\n          digest=$(jq -r '.\"static-builder-gnu\".\"containerimage.digest\"' <<< ${METADATA})\n          touch \"/tmp/metadata-gnu/${digest#sha256:}\"\n        env:\n          METADATA: ${{ steps.build.outputs.metadata }}\n      - name: Upload metadata\n        if: fromJson(needs.prepare.outputs.push)\n        uses: actions/upload-artifact@v7\n        with:\n          name: metadata-static-builder-gnu-${{ steps.prepare.outputs.sanitized_platform }}\n          path: /tmp/metadata-gnu/*\n          if-no-files-found: error\n          retention-days: 1\n      - name: Copy all frankenphp* files\n        run: |\n          # shellcheck disable=SC2034\n          # TODO: remove \"containerimage.config.digest\" fallback once all runners use buildx v0.18+\n          # which replaced it with \"containerimage.digest\" and \"containerimage.descriptor\"\n          digest=$(jq -r '.\"static-builder-gnu\" | ${{ fromJson(needs.prepare.outputs.push) && '.\"containerimage.digest\"' || '(.\"containerimage.config.digest\" // .\"containerimage.digest\")' }}' <<< \"${METADATA}\")\n          container_id=$(docker create --platform=\"${PLATFORM}\" \"${{ fromJson(needs.prepare.outputs.push) && '${IMAGE_NAME}@${digest}' || '${digest}' }}\")\n          mkdir -p gh-output\n          cd gh-output\n          for file in $(docker run --rm \"${{ fromJson(needs.prepare.outputs.push) && '${IMAGE_NAME}@${digest}' || '${digest}' }}\" sh -c \"ls /go/src/app/dist | grep '^frankenphp'\"); do\n            docker cp \"${container_id}:/go/src/app/dist/${file}\" \"./${file}\"\n          done\n          docker rm \"${container_id}\"\n          mv \"${BINARY}\" \"${BINARY}-gnu\"\n        env:\n          METADATA: ${{ steps.build.outputs.metadata }}\n          BINARY: frankenphp-linux-${{ matrix.platform == 'linux/amd64' && 'x86_64' || 'aarch64' }}\n          PLATFORM: ${{ matrix.platform }}\n      - name: Upload artifact\n        if: ${{ !fromJson(needs.prepare.outputs.push) }}\n        uses: actions/upload-artifact@v7\n        with:\n          name: frankenphp-linux-${{ matrix.platform == 'linux/amd64' && 'x86_64' || 'aarch64' }}-gnu-files\n          path: gh-output/*\n      - name: Upload assets\n        if: fromJson(needs.prepare.outputs.push) && (needs.prepare.outputs.ref || github.ref_type == 'tag')\n        run: gh release upload \"${REF}\" gh-output/* --repo dunglas/frankenphp --clobber\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          REF: ${{ (github.ref_type == 'tag' && github.ref_name) || needs.prepare.outputs.ref }}\n      - if: fromJson(needs.prepare.outputs.push) && (needs.prepare.outputs.ref || github.ref_type == 'tag')\n        uses: actions/attest-build-provenance@v4\n        with:\n          subject-path: ${{ github.workspace }}/gh-output/frankenphp-linux-*-gnu\n      - name: Run sanity checks\n        run: |\n          \"${BINARY}\" version\n          \"${BINARY}\" list-modules | grep frankenphp\n          \"${BINARY}\" list-modules | grep http.encoders.br\n          \"${BINARY}\" list-modules | grep http.handlers.mercure\n          \"${BINARY}\" list-modules | grep http.handlers.mercure\n          \"${BINARY}\" list-modules | grep http.handlers.vulcain\n          \"${BINARY}\" php-cli -r \"echo 'Sanity check passed';\"\n        env:\n          BINARY: ./gh-output/frankenphp-linux-${{ matrix.platform == 'linux/amd64' && 'x86_64' || 'aarch64' }}-gnu\n\n  # Adapted from https://docs.docker.com/build/ci/github-actions/multi-platform/\n  push:\n    runs-on: ubuntu-24.04\n    needs:\n      - prepare\n      - build-linux-musl\n      - build-linux-gnu\n    if: fromJson(needs.prepare.outputs.push)\n    steps:\n      - name: Download metadata\n        uses: actions/download-artifact@v8\n        with:\n          pattern: metadata-static-builder-musl-*\n          path: /tmp/metadata\n          merge-multiple: true\n      - name: Download GNU metadata\n        uses: actions/download-artifact@v8\n        with:\n          pattern: metadata-static-builder-gnu-*\n          path: /tmp/metadata-gnu\n          merge-multiple: true\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v4\n      - name: Login to DockerHub\n        uses: docker/login-action@v4\n        if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository\n        with:\n          username: ${{ secrets.REGISTRY_USERNAME }}\n          password: ${{ secrets.REGISTRY_PASSWORD }}\n      - name: Create manifest list and push\n        working-directory: /tmp/metadata\n        run: |\n          # shellcheck disable=SC2046,SC2086\n          docker buildx imagetools create $(jq -cr '.target.\"static-builder-musl\".tags | map(\"-t \" + .) | join(\" \")' <<< \"${METADATA}\") \\\n            $(printf \"${IMAGE_NAME}@sha256:%s \" *)\n        env:\n          METADATA: ${{ needs.prepare.outputs.metadata }}\n      - name: Create GNU manifest list and push\n        working-directory: /tmp/metadata-gnu\n        run: |\n          # shellcheck disable=SC2046,SC2086\n          docker buildx imagetools create $(jq -cr '.target.\"static-builder-gnu\".tags | map(\"-t \" + .) | join(\" \")' <<< \"${GNU_METADATA}\") \\\n            $(printf \"${IMAGE_NAME}@sha256:%s \" *)\n        env:\n          GNU_METADATA: ${{ needs.prepare.outputs.gnu_metadata }}\n      - name: Inspect image\n        run: |\n          # shellcheck disable=SC2046,SC2086\n          docker buildx imagetools inspect \"$(jq -cr '.target.\"static-builder-musl\".tags | first' <<< \"${METADATA}\")\"\n        env:\n          METADATA: ${{ needs.prepare.outputs.metadata }}\n      - name: Inspect GNU image\n        run: |\n          # shellcheck disable=SC2046,SC2086\n          docker buildx imagetools inspect \"$(jq -cr '.target.\"static-builder-gnu\".tags | first' <<< \"${GNU_METADATA}\")-gnu\"\n        env:\n          GNU_METADATA: ${{ needs.prepare.outputs.gnu_metadata }}\n\n  build-mac:\n    permissions:\n      contents: write\n      id-token: write\n      attestations: write\n    strategy:\n      fail-fast: false\n      matrix:\n        platform: [\"arm64\", \"x86_64\"]\n    name: Build macOS ${{ matrix.platform }} binaries\n    runs-on: ${{ matrix.platform == 'arm64' && 'macos-15' || 'macos-15-intel' }}\n    needs: [prepare]\n    env:\n      HOMEBREW_NO_AUTO_UPDATE: 1\n    steps:\n      - uses: actions/checkout@v6\n        with:\n          ref: ${{ needs.prepare.outputs.ref }}\n          persist-credentials: false\n      - uses: actions/setup-go@v6\n        with: # zizmor: ignore[cache-poisoning]\n          go-version: \"1.26\"\n          cache-dependency-path: |\n            go.sum\n            caddy/go.sum\n          cache: ${{ github.event_name != 'release' }}\n      - name: Set FRANKENPHP_VERSION\n        run: |\n          if [ \"${GITHUB_REF_TYPE}\" == \"tag\" ]; then\n            export FRANKENPHP_VERSION=${GITHUB_REF_NAME:1}\n          elif [ \"${GITHUB_EVENT_NAME}\" == \"schedule\" ]; then\n            export FRANKENPHP_VERSION=\"${REF}\"\n          else\n            export FRANKENPHP_VERSION=${GITHUB_SHA}\n          fi\n\n          echo \"FRANKENPHP_VERSION=${FRANKENPHP_VERSION}\" >> \"${GITHUB_ENV}\"\n        env:\n          REF: ${{ needs.prepare.outputs.ref }}\n      - name: Build FrankenPHP\n        run: ./build-static.sh\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          RELEASE: ${{ (needs.prepare.outputs.ref || github.ref_type == 'tag') && '1' || '' }}\n          NO_COMPRESS: ${{ github.event_name == 'pull_request' && '1' || '' }}\n      - name: Upload logs\n        if: ${{ failure() }}\n        uses: actions/upload-artifact@v7\n        with:\n          path: dist/static-php-cli/log\n          name: static-php-cli-log-${{ matrix.platform }}-${{ github.sha }}\n      - if: needs.prepare.outputs.ref || github.ref_type == 'tag'\n        uses: actions/attest-build-provenance@v4\n        with:\n          subject-path: ${{ github.workspace }}/dist/frankenphp-mac-*\n      - name: Upload artifact\n        if: github.ref_type == 'branch'\n        uses: actions/upload-artifact@v7\n        with:\n          name: frankenphp-mac-${{ matrix.platform }}\n          path: dist/frankenphp-mac-${{ matrix.platform }}\n          compression-level: 0\n      - name: Run sanity checks\n        run: |\n          \"${BINARY}\" version\n          \"${BINARY}\" build-info\n          \"${BINARY}\" list-modules | grep frankenphp\n          \"${BINARY}\" list-modules | grep http.encoders.br\n          \"${BINARY}\" list-modules | grep http.handlers.mercure\n          \"${BINARY}\" list-modules | grep http.handlers.mercure\n          \"${BINARY}\" list-modules | grep http.handlers.vulcain\n          \"${BINARY}\" php-cli -r \"echo 'Sanity check passed';\"\n        env:\n          BINARY: dist/frankenphp-mac-${{ matrix.platform }}\n"
  },
  {
    "path": ".github/workflows/tests.yaml",
    "content": "---\nname: Tests\nconcurrency:\n  cancel-in-progress: true\n  group: ${{ github.workflow }}-${{ github.ref }}\non:\n  pull_request:\n    branches:\n      - main\n    paths-ignore:\n      - \"docs/**\"\n  push:\n    branches:\n      - main\n    paths-ignore:\n      - \"docs/**\"\npermissions:\n  contents: read\nenv:\n  GOTOOLCHAIN: local\n  GOEXPERIMENT: cgocheck2\njobs:\n  tests-linux:\n    name: Tests (Linux, PHP ${{ matrix.php-versions }})\n    runs-on: ubuntu-latest\n    continue-on-error: false\n    strategy:\n      fail-fast: false\n      matrix:\n        include:\n          - php-versions: \"8.2\"\n          - php-versions: \"8.3\"\n          - php-versions: \"8.4\"\n          - php-versions: \"8.5\"\n    env:\n      GOMAXPROCS: 10\n      LIBRARY_PATH: ${{ github.workspace }}/watcher/target/lib\n      GOFLAGS: \"-tags=nobadger,nomysql,nopgx\"\n    steps:\n      - uses: actions/checkout@v6\n        with:\n          persist-credentials: false\n      - uses: actions/setup-go@v6\n        with:\n          go-version: \"1.26\"\n          cache-dependency-path: |\n            go.sum \n            caddy/go.sum\n      - uses: shivammathur/setup-php@v2\n        with:\n          php-version: ${{ matrix.php-versions }}\n          ini-file: development\n          coverage: none\n          tools: none\n        env:\n          phpts: ts\n          debug: true\n      - name: Install e-dant/watcher\n        uses: ./.github/actions/watcher\n      - name: Set CGO flags\n        run: echo \"CGO_CFLAGS=-I${PWD}/watcher/target/include $(php-config --includes)\" >> \"${GITHUB_ENV}\"\n      - name: Build\n        run: go build\n      - name: Build testcli binary\n        working-directory: internal/testcli/\n        run: go build\n      - name: Compile library tests\n        run: go test -race -v -x -c\n      - name: Run library tests\n        run: ./frankenphp.test -test.v\n      - name: Run Caddy module tests\n        working-directory: caddy/\n        run: go test -race -v ./...\n      - name: Run Fuzzing Tests\n        working-directory: caddy/\n        run: go test -fuzz FuzzRequest -fuzztime 20s\n      - name: Build the server\n        working-directory: caddy/frankenphp/\n        run: go build\n      - name: Start the server\n        working-directory: testdata/\n        run: sudo ../caddy/frankenphp/frankenphp start\n      - name: Run integrations tests\n        run: ./reload_test.sh\n      - name: Lint Go code\n        uses: golangci/golangci-lint-action@v9\n        if: matrix.php-versions == '8.5'\n        with:\n          version: latest\n      - name: Ensure go.mod is tidy\n        if: matrix.php-versions == '8.5'\n        run: go mod tidy -diff\n      - name: Ensure caddy/go.mod is tidy\n        if: matrix.php-versions == '8.5'\n        run: go mod tidy -diff\n        working-directory: caddy/\n  integration-tests:\n    name: Integration Tests (Linux, PHP ${{ matrix.php-versions }})\n    runs-on: ubuntu-latest\n    strategy:\n      fail-fast: false\n      matrix:\n        php-versions: [\"8.3\", \"8.4\", \"8.5\"]\n    env:\n      XCADDY_GO_BUILD_FLAGS: \"-tags=nobadger,nomysql,nopgx\"\n    steps:\n      - uses: actions/checkout@v6\n        with:\n          persist-credentials: false\n      - uses: actions/setup-go@v6\n        with:\n          go-version: \"1.26\"\n          cache-dependency-path: |\n            go.sum\n            caddy/go.sum\n      - uses: shivammathur/setup-php@v2\n        with:\n          php-version: ${{ matrix.php-versions }}\n          ini-file: development\n          coverage: none\n          tools: none\n        env:\n          phpts: ts\n          debug: true\n      - name: Install PHP development libraries\n        run: sudo apt-get update && sudo apt-get install -y libkrb5-dev libsodium-dev libargon2-dev\n      - name: Install xcaddy\n        run: go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest\n      - name: Download PHP sources\n        run: |\n          PHP_VERSION=$(php -r \"echo PHP_VERSION;\")\n          wget -q \"https://www.php.net/distributions/php-${PHP_VERSION}.tar.gz\" || wget -q \"https://phpmirror.static-php.dev/distributions/php-${PHP_VERSION}.tar.gz\"\n          tar xzf \"php-${PHP_VERSION}.tar.gz\"\n          echo \"GEN_STUB_SCRIPT=${PWD}/php-${PHP_VERSION}/build/gen_stub.php\" >> \"${GITHUB_ENV}\"\n      - name: Set CGO flags\n        run: |\n          echo \"CGO_CFLAGS=$(php-config --includes)\" >> \"${GITHUB_ENV}\"\n          echo \"CGO_LDFLAGS=$(php-config --ldflags) $(php-config --libs)\" >> \"${GITHUB_ENV}\"\n      - name: Run integration tests\n        working-directory: internal/extgen/\n        run: go test -tags integration -v -timeout 30m\n  tests-mac:\n    name: Tests (macOS, PHP 8.5)\n    runs-on: macos-latest\n    env:\n      HOMEBREW_NO_AUTO_UPDATE: 1\n      GOFLAGS: \"-tags=nowatcher,nobadger,nomysql,nopgx\"\n    steps:\n      - uses: actions/checkout@v6\n        with:\n          persist-credentials: false\n      - uses: actions/setup-go@v6\n        with:\n          go-version: \"1.26\"\n          cache-dependency-path: |\n            go.sum\n            caddy/go.sum\n      - uses: shivammathur/setup-php@v2\n        with:\n          php-version: 8.5\n          ini-file: development\n          coverage: none\n          tools: none\n        env:\n          phpts: ts\n          debug: true\n      - name: Set Set CGO flags\n        run: |\n          {\n           echo \"CGO_CFLAGS=-I/opt/homebrew/include/ $(php-config --includes)\"\n           echo \"CGO_LDFLAGS=-L/opt/homebrew/lib/ $(php-config --ldflags) $(php-config --libs)\"\n          } >> \"${GITHUB_ENV}\"\n      - name: Build\n        run: go build -tags nowatcher\n      - name: Run library tests\n        run: go test -tags nowatcher -race -v ./...\n      - name: Run Caddy module tests\n        working-directory: caddy/\n        run: go test -race -v ./...\n"
  },
  {
    "path": ".github/workflows/translate.yaml",
    "content": "name: Translate Docs\nconcurrency:\n  cancel-in-progress: true\n  group: ${{ github.workflow }}-${{ github.ref }}\non:\n  push:\n    branches:\n      - main\n    paths:\n      - \"docs/*\"\npermissions:\n  contents: write\n  pull-requests: write\njobs:\n  build:\n    name: Translate Docs\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout Code\n        uses: actions/checkout@v6\n        with:\n          fetch-depth: 0\n          # zizmor: ignore[artipacked]\n          # persist-credentials is intentionally left enabled (unlike other workflows)\n          # because this workflow needs to push a branch via git push\n      - id: md_files\n        run: |\n          FILES=$(git diff --name-only \"${{ github.event.before }}\" \"${{ github.sha }}\" -- 'docs/*.md' ':(exclude)docs/*/*.md')\n          FILES=$(echo \"$FILES\" | xargs -n1 basename | tr '\\n' ' ')\n          [ -z \"$FILES\" ] && echo \"found=false\" >> \"$GITHUB_OUTPUT\" || echo \"found=true\" >> \"$GITHUB_OUTPUT\"\n          echo \"files=$FILES\" >> \"$GITHUB_OUTPUT\"\n      - name: Set up PHP\n        if: steps.md_files.outputs.found == 'true'\n        uses: shivammathur/setup-php@v2\n        with:\n          php-version: \"8.5\"\n      - name: run translation script\n        if: steps.md_files.outputs.found == 'true'\n        env:\n          GEMINI_API_KEY: \"${{ secrets.GEMINI_API_KEY }}\"\n          MD_FILES: \"${{ steps.md_files.outputs.files }}\"\n        run: |\n          php ./docs/translate.php \"$MD_FILES\"\n      - name: Run Linter\n        if: steps.md_files.outputs.found == 'true'\n        continue-on-error: true\n        uses: super-linter/super-linter/slim@v8\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          LINTER_RULES_PATH: /\n          MARKDOWN_CONFIG_FILE: .markdown-lint.yaml\n          VALIDATE_NATURAL_LANGUAGE: false\n          FIX_MARKDOWN: true\n      - name: Create Pull Request\n        if: steps.md_files.outputs.found == 'true'\n        env:\n          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          ACTOR: ${{ github.actor }}\n          ACTOR_ID: ${{ github.actor_id }}\n          RUN_ID: ${{ github.run_id }}\n          MD_FILES_LIST: ${{ steps.md_files.outputs.files }}\n        run: |\n          git config user.name \"github-actions[bot]\"\n          git config user.email \"41898282+github-actions[bot]@users.noreply.github.com\"\n          BRANCH=\"translations/$RUN_ID\"\n          git checkout -b \"$BRANCH\"\n          git add docs/\n          git diff --cached --quiet && exit 0\n          git commit -m \"docs: update translations\" --author=\"$ACTOR <$ACTOR_ID+$ACTOR@users.noreply.github.com>\"\n          git push origin \"$BRANCH\"\n          gh pr create \\\n            --title \"docs: update translations\" \\\n            --body \"Translation updates for: $MD_FILES_LIST.\" \\\n            --label \"translations\" \\\n            --label \"bot\"\n"
  },
  {
    "path": ".github/workflows/windows.yaml",
    "content": "---\nname: Build Windows release\n\nconcurrency:\n  cancel-in-progress: true\n  group: ${{ github.workflow }}-${{ github.ref }}\n\non:\n  pull_request:\n    branches:\n      - main\n    paths-ignore:\n      - \"docs/**\"\n  push:\n    branches:\n      - main\n    tags:\n      - v*.*.*\n    paths-ignore:\n      - \"docs/**\"\n  workflow_dispatch:\n    inputs:\n      #checkov:skip=CKV_GHA_7\n      version:\n        description: \"FrankenPHP version\"\n        required: false\n        type: string\n  schedule:\n    - cron: \"0 8 * * *\"\n\npermissions:\n  contents: read\n\nenv:\n  GOTOOLCHAIN: local\n  GOFLAGS: \"-ldflags=-extldflags=-fuse-ld=lld -tags=nobadger,nomysql,nopgx\"\n  PHP_DOWNLOAD_BASE: \"https://downloads.php.net/~windows/releases/\"\n  CC: clang\n  CXX: clang++\n\njobs:\n  build:\n    permissions:\n      contents: write\n    runs-on: windows-latest\n    steps:\n      - name: Determine ref\n        run: |\n          $ref = $env:REF\n          if (-not $ref -and $env:GITHUB_EVENT_NAME -eq \"schedule\") {\n            $ref = (gh release view --repo php/frankenphp --json tagName --jq '.tagName')\n          }\n\n          \"REF=$ref\" >> $env:GITHUB_ENV\n        env:\n          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          REF: ${{ (github.ref_type == 'tag' && github.ref_name) || (github.event_name == 'workflow_dispatch' && inputs.version) || '' }}\n\n      - name: Configure Git\n        run: |\n          git config --global core.autocrlf false\n          git config --global core.eol lf\n\n      - name: Checkout Code\n        uses: actions/checkout@v6\n        with:\n          ref: ${{ env.REF || '' }}\n          path: frankenphp\n          persist-credentials: false\n\n      - name: Set FRANKENPHP_VERSION\n        run: |\n          $ref = $env:REF\n\n          if ($env:GITHUB_REF_TYPE -eq \"tag\") {\n            $frankenphpVersion = $env:GITHUB_REF_NAME.Substring(1)\n          } elseif ($ref) {\n            if ($ref.StartsWith(\"v\")) {\n              $frankenphpVersion = $ref.Substring(1)\n            } else {\n              $frankenphpVersion = $ref\n            }\n          } else {\n            $frankenphpVersion = $env:GITHUB_SHA\n          }\n\n          \"FRANKENPHP_VERSION=$frankenphpVersion\" >> $env:GITHUB_ENV\n\n      - name: Setup Go\n        uses: actions/setup-go@v6\n        with: # zizmor: ignore[cache-poisoning]\n          go-version: \"1.26\"\n          cache-dependency-path: |\n            frankenphp/go.sum\n            frankenphp/caddy/go.sum\n          cache: ${{ !startsWith(github.ref, 'refs/tags/') }}\n          check-latest: true\n\n      - name: Install Vcpkg Libraries\n        working-directory: frankenphp\n        run: \"vcpkg install\"\n\n      - name: Download Watcher\n        run: |\n          $latestTag = gh release list --repo e-dant/watcher --limit 1 --exclude-drafts --exclude-pre-releases --json tagName --jq '.[0].tagName'\n          Write-Host \"Latest Watcher version: $latestTag\"\n\n          gh release download $latestTag --repo e-dant/watcher --pattern \"*x86_64-pc-windows-msvc.tar\" -O watcher.tar\n\n          tar -xf \"watcher.tar\" -C \"$env:GITHUB_WORKSPACE\"\n          Rename-Item -Path \"$env:GITHUB_WORKSPACE\\x86_64-pc-windows-msvc\" -NewName \"watcher\"\n        env:\n          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Download PHP\n        run: |\n          $webContent = Invoke-WebRequest -Uri $env:PHP_DOWNLOAD_BASE\n          $links = $webContent.Links.Href | Where-Object { $_ -match \"php-\\d+\\.\\d+\\.\\d+-Win32-vs17-x64\\.zip$\" }\n\n          if (-not $links) { throw \"Could not find PHP zip files at $env:PHP_DOWNLOAD_BASE\" }\n\n          $latestFile = $links | Sort-Object { if ($_ -match '(\\d+\\.\\d+\\.\\d+)') { [version]$matches[1] } } | Select-Object -Last 1\n\n          $version = if ($latestFile -match '(\\d+\\.\\d+\\.\\d+)') { $matches[1] }\n          Write-Host \"Detected latest PHP version: $version\"\n\n          \"PHP_VERSION=$version\" >> $env:GITHUB_ENV\n\n          $phpZip = \"php-$version-Win32-vs17-x64.zip\"\n          $develZip = \"php-devel-pack-$version-Win32-vs17-x64.zip\"\n\n          $dirName = \"frankenphp-windows-x86_64\"\n\n          \"DIR_NAME=$dirName\" >> $env:GITHUB_ENV\n\n          Invoke-WebRequest -Uri \"$env:PHP_DOWNLOAD_BASE/$phpZip\" -OutFile \"$env:TEMP\\php.zip\"\n          Expand-Archive -Path \"$env:TEMP\\php.zip\" -DestinationPath \"$env:GITHUB_WORKSPACE\\$dirName\"\n\n          Invoke-WebRequest -Uri \"$env:PHP_DOWNLOAD_BASE/$develZip\" -OutFile \"$env:TEMP\\php-devel.zip\"\n          Expand-Archive -Path \"$env:TEMP\\php-devel.zip\" -DestinationPath \"$env:GITHUB_WORKSPACE\\php-devel\"\n\n      - name: Prepare env\n        run: |\n          $vcpkgRoot = \"$env:GITHUB_WORKSPACE\\frankenphp\\vcpkg_installed\\x64-windows\"\n          $watcherRoot = \"$env:GITHUB_WORKSPACE\\watcher\"\n          $phpBin = \"$env:GITHUB_WORKSPACE\\$env:DIR_NAME\"\n          $phpDevel = \"$env:GITHUB_WORKSPACE\\php-devel\\php-$env:PHP_VERSION-devel-vs17-x64\"\n\n          \"C:\\Program Files\\Microsoft Visual Studio\\2022\\Enterprise\\VC\\Tools\\Llvm\\bin\" >> $env:GITHUB_PATH\n          \"$vcpkgRoot\\bin\" >> $env:GITHUB_PATH\n          \"$watcherRoot\" >> $env:GITHUB_PATH\n          \"$phpBin\" >> $env:GITHUB_PATH\n\n          \"CGO_CFLAGS=-DFRANKENPHP_VERSION=$env:FRANKENPHP_VERSION -I$vcpkgRoot\\include -I$watcherRoot -I$phpDevel\\include -I$phpDevel\\include\\main -I$phpDevel\\include\\TSRM -I$phpDevel\\include\\Zend -I$phpDevel\\include\\ext\" >> $env:GITHUB_ENV\n          \"CGO_LDFLAGS=-L$vcpkgRoot\\lib -lbrotlienc -L$watcherRoot -llibwatcher-c -L$phpBin -L$phpDevel\\lib -lphp8ts -lphp8embed\" >> $env:GITHUB_ENV\n\n      - name: Embed Windows icon and metadata\n        working-directory: frankenphp\\caddy\\frankenphp\n        run: |\n          go install github.com/josephspurrier/goversioninfo/cmd/goversioninfo@latest\n\n          $major = 0; $minor = 0; $patch = 0; $build = 0\n          if ($env:FRANKENPHP_VERSION -match '^(?<major>\\d+)\\.(?<minor>\\d+)\\.(?<patch>\\d+)$') {\n            $major = [int]$Matches['major']\n            $minor = [int]$Matches['minor']\n            $patch = [int]$Matches['patch']\n          }\n\n          $json = @{\n            FixedFileInfo = @{\n              FileVersion = @{ Major = $major; Minor = $minor; Patch = $patch; Build = $build }\n              ProductVersion = @{ Major = $major; Minor = $minor; Patch = $patch; Build = $build }\n            }\n            StringFileInfo = @{\n              CompanyName = \"FrankenPHP\"\n              FileDescription = \"The modern PHP app server\"\n              FileVersion = $env:FRANKENPHP_VERSION\n              InternalName = \"frankenphp\"\n              OriginalFilename = \"frankenphp.exe\"\n              LegalCopyright = \"(c) 2022 Kévin Dunglas, MIT License\"\n              ProductName = \"FrankenPHP\"\n              ProductVersion = $env:FRANKENPHP_VERSION\n              Comments = \"https://frankenphp.dev/\"\n            }\n            VarFileInfo = @{\n              Translation = @{ LangID = 9; CharsetID = 1200 }\n            }\n          } | ConvertTo-Json -Depth 10\n          $json | Set-Content \"versioninfo.json\"\n\n          goversioninfo -64 -icon ..\\..\\frankenphp.ico versioninfo.json -o resource.syso\n\n      - name: Build FrankenPHP\n        run: |\n          $customVersion = \"FrankenPHP $env:FRANKENPHP_VERSION PHP $env:PHP_VERSION Caddy\"\n          go build -ldflags=\"-extldflags=-fuse-ld=lld -X 'github.com/caddyserver/caddy/v2.CustomVersion=$customVersion' -X 'github.com/caddyserver/caddy/v2.CustomBinaryName=frankenphp' -X 'github.com/caddyserver/caddy/v2/modules/caddyhttp.ServerHeader=FrankenPHP Caddy'\"\n        working-directory: frankenphp\\caddy\\frankenphp\n\n      - name: Create Directory\n        run: |\n          Copy-Item frankenphp\\caddy\\frankenphp\\frankenphp.exe $env:DIR_NAME\n          Copy-Item watcher\\libwatcher-c.dll $env:DIR_NAME\n          Copy-Item frankenphp\\vcpkg_installed\\x64-windows\\bin\\brotlienc.dll $env:DIR_NAME\n          Copy-Item frankenphp\\vcpkg_installed\\x64-windows\\bin\\brotlidec.dll $env:DIR_NAME\n          Copy-Item frankenphp\\vcpkg_installed\\x64-windows\\bin\\brotlicommon.dll $env:DIR_NAME\n          Copy-Item frankenphp\\vcpkg_installed\\x64-windows\\bin\\pthreadVC3.dll $env:DIR_NAME\n\n      - name: Upload Artifact\n        if: ${{ !env.REF }}\n        uses: actions/upload-artifact@v7\n        with:\n          name: ${{ env.DIR_NAME }}\n          path: ${{ env.DIR_NAME }}\n          if-no-files-found: error\n\n      - name: Zip Release Artifact\n        if: ${{ env.REF }}\n        run: Compress-Archive -Path \"$env:DIR_NAME\\*\" -DestinationPath \"$env:DIR_NAME.zip\"\n\n      - name: Upload Release Asset\n        if: ${{ env.REF }}\n        run: gh release upload \"$env:REF\" \"$env:DIR_NAME.zip\" --repo php/frankenphp --clobber\n        env:\n          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Run Tests\n        run: |\n          \"opcache.enable=0`r`nopcache.enable_cli=0\" | Out-File php.ini\n          $env:PHPRC = Get-Location\n\n          go test -race ./...\n          cd caddy\n          go test -race ./...\n        working-directory: ${{ github.workspace }}\\frankenphp\n"
  },
  {
    "path": ".github/workflows/wrap-issue-details.yaml",
    "content": "name: Wrap Issue Content\non:\n  issues:\n    types: [opened, edited]\n\npermissions:\n  contents: read\n\njobs:\n  wrap_content:\n    runs-on: ubuntu-latest\n    permissions:\n      issues: write\n    steps:\n      - uses: actions/github-script@v8\n        with:\n          script: |\n            const body = context.payload.issue.body;\n\n            const wrapSection = (inputBody, marker, summary) => {\n              const regex = new RegExp(`(${marker})\\\\s*([\\\\s\\\\S]*?)(?=\\\\n### |$)`);\n\n              return inputBody.replace(regex, (match, header, content) => {\n                const trimmed = content.trim();\n                if (!trimmed || trimmed.includes(\"<details>\")) return match;\n\n                return `${header}\\n\\n<details>\\n<summary>${summary}</summary>\\n\\n${trimmed}\\n\\n</details>\\n`;\n              });\n            };\n\n            let newBody = body;\n            newBody = wrapSection(newBody, \"### PHP configuration\", \"phpinfo() output\");\n            newBody = wrapSection(newBody, \"### Relevant log output\", \"Relevant log output\");\n\n            if (newBody !== body) {\n              await github.rest.issues.update({\n                owner: context.repo.owner,\n                repo: context.repo.repo,\n                issue_number: context.issue.number,\n                body: newBody\n              });\n            }\n"
  },
  {
    "path": ".gitignore",
    "content": "/caddy/frankenphp/Build\n/caddy/frankenphp/frankenphp\n/caddy/frankenphp/frankenphp.exe\n/dist\n/github_conf\n/internal/testserver/testserver\n/internal/testcli/testcli\n/package/etc/php.ini\n/super-linter-output\n/vcpkg_installed/\n.DS_Store\n.idea/\n.vscode/\n__debug_bin\nfrankenphp.test\n*.log\ncompile_flags.txt\n"
  },
  {
    "path": ".gitleaksignore",
    "content": "/github/workspace/docs/mercure.md:jwt:88\n/github/workspace/docs/mercure.md:jwt:90\n"
  },
  {
    "path": ".golangci.yaml",
    "content": "---\nversion: \"2\"\nrun:\n  build-tags:\n    - nobadger\n    - nomysql\n    - nopgx\n"
  },
  {
    "path": ".hadolint.yaml",
    "content": "---\nignored:\n  - DL3006\n  - DL3008\n  - DL3018\n  - DL3022\n"
  },
  {
    "path": ".markdown-lint.yaml",
    "content": "---\nMD010: false\nMD013: false\nMD033: false\nMD060: false\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing\n\n## Compiling PHP\n\n### With Docker (Linux)\n\nBuild the dev Docker image:\n\n```console\ndocker build -t frankenphp-dev -f dev.Dockerfile .\ndocker run --cap-add=SYS_PTRACE --security-opt seccomp=unconfined -p 8080:8080 -p 443:443 -p 443:443/udp -v $PWD:/go/src/app -it frankenphp-dev\n```\n\nThe image contains the usual development tools (Go, GDB, Valgrind, Neovim...) and uses the following php setting locations\n\n- php.ini: `/etc/frankenphp/php.ini` A php.ini file with development presets is provided by default.\n- additional configuration files: `/etc/frankenphp/php.d/*.ini`\n- php extensions: `/usr/lib/frankenphp/modules/`\n\nIf your Docker version is lower than 23.0, the build will fail due to dockerignore [pattern issue](https://github.com/moby/moby/pull/42676). Add directories to `.dockerignore`:\n\n```patch\n !testdata/*.php\n !testdata/*.txt\n+!caddy\n+!internal\n```\n\n### Without Docker (Linux and macOS)\n\n[Follow the instructions to compile from sources](https://frankenphp.dev/docs/compile/) and pass the `--debug` configuration flag.\n\n## Running the Test Suite\n\n```console\nexport CGO_CFLAGS=-O0 -g $(php-config --includes) CGO_LDFLAGS=\"$(php-config --ldflags) $(php-config --libs)\"\ngo test -race -v ./...\n```\n\n## Caddy Module\n\nBuild Caddy with the FrankenPHP Caddy module:\n\n```console\ncd caddy/frankenphp/\ngo build -tags nobadger,nomysql,nopgx\ncd ../../\n```\n\nRun the Caddy with the FrankenPHP Caddy module:\n\n```console\ncd testdata/\n../caddy/frankenphp/frankenphp run\n```\n\nThe server is listening on `127.0.0.1:80`:\n\n> [!NOTE]\n> If you are using Docker, you will have to either bind container port 80 or execute from inside the container\n\n```console\ncurl -vk http://127.0.0.1/phpinfo.php\n```\n\n## Minimal Test Server\n\nBuild the minimal test server:\n\n```console\ncd internal/testserver/\ngo build\ncd ../../\n```\n\nRun the test server:\n\n```console\ncd testdata/\n../internal/testserver/testserver\n```\n\nThe server is listening on `127.0.0.1:8080`:\n\n```console\ncurl -v http://127.0.0.1:8080/phpinfo.php\n```\n\n## Windows Development\n\n1. Configure Git to always use `lf` line endings\n\n    ```powershell\n    git config --global core.autocrlf false\n    git config --global core.eol lf\n    ```\n\n2. Install Visual Studio, Git, and Go:\n\n    ```powershell\n    winget install -e --id Microsoft.VisualStudio.2022.Community --override \"--passive --wait --add Microsoft.VisualStudio.Workload.NativeDesktop --add Microsoft.VisualStudio.Component.VC.Llvm.Clang --includeRecommended\"\n    winget install -e --id GoLang.Go\n    winget install -e --id Git.Git\n    ```\n\n3. Install vcpkg:\n\n    ```powershell\n    cd C:\\\n    git clone https://github.com/microsoft/vcpkg\n    .\\vcpkg\\bootstrap-vcpkg.bat\n    ```\n\n4. [Download the latest version of the watcher library for Windows](https://github.com/e-dant/watcher/releases) and extract it to a directory named `C:\\watcher`\n5. [Download the latest **Thread Safe** version of PHP and of the PHP SDK for Windows](https://windows.php.net/download/), extract them in directories named `C:\\php` and `C:\\php-devel`\n6. Clone the FrankenPHP Git repository:\n\n    ```powershell\n    git clone https://github.com/php/frankenphp C:\\frankenphp\n    cd C:\\frankenphp\n    ```\n\n7. Install the dependencies:\n\n    ```powershell\n    C:\\vcpkg\\vcpkg.exe install\n    ```\n\n8. Configure the needed environment variables (PowerShell):\n\n    ```powershell\n    $env:PATH += ';C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\VC\\Tools\\Llvm\\bin'\n    $env:CC = 'clang'\n    $env:CXX = 'clang++'\n    $env:CGO_CFLAGS = \"-O0 -g -IC:\\frankenphp\\vcpkg_installed\\x64-windows\\include -IC:\\watcher -IC:\\php-devel\\include -IC:\\php-devel\\include\\main -IC:\\php-devel\\include\\TSRM -IC:\\php-devel\\include\\Zend -IC:\\php-devel\\include\\ext\"\n    $env:CGO_LDFLAGS = '-LC:\\frankenphp\\vcpkg_installed\\x64-windows\\lib -lbrotlienc -LC:\\watcher -llibwatcher-c -LC:\\php -LC:\\php-devel\\lib -lphp8ts -lphp8embed'\n    ```\n\n9. Run the tests:\n\n    ```powershell\n    go test -race -ldflags '-extldflags=\"-fuse-ld=lld\"' ./...\n    cd caddy\n    go test -race -ldflags '-extldflags=\"-fuse-ld=lld\"' -tags nobadger,nomysql,nopgx ./...\n    cd ..\n    ```\n\n10. Build the binary:\n\n    ```powershell\n    cd caddy/frankenphp\n    go build -ldflags '-extldflags=\"-fuse-ld=lld\"' -tags nobadger,nomysql,nopgx\n    cd ../..\n    ```\n\n## Building Docker Images Locally\n\nPrint Bake plan:\n\n```console\ndocker buildx bake -f docker-bake.hcl --print\n```\n\nBuild FrankenPHP images for amd64 locally:\n\n```console\ndocker buildx bake -f docker-bake.hcl --pull --load --set \"*.platform=linux/amd64\"\n```\n\nBuild FrankenPHP images for arm64 locally:\n\n```console\ndocker buildx bake -f docker-bake.hcl --pull --load --set \"*.platform=linux/arm64\"\n```\n\nBuild FrankenPHP images from scratch for arm64 & amd64 and push to Docker Hub:\n\n```console\ndocker buildx bake -f docker-bake.hcl --pull --no-cache --push\n```\n\n## Debugging Segmentation Faults With Static Builds\n\n1. Download the debug version of the FrankenPHP binary from GitHub or create your custom static build including debug symbols:\n\n   ```console\n   docker buildx bake \\\n       --load \\\n       --set static-builder.args.DEBUG_SYMBOLS=1 \\\n       --set \"static-builder.platform=linux/amd64\" \\\n       static-builder\n   docker cp $(docker create --name static-builder-musl dunglas/frankenphp:static-builder-musl):/go/src/app/dist/frankenphp-linux-$(uname -m) frankenphp\n   ```\n\n2. Replace your current version of `frankenphp` with the debug FrankenPHP executable\n3. Start FrankenPHP as usual (alternatively, you can directly start FrankenPHP with GDB: `gdb --args frankenphp run`)\n4. Attach to the process with GDB:\n\n   ```console\n   gdb -p `pidof frankenphp`\n   ```\n\n5. If necessary, type `continue` in the GDB shell\n6. Make FrankenPHP crash\n7. Type `bt` in the GDB shell\n8. Copy the output\n\n## Debugging Segmentation Faults in GitHub Actions\n\n1. Open `.github/workflows/tests.yml`\n2. Enable PHP debug symbols\n\n   ```patch\n       - uses: shivammathur/setup-php@v2\n         # ...\n         env:\n           phpts: ts\n   +       debug: true\n   ```\n\n3. Enable `tmate` to connect to the container\n\n   ```patch\n       - name: Set CGO flags\n         run: echo \"CGO_CFLAGS=-O0 -g $(php-config --includes)\" >> \"$GITHUB_ENV\"\n   +   - run: |\n   +       sudo apt install gdb\n   +       mkdir -p /home/runner/.config/gdb/\n   +       printf \"set auto-load safe-path /\\nhandle SIG34 nostop noprint pass\" > /home/runner/.config/gdb/gdbinit\n   +   - uses: mxschmitt/action-tmate@v3\n   ```\n\n4. Connect to the container\n5. Open `frankenphp.go`\n6. Enable `cgosymbolizer`\n\n   ```patch\n   -\t//_ \"github.com/ianlancetaylor/cgosymbolizer\"\n   +\t_ \"github.com/ianlancetaylor/cgosymbolizer\"\n   ```\n\n7. Download the module: `go get`\n8. In the container, you can use GDB and the like:\n\n   ```console\n   go test -tags -c -ldflags=-w\n   gdb --args frankenphp.test -test.run ^MyTest$\n   ```\n\n9. When the bug is fixed, revert all these changes\n\n## Development Environment Setup (WSL/Unix)\n\n### Initial setup\n\nFollow the instructions in [compiling from sources](https://frankenphp.dev/docs/compile/).\nThe steps assume the following environment:\n\n- Go installed at `/usr/local/go`\n- PHP source cloned to `~/php-src`\n- PHP built at: `/usr/local/bin/php`\n- FrankenPHP source cloned to `~/frankenphp`\n\n### CLion Setup for CGO glue/PHP Source Development\n\n1. Install CLion (on your host OS)\n\n   - Download from [JetBrains](https://www.jetbrains.com/clion/download/)\n   - Launch (if on Windows, in WSL):\n\n     ```bash\n     clion &>/dev/null\n     ```\n\n2. Open Project in CLion\n\n   - Open CLion → Open → Select the `~/frankenphp` directory\n   - Add a build chain: Settings → Build, Execution, Deployment → Custom Build Targets\n   - Select any Build Target, under `Build` set up an External Tool (call it e.g. go build)\n   - Set up a wrapper script that builds frankenphp for you, called `go_compile_frankenphp.sh`\n\n   ```bash\n   CGO_CFLAGS=\"-O0 -g\" ./go.sh\n   ```\n\n   - Under Program, select `go_compile_frankenphp.sh`\n   - Leave Arguments blank\n   - Working Directory: `~/frankenphp/caddy/frankenphp`\n\n3. Configure Run Targets\n\n   - Go to Run → Edit Configurations\n   - Create:\n     - frankenphp:\n       - Type: Native Application\n       - Target: select the `go build` target you created\n       - Executable: `~/frankenphp/caddy/frankenphp/frankenphp`\n       - Arguments: the arguments you want to start frankenphp with, e.g. `php-cli test.php`\n\n4. Debug Go files from CLion\n\n   - Right click on a *.go file in the Project view on the left\n   - Override file type → C/C++\n\n   Now you can place breakpoints in C, C++ and Go files.\n   To get syntax highlighting for imports from php-src, you may need to tell CLion about the include paths. Create a\n   `compile_flags.txt` file in `~/frankenphp` with the following contents:\n\n   ```gcc\n   -I/usr/local/include/php\n   -I/usr/local/include/php/Zend\n   -I/usr/local/include/php/main\n   -I/usr/local/include/php/TSRM\n   ```\n\n---\n\n### GoLand Setup for FrankenPHP Development\n\nUse GoLand for primary Go development, but the debugger cannot debug C code.\n\n1. Install GoLand (on your host OS)\n\n   - Download from [JetBrains](https://www.jetbrains.com/go/download/)\n\n     ```bash\n     goland &>/dev/null\n     ```\n\n2. Open in GoLand\n\n   - Launch GoLand → Open → Select the `~/frankenphp` directory\n\n---\n\n### Go Configuration\n\n- Select Go Build\n  - Name `frankenphp`\n  - Run kind: Directory\n- Directory: `~/frankenphp/caddy/frankenphp`\n- Output directory: `~/frankenphp/caddy/frankenphp`\n- Working directory: `~/frankenphp/caddy/frankenphp`\n- Environment (adjust for your $(php-config ...) output):\n  `CGO_CFLAGS=-O0 -g -I/usr/local/include/php -I/usr/local/include/php/main -I/usr/local/include/php/TSRM -I/usr/local/include/php/Zend -I/usr/local/include/php/ext -I/usr/local/include/php/ext/date/lib;CGO_LDFLAGS=-lm -lpthread -lsqlite3 -lxml2 -lbrotlienc -lbrotlidec -lbrotlicommon -lwatcher`\n- Go tool arguments: `-tags=nobadger,nomysql,nopgx`\n- Program arguments: e.g. `php-cli -i`\n\nTo debug C files from GoLand\n\n- Right click on a *.c file in the Project view on the left\n- Override file type → Go\n\nNow you can place breakpoints in C, C++ and Go files.\n\n---\n\n### GoLand Setup on Windows\n\n1. Follow the [Windows Development section](#windows-development)\n\n2. Install GoLand\n\n   - Download from [JetBrains](https://www.jetbrains.com/go/download/)\n   - Launch GoLand\n\n3. Open in GoLand\n\n   - Select **Open** → Choose the directory where you cloned `frankenphp`\n\n4. Configure Go Build\n\n   - Go to **Run** → **Edit Configurations**\n   - Click **+** and select **Go Build**\n   - Name: `frankenphp`\n   - Run kind: **Directory**\n   - Directory: `.\\caddy\\frankenphp`\n   - Output directory: `.\\caddy\\frankenphp`\n   - Working directory: `.\\caddy\\frankenphp`\n   - Go tool arguments: `-tags=nobadger,nomysql,nopgx`\n   - Environment variables: see the [Windows Development section](#windows-development)\n   - Program arguments: e.g. `php-server`\n\n---\n\n### Debugging and Integration Notes\n\n- Use CLion for debugging PHP internals and `cgo` glue code\n- Use GoLand for primary Go development and debugging\n- FrankenPHP can be added as a run configuration in CLion for unified C/Go debugging if needed, but syntax highlighting won't work in Go files\n\n## Misc Dev Resources\n\n- [PHP embedding in uWSGI](https://github.com/unbit/uwsgi/blob/master/plugins/php/php_plugin.c)\n- [PHP embedding in NGINX Unit](https://github.com/nginx/unit/blob/master/src/nxt_php_sapi.c)\n- [PHP embedding in Go (go-php)](https://github.com/deuill/go-php)\n- [PHP embedding in Go (GoEmPHP)](https://github.com/mikespook/goemphp)\n- [PHP embedding in C++](https://gist.github.com/paresy/3cbd4c6a469511ac7479aa0e7c42fea7)\n- [Extending and Embedding PHP by Sara Golemon](https://books.google.fr/books?id=zMbGvK17_tYC&pg=PA254&lpg=PA254#v=onepage&q&f=false)\n- [What the heck is TSRMLS_CC, anyway?](http://blog.golemon.com/2006/06/what-heck-is-tsrmlscc-anyway.html)\n- [SDL bindings](https://pkg.go.dev/github.com/veandco/go-sdl2@v0.4.21/sdl#Main)\n\n## Docker-Related Resources\n\n- [Bake file definition](https://docs.docker.com/build/customize/bake/file-definition/)\n- [`docker buildx build`](https://docs.docker.com/engine/reference/commandline/buildx_build/)\n\n## Useful Command\n\n```console\napk add strace util-linux gdb\nstrace -e 'trace=!futex,epoll_ctl,epoll_pwait,tgkill,rt_sigreturn' -p 1\n```\n\n## Translating the Documentation\n\nTo translate the documentation and the site into a new language,\nfollow these steps:\n\n1. Create a new directory named with the language's 2-character ISO code in this repository's `docs/` directory\n2. Copy all the `.md` files in the root of the `docs/` directory into the new directory (always use the English version as source for translation, as it's always up to date)\n3. Copy the `README.md` and `CONTRIBUTING.md` files from the root directory to the new directory\n4. Translate the content of the files, but don't change the filenames, also don't translate strings starting with `> [!` (it's special markup for GitHub)\n5. Create a Pull Request with the translations\n6. In the [site repository](https://github.com/dunglas/frankenphp-website/tree/main), copy and translate the translation files in the `content/`, `data/`, and `i18n/` directories\n7. Translate the values in the created YAML file\n8. Open a Pull Request on the site repository\n"
  },
  {
    "path": "Dockerfile",
    "content": "# syntax=docker/dockerfile:1\n#checkov:skip=CKV_DOCKER_2\n#checkov:skip=CKV_DOCKER_3\n#checkov:skip=CKV_DOCKER_7\nFROM php-base AS common\n\nWORKDIR /app\n\nRUN apt-get update && \\\n\tapt-get -y --no-install-recommends install \\\n\t\tmailcap \\\n\t\tlibcap2-bin \\\n\t&& \\\n\tapt-get clean && \\\n\trm -rf /var/lib/apt/lists/*\n\nRUN set -eux; \\\n\tmkdir -p \\\n\t\t/app/public \\\n\t\t/config/caddy \\\n\t\t/data/caddy \\\n\t\t/etc/caddy \\\n\t\t/etc/frankenphp; \\\n\tsed -i 's/php/frankenphp run/g' /usr/local/bin/docker-php-entrypoint; \\\n\techo '<?php phpinfo();' > /app/public/index.php\n\nCOPY --link caddy/frankenphp/Caddyfile /etc/caddy/Caddyfile\nRUN ln /etc/caddy/Caddyfile /etc/frankenphp/Caddyfile && \\\n\tcurl -sSLf \\\n\t\t-o /usr/local/bin/install-php-extensions \\\n\t\thttps://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions && \\\n\tchmod +x /usr/local/bin/install-php-extensions\n\nCMD [\"--config\", \"/etc/frankenphp/Caddyfile\", \"--adapter\", \"caddyfile\"]\nHEALTHCHECK CMD curl -f http://localhost:2019/metrics || exit 1\n\n# See https://caddyserver.com/docs/conventions#file-locations for details\nENV XDG_CONFIG_HOME=/config\nENV XDG_DATA_HOME=/data\n\nEXPOSE 80\nEXPOSE 443\nEXPOSE 443/udp\nEXPOSE 2019\n\nLABEL org.opencontainers.image.title=FrankenPHP\nLABEL org.opencontainers.image.description=\"The modern PHP app server\"\nLABEL org.opencontainers.image.url=https://frankenphp.dev\nLABEL org.opencontainers.image.source=https://github.com/php/frankenphp\nLABEL org.opencontainers.image.licenses=MIT\nLABEL org.opencontainers.image.vendor=\"Kévin Dunglas\"\n\n\nFROM common AS builder\n\nARG FRANKENPHP_VERSION='dev'\nSHELL [\"/bin/bash\", \"-o\", \"pipefail\", \"-c\"]\n\nCOPY --from=golang-base /usr/local/go /usr/local/go\n\nENV PATH=/usr/local/go/bin:$PATH\nENV GOTOOLCHAIN=local\n\n# This is required to link the FrankenPHP binary to the PHP binary\nRUN apt-get update && \\\n\tapt-get -y --no-install-recommends install \\\n\tcmake \\\n\tgit \\\n\tlibargon2-dev \\\n\tlibbrotli-dev \\\n\tlibcurl4-openssl-dev \\\n\tlibonig-dev \\\n\tlibreadline-dev \\\n\tlibsodium-dev \\\n\tlibsqlite3-dev \\\n\tlibssl-dev \\\n\tlibxml2-dev \\\n\tzlib1g-dev \\\n\t&& \\\n\tapt-get clean\n\n# Install e-dant/watcher (necessary for file watching)\nWORKDIR /usr/local/src/watcher\nRUN --mount=type=secret,id=github-token \\\n    if [ -f /run/secrets/github-token ] && [ -s /run/secrets/github-token ]; then \\\n        curl -s -H \"Authorization: Bearer $(cat /run/secrets/github-token)\" https://api.github.com/repos/e-dant/watcher/releases/latest; \\\n    else \\\n        curl -s https://api.github.com/repos/e-dant/watcher/releases/latest; \\\n    fi | \\\n    grep tarball_url | \\\n    awk '{ print $2 }' | \\\n    sed 's/,$//' | \\\n    sed 's/\"//g' | \\\n    xargs curl -L | \\\n    tar xz --strip-components 1 && \\\n    # -Wno-error=use-after-free: GCC 12 on Bookworm i386 emits a spurious warning in libstdc++ basic_string.h\n    cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_FLAGS=\"-Wno-error=use-after-free\" && \\\n    cmake --build build && \\\n    cmake --install build && \\\n    ldconfig\n\nWORKDIR /go/src/app\n\nCOPY --link go.mod go.sum ./\nRUN go mod download\n\nWORKDIR /go/src/app/caddy\nCOPY --link caddy/go.mod caddy/go.sum ./\nRUN go mod download\n\nWORKDIR /go/src/app\nCOPY --link . ./\n\n# See https://github.com/docker-library/php/blob/master/8.5/trixie/zts/Dockerfile#L57-L59 for PHP values\nENV CGO_CFLAGS=\"-DFRANKENPHP_VERSION=$FRANKENPHP_VERSION $PHP_CFLAGS\"\nENV CGO_CPPFLAGS=$PHP_CPPFLAGS\nENV CGO_LDFLAGS=\"-L/usr/local/lib -lssl -lcrypto -lreadline -largon2 -lcurl -lonig -lz $PHP_LDFLAGS\"\n\nWORKDIR /go/src/app/caddy/frankenphp\nRUN GOBIN=/usr/local/bin \\\n\t../../go.sh install -ldflags \"-w -s -X 'github.com/caddyserver/caddy/v2.CustomVersion=FrankenPHP $FRANKENPHP_VERSION PHP $PHP_VERSION Caddy' -X 'github.com/caddyserver/caddy/v2.CustomBinaryName=frankenphp' -X 'github.com/caddyserver/caddy/v2/modules/caddyhttp.ServerHeader=FrankenPHP Caddy'\" -buildvcs=true && \\\n\tsetcap cap_net_bind_service=+ep /usr/local/bin/frankenphp && \\\n\tcp Caddyfile /etc/frankenphp/Caddyfile && \\\n\tfrankenphp version && \\\n \tfrankenphp build-info\n\nWORKDIR /go/src/app\n\n\nFROM common AS runner\n\nENV GODEBUG=cgocheck=0\n\n# copy watcher shared library\nCOPY --from=builder /usr/local/lib/libwatcher* /usr/local/lib/\n# fix for the file watcher on arm\nRUN apt-get install -y --no-install-recommends libstdc++6 && \\\n\tapt-get clean && \\\n\tldconfig\n\nCOPY --from=builder /usr/local/bin/frankenphp /usr/local/bin/frankenphp\nRUN setcap cap_net_bind_service=+ep /usr/local/bin/frankenphp && \\\n\tfrankenphp version && \\\n\tfrankenphp build-info\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT license\n\nCopyright (c) 2022-present Kévin Dunglas\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is furnished\nto do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# FrankenPHP: Modern App Server for PHP\n\n<h1 align=\"center\"><a href=\"https://frankenphp.dev\"><img src=\"frankenphp.png\" alt=\"FrankenPHP\" width=\"600\"></a></h1>\n\nFrankenPHP is a modern application server for PHP built on top of the [Caddy](https://caddyserver.com/) web server.\n\nFrankenPHP gives superpowers to your PHP apps thanks to its stunning features: [_Early Hints_](https://frankenphp.dev/docs/early-hints/), [worker mode](https://frankenphp.dev/docs/worker/), [real-time capabilities](https://frankenphp.dev/docs/mercure/), [hot reloading](https://frankenphp.dev/docs/hot-reload/), automatic HTTPS, HTTP/2, and HTTP/3 support...\n\nFrankenPHP works with any PHP app and makes your Laravel and Symfony projects faster than ever thanks to their official integrations with the worker mode.\n\nFrankenPHP can also be used as a standalone Go library to embed PHP in any app using `net/http`.\n\n[**Learn more** on _frankenphp.dev_](https://frankenphp.dev) and in this slide deck:\n\n<a href=\"https://dunglas.dev/2022/10/frankenphp-the-modern-php-app-server-written-in-go/\"><img src=\"https://dunglas.dev/wp-content/uploads/2022/10/frankenphp.png\" alt=\"Slides\" width=\"600\"></a>\n\n## Getting Started\n\n### Install Script\n\nOn Linux and macOS, copy this line into your terminal to automatically\ninstall an appropriate version for your platform:\n\n```console\ncurl https://frankenphp.dev/install.sh | sh\n```\n\nOn Windows, run this in PowerShell:\n\n```powershell\nirm https://frankenphp.dev/install.ps1 | iex\n```\n\n### Standalone Binary\n\nWe provide FrankenPHP binaries for Linux, macOS and Windows\ncontaining [PHP 8.5](https://www.php.net/releases/8.5/).\n\nLinux binaries are statically linked, so they can be used on any Linux distribution without installing any dependency. macOS binaries are also self-contained.\nThey contain most popular PHP extensions.\nWindows archives contain the official PHP binary for Windows.\n\n[Download FrankenPHP](https://github.com/php/frankenphp/releases)\n\n### rpm Packages\n\nOur maintainers offer rpm packages for all systems using `dnf`. To install, run:\n\n```console\nsudo dnf install https://rpm.henderkes.com/static-php-1-0.noarch.rpm\nsudo dnf module enable php-zts:static-8.5 # 8.2-8.5 available\nsudo dnf install frankenphp\n```\n\n**Installing extensions:** `sudo dnf install php-zts-<extension>`\n\nFor extensions not available by default, use [PIE](https://github.com/php/pie):\n\n```console\nsudo dnf install pie-zts\nsudo pie-zts install asgrim/example-pie-extension\n```\n\n### deb Packages\n\nOur maintainers offer deb packages for all systems using `apt`. To install, run:\n\n```console\nVERSION=85 # 82-85 available\nsudo curl https://pkg.henderkes.com/api/packages/${VERSION}/debian/repository.key -o /etc/apt/keyrings/static-php${VERSION}.asc\necho \"deb [signed-by=/etc/apt/keyrings/static-php${VERSION}.asc] https://pkg.henderkes.com/api/packages/${VERSION}/debian php-zts main\" | sudo tee -a /etc/apt/sources.list.d/static-php${VERSION}.list\nsudo apt update\nsudo apt install frankenphp\n```\n\n**Installing extensions:** `sudo apt install php-zts-<extension>`\n\nFor extensions not available by default, use [PIE](https://github.com/php/pie):\n\n```console\nsudo apt install pie-zts\nsudo pie-zts install asgrim/example-pie-extension\n```\n\n### apk Packages\n\nOur maintainers offer apk packages for all systems using `apk`. To install, run:\n\n```console\nVERSION=85 # 82-85 available\necho \"https://pkg.henderkes.com/api/packages/${VERSION}/alpine/main/php-zts\" | sudo tee -a /etc/apk/repositories\nKEYFILE=$(curl -sJOw '%{filename_effective}' https://pkg.henderkes.com/api/packages/${VERSION}/alpine/key)\nsudo mv ${KEYFILE} /etc/apk/keys/ && \nsudo apk update && \nsudo apk add frankenphp\n```\n\n**Installing extensions:** `sudo apk add php-zts-<extension>`\n\nFor extensions not available by default, use [PIE](https://github.com/php/pie):\n\n```console\nsudo apk add pie-zts\nsudo pie-zts install asgrim/example-pie-extension\n```\n\n### Homebrew\n\nFrankenPHP is also available as a [Homebrew](https://brew.sh) package for macOS and Linux.\n\n```console\nbrew install dunglas/frankenphp/frankenphp\n```\n\n**Installing extensions:** Use [PIE](https://github.com/php/pie).\n\n### Usage\n\nTo serve the content of the current directory, run:\n\n```console\nfrankenphp php-server\n```\n\nYou can also run command-line scripts with:\n\n```console\nfrankenphp php-cli /path/to/your/script.php\n```\n\nFor the deb and rpm packages, you can also start the systemd service:\n\n```console\nsudo systemctl start frankenphp\n```\n\n### Docker\n\nAlternatively, [Docker images](https://frankenphp.dev/docs/docker/) are available:\n\n```console\ndocker run -v .:/app/public \\\n    -p 80:80 -p 443:443 -p 443:443/udp \\\n    dunglas/frankenphp\n```\n\nGo to `https://localhost`, and enjoy!\n\n> [!TIP]\n>\n> Do not attempt to use `https://127.0.0.1`. Use `https://localhost` and accept the self-signed certificate.\n> Use the [`SERVER_NAME` environment variable](docs/config.md#environment-variables) to change the domain to use.\n\n## Docs\n\n- [Classic mode](https://frankenphp.dev/docs/classic/)\n- [Worker mode](https://frankenphp.dev/docs/worker/)\n- [Early Hints support (103 HTTP status code)](https://frankenphp.dev/docs/early-hints/)\n- [Real-time](https://frankenphp.dev/docs/mercure/)\n- [Logging](https://frankenphp.dev/docs/logging/)\n- [Hot reloading](https://frankenphp.dev/docs/hot-reload/)\n- [Efficiently Serving Large Static Files](https://frankenphp.dev/docs/x-sendfile/)\n- [Configuration](https://frankenphp.dev/docs/config/)\n- [Writing PHP Extensions in Go](https://frankenphp.dev/docs/extensions/)\n- [Docker images](https://frankenphp.dev/docs/docker/)\n- [Deploy in production](https://frankenphp.dev/docs/production/)\n- [Performance optimization](https://frankenphp.dev/docs/performance/)\n- [Create **standalone**, self-executable PHP apps](https://frankenphp.dev/docs/embed/)\n- [Create static binaries](https://frankenphp.dev/docs/static/)\n- [Compile from sources](https://frankenphp.dev/docs/compile/)\n- [Monitoring FrankenPHP](https://frankenphp.dev/docs/metrics/)\n- [WordPress integration](https://frankenphp.dev/docs/wordpress/)\n- [Laravel integration](https://frankenphp.dev/docs/laravel/)\n- [Known issues](https://frankenphp.dev/docs/known-issues/)\n- [Demo app (Symfony) and benchmarks](https://github.com/dunglas/frankenphp-demo)\n- [Go library documentation](https://pkg.go.dev/github.com/dunglas/frankenphp)\n- [Contributing and debugging](https://frankenphp.dev/docs/contributing/)\n\n## Examples and Skeletons\n\n- [Symfony](https://github.com/dunglas/symfony-docker)\n- [API Platform](https://api-platform.com/docs/symfony)\n- [Laravel](https://frankenphp.dev/docs/laravel/)\n- [Sulu](https://sulu.io/blog/running-sulu-with-frankenphp)\n- [WordPress](https://github.com/StephenMiracle/frankenwp)\n- [Drupal](https://github.com/dunglas/frankenphp-drupal)\n- [Joomla](https://github.com/alexandreelise/frankenphp-joomla)\n- [TYPO3](https://github.com/ochorocho/franken-typo3)\n- [Magento2](https://github.com/ekino/frankenphp-magento2)\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Security Policy\n\n## Supported Versions\n\nOnly the latest version is supported.\nPlease ensure that you're always using the latest release.\n\nBinaries and Docker images are rebuilt nightly using the latest versions of dependencies.\n\n## Reporting a Vulnerability\n\nIf you believe you have discovered a security issue directly affecting FrankenPHP,\nplease do **NOT** report it publicly.\n\nPlease write a detailed vulnerability report and send it [through GitHub](https://github.com/php/frankenphp/security/advisories/new) or to [kevin+frankenphp-security@dunglas.dev](mailto:kevin+frankenphp-security@dunglas.dev?subject=Security%20issue%20affecting%20FrankenPHP).\n\nOnly vulnerabilities directly affecting FrankenPHP should be reported to this project.\nFlaws affecting components used by FrankenPHP (PHP, Caddy, Go...) or using FrankenPHP (Laravel Octane, PHP Runtime...) should be reported to the relevant projects.\n"
  },
  {
    "path": "alpine.Dockerfile",
    "content": "# syntax=docker/dockerfile:1\n#checkov:skip=CKV_DOCKER_2\n#checkov:skip=CKV_DOCKER_3\n#checkov:skip=CKV_DOCKER_7\nFROM php-base AS common\n\nARG TARGETARCH\n\nWORKDIR /app\n\nRUN apk add --no-cache \\\n\tca-certificates \\\n\tlibcap \\\n\tmailcap\n\nRUN set -eux; \\\n\tmkdir -p \\\n\t\t/app/public \\\n\t\t/config/caddy \\\n\t\t/data/caddy \\\n\t\t/etc/caddy \\\n\t\t/etc/frankenphp; \\\n\tsed -i 's/php/frankenphp run/g' /usr/local/bin/docker-php-entrypoint; \\\n\techo '<?php phpinfo();' > /app/public/index.php\n\nCOPY --link caddy/frankenphp/Caddyfile /etc/caddy/Caddyfile\n\nRUN ln /etc/caddy/Caddyfile /etc/frankenphp/Caddyfile && \\\n\tcurl -sSLf \\\n\t\t-o /usr/local/bin/install-php-extensions \\\n\t\thttps://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions && \\\n\tchmod +x /usr/local/bin/install-php-extensions\n\nCMD [\"--config\", \"/etc/frankenphp/Caddyfile\", \"--adapter\", \"caddyfile\"]\nHEALTHCHECK CMD curl -f http://localhost:2019/metrics || exit 1\n\n# See https://caddyserver.com/docs/conventions#file-locations for details\nENV XDG_CONFIG_HOME=/config\nENV XDG_DATA_HOME=/data\n\nEXPOSE 80\nEXPOSE 443\nEXPOSE 443/udp\nEXPOSE 2019\n\nLABEL org.opencontainers.image.title=FrankenPHP\nLABEL org.opencontainers.image.description=\"The modern PHP app server\"\nLABEL org.opencontainers.image.url=https://frankenphp.dev\nLABEL org.opencontainers.image.source=https://github.com/php/frankenphp\nLABEL org.opencontainers.image.licenses=MIT\nLABEL org.opencontainers.image.vendor=\"Kévin Dunglas\"\n\n\nFROM common AS builder\n\nARG FRANKENPHP_VERSION='dev'\nARG NO_COMPRESS=''\nSHELL [\"/bin/ash\", \"-eo\", \"pipefail\", \"-c\"]\n\nCOPY --link --from=golang-base /usr/local/go /usr/local/go\n\nENV PATH=/usr/local/go/bin:$PATH\nENV GOTOOLCHAIN=local\n\n# hadolint ignore=SC2086\nRUN apk add --no-cache --virtual .build-deps \\\n\t$PHPIZE_DEPS \\\n\targon2-dev \\\n\t# Needed for the custom Go build \\\n\tbash \\\n\tbrotli-dev \\\n\tcoreutils \\\n\tcurl-dev \\\n\t# Needed for the custom Go build \\\n\tgit \\\n\tgnu-libiconv-dev \\\n\tlibsodium-dev \\\n\t# Needed for the file watcher \\\n\tcmake \\\n\tlibstdc++ \\\n\tlibxml2-dev \\\n\tlinux-headers \\\n\toniguruma-dev \\\n\topenssl-dev \\\n\treadline-dev \\\n\tsqlite-dev \\\n\tupx\n\n# Install e-dant/watcher (necessary for file watching)\nWORKDIR /usr/local/src/watcher\nRUN --mount=type=secret,id=github-token \\\n\t\tif [ -f /run/secrets/github-token ] && [ -s /run/secrets/github-token ]; then \\\n\t\t\t\tcurl -s -H \"Authorization: Bearer $(cat /run/secrets/github-token)\" https://api.github.com/repos/e-dant/watcher/releases/latest; \\\n\t\telse \\\n\t\t\t\tcurl -s https://api.github.com/repos/e-dant/watcher/releases/latest; \\\n\t\tfi | \\\n\t\tgrep tarball_url | \\\n\t\tawk '{ print $2 }' | \\\n\t\tsed 's/,$//' | \\\n\t\tsed 's/\"//g' | \\\n\t\txargs curl -L | \\\n\t\ttar xz --strip-components 1 && \\\n\t\tcmake -S . -B build -DCMAKE_BUILD_TYPE=Release && \\\n\t\tcmake --build build && \\\n\t\tcmake --install build\n\nWORKDIR /go/src/app\n\nCOPY --link go.mod go.sum ./\nRUN go mod download\n\nWORKDIR /go/src/app/caddy\nCOPY caddy/go.mod caddy/go.sum ./\nRUN go mod download\n\nWORKDIR /go/src/app\nCOPY --link . ./\n\n# See https://github.com/docker-library/php/blob/master/8.3/alpine3.20/zts/Dockerfile#L53-L55\nENV CGO_CFLAGS=\"-DFRANKENPHP_VERSION=$FRANKENPHP_VERSION $PHP_CFLAGS\"\nENV CGO_CPPFLAGS=$PHP_CPPFLAGS\nENV CGO_LDFLAGS=\"-lssl -lcrypto -lreadline -largon2 -lcurl -lonig -lz $PHP_LDFLAGS\"\n\nWORKDIR /go/src/app/caddy/frankenphp\nRUN GOBIN=/usr/local/bin \\\n\t\t../../go.sh install -ldflags \"-w -s -extldflags '-Wl,-z,stack-size=0x80000' -X 'github.com/caddyserver/caddy/v2.CustomVersion=FrankenPHP $FRANKENPHP_VERSION PHP $PHP_VERSION Caddy' -X 'github.com/caddyserver/caddy/v2.CustomBinaryName=frankenphp' -X 'github.com/caddyserver/caddy/v2/modules/caddyhttp.ServerHeader=FrankenPHP Caddy'\" -buildvcs=true && \\\n\tsetcap cap_net_bind_service=+ep /usr/local/bin/frankenphp && \\\n\t([ -z \"${NO_COMPRESS}\" ] && upx --best /usr/local/bin/frankenphp || true) && \\\n\tfrankenphp version && \\\n\tfrankenphp build-info\n\nWORKDIR /go/src/app\n\n\nFROM common AS runner\n\nENV GODEBUG=cgocheck=0\n\n# copy watcher shared library (libgcc and libstdc++ are needed for the watcher)\nCOPY --from=builder /usr/local/lib/libwatcher* /usr/local/lib/\nRUN apk add --no-cache libstdc++ && \\\n\tldconfig /usr/local/lib\n\nCOPY --from=builder /usr/local/bin/frankenphp /usr/local/bin/frankenphp\nRUN setcap cap_net_bind_service=+ep /usr/local/bin/frankenphp && \\\n\tfrankenphp version && \\\n\tfrankenphp build-info\n"
  },
  {
    "path": "app_checksum.txt",
    "content": ""
  },
  {
    "path": "build-static.sh",
    "content": "#!/bin/bash\n\nset -o errexit\nset -x\n\nif ! type \"git\" >/dev/null 2>&1; then\n\techo \"The \\\"git\\\" command must be installed.\"\n\texit 1\nfi\n\nCURRENT_DIR=$(pwd)\n\narch=\"$(uname -m)\"\nos=\"$(uname -s | tr '[:upper:]' '[:lower:]')\"\n[ \"$os\" = \"darwin\" ] && os=\"mac\"\n\n# Supported variables:\n# - PHP_VERSION: PHP version to build (default: \"8.4\")\n# - PHP_EXTENSIONS: PHP extensions to build (default: ${defaultExtensions} set below)\n# - PHP_EXTENSION_LIBS: PHP extension libraries to build (default: ${defaultExtensionLibs} set below)\n# - FRANKENPHP_VERSION: FrankenPHP version (default: current Git commit)\n# - EMBED: Path to the PHP app to embed (default: none)\n# - DEBUG_SYMBOLS: Enable debug symbols if set to 1 (default: none)\n# - MIMALLOC: Use mimalloc as the allocator if set to 1 (default: none)\n# - XCADDY_ARGS: Additional arguments to pass to xcaddy\n# - RELEASE: [maintainer only] Create a GitHub release if set to 1 (default: none)\n\n# - SPC_REL_TYPE: Release type to download (accept \"source\" and \"binary\", default: \"source\")\n# - SPC_OPT_BUILD_ARGS: Additional arguments to pass to spc build\n# - SPC_OPT_DOWNLOAD_ARGS: Additional arguments to pass to spc download\n# - SPC_LIBC: Set to glibc to build with GNU toolchain (default: musl)\n\n# init spc command, if we use spc binary, just use it instead of fetching source\nif [ -z \"${SPC_REL_TYPE}\" ]; then\n\tSPC_REL_TYPE=\"source\"\nfi\n# init spc libc\nif [ -z \"${SPC_LIBC}\" ]; then\n\tif [ \"${os}\" = \"linux\" ]; then\n\t\tSPC_LIBC=\"musl\"\n\tfi\nfi\n# init spc build additional args\nif [ -z \"${SPC_OPT_BUILD_ARGS}\" ]; then\n\tSPC_OPT_BUILD_ARGS=\"\"\nfi\nif [ \"${SPC_LIBC}\" = \"musl\" ] && [[ \"${SPC_OPT_BUILD_ARGS}\" != *\"--disable-opcache-jit\"* ]]; then\n\tSPC_OPT_BUILD_ARGS=\"${SPC_OPT_BUILD_ARGS} --disable-opcache-jit\"\nfi\n# init spc download additional args\nif [ -z \"${SPC_OPT_DOWNLOAD_ARGS}\" ]; then\n\tSPC_OPT_DOWNLOAD_ARGS=\"--ignore-cache-sources=php-src --retry 5\"\n\tif [ \"${SPC_LIBC}\" = \"musl\" ]; then\n\t\tSPC_OPT_DOWNLOAD_ARGS=\"${SPC_OPT_DOWNLOAD_ARGS} --prefer-pre-built\"\n\tfi\nfi\n# if we need debug symbols, disable strip\nif [ -n \"${DEBUG_SYMBOLS}\" ]; then\n\tSPC_OPT_BUILD_ARGS=\"${SPC_OPT_BUILD_ARGS} --no-strip\"\nfi\n# php version to build\nif [ -z \"${PHP_VERSION}\" ]; then\n\tget_latest_php_version() {\n\t\tinput=\"$1\"\n\t\tjson=$(curl -fsSL \"https://www.php.net/releases/index.php?json&version=$input\" 2>/dev/null || curl -fsSL \"https://phpmirror.static-php.dev/releases/index.php?json&version=$input\")\n\t\tlatest=$(echo \"$json\" | jq -r '.version')\n\n\t\tif [[ \"$latest\" == \"$input\"* ]]; then\n\t\t\techo \"$latest\"\n\t\telse\n\t\t\techo \"$input\"\n\t\tfi\n\t}\n\n\tPHP_VERSION=\"$(get_latest_php_version \"8.5\")\"\n\texport PHP_VERSION\nfi\n# default extension set\ndefaultExtensions=\"amqp,apcu,ast,bcmath,brotli,bz2,calendar,ctype,curl,dba,dom,exif,fileinfo,filter,ftp,gd,gmp,gettext,iconv,igbinary,imagick,intl,ldap,lz4,mbregex,mbstring,memcached,mysqli,mysqlnd,opcache,openssl,password-argon2,parallel,pcntl,pdo,pdo_mysql,pdo_pgsql,pdo_sqlite,pgsql,phar,posix,protobuf,readline,redis,session,shmop,simplexml,soap,sockets,sodium,sqlite3,ssh2,sysvmsg,sysvsem,sysvshm,tidy,tokenizer,xlswriter,xml,xmlreader,xmlwriter,xsl,xz,zip,zlib,yaml,zstd\"\ndefaultExtensionLibs=\"libavif,nghttp2,nghttp3,ngtcp2,watcher\"\n\nif [ -z \"${FRANKENPHP_VERSION}\" ]; then\n\tFRANKENPHP_VERSION=\"$(git rev-parse --verify HEAD)\"\n\texport FRANKENPHP_VERSION\nelif [ -d \".git/\" ]; then\n\tCURRENT_REF=\"$(git rev-parse --abbrev-ref HEAD)\"\n\texport CURRENT_REF\n\n\tif echo \"${FRANKENPHP_VERSION}\" | grep -F -q \".\"; then\n\t\t# Tag\n\n\t\t# Trim \"v\" prefix if any\n\t\tFRANKENPHP_VERSION=${FRANKENPHP_VERSION#v}\n\t\texport FRANKENPHP_VERSION\n\n\t\tgit checkout \"v${FRANKENPHP_VERSION}\"\n\telse\n\t\tgit checkout \"${FRANKENPHP_VERSION}\"\n\tfi\nfi\n\nif [ -n \"${CLEAN}\" ]; then\n\trm -Rf dist/\n\tgo clean -cache\nfi\n\nmkdir -p dist/\ncd dist/\n\nif type \"brew\" >/dev/null 2>&1; then\n\tif ! type \"composer\" >/dev/null; then\n\t\tpackages=\"composer\"\n\tfi\n\tif ! type \"go\" >/dev/null 2>&1; then\n\t\tpackages=\"${packages} go\"\n\tfi\n\tif [ -n \"${RELEASE}\" ] && ! type \"gh\" >/dev/null 2>&1; then\n\t\tpackages=\"${packages} gh\"\n\tfi\n\n\tif [ -n \"${packages}\" ]; then\n\t\t# shellcheck disable=SC2086\n\t\tbrew install --formula --quiet ${packages}\n\tfi\nfi\n\nif [ \"${SPC_REL_TYPE}\" = \"binary\" ]; then\n\tmkdir -p static-php-cli/\n\tcd static-php-cli/\n\tif [[ \"${arch}\" =~ \"arm\" ]]; then\n\t\tdl_arch=\"aarch64\"\n\telse\n\t\tdl_arch=\"${arch}\"\n\tfi\n\tcurl -o spc -fsSL \"https://dl.static-php.dev/static-php-cli/spc-bin/nightly/spc-linux-${dl_arch}\"\n\tchmod +x spc\n\tspcCommand=\"./spc\"\nelif [ -d \"static-php-cli/src\" ]; then\n\tcd static-php-cli/\n\tgit pull\n\tcomposer install --no-dev -a --no-interaction\n\tspcCommand=\"./bin/spc\"\nelse\n\tgit clone --depth 1 https://github.com/crazywhalecc/static-php-cli --branch main\n\tcd static-php-cli/\n\tcomposer install --no-dev -a --no-interaction\n\tspcCommand=\"./bin/spc\"\nfi\n\n# turn potentially relative EMBED path into absolute path\nif [ -n \"${EMBED}\" ]; then\n\tif [[ \"${EMBED}\" != /* ]]; then\n\t\tEMBED=\"${CURRENT_DIR}/${EMBED}\"\n\tfi\nfi\n\n# Extensions to build\nif [ -z \"${PHP_EXTENSIONS}\" ]; then\n\t# enable EMBED mode, first check if project has dumped extensions\n\tif [ -n \"${EMBED}\" ] && [ -f \"${EMBED}/composer.json\" ] && [ -f \"${EMBED}/composer.lock\" ] && [ -f \"${EMBED}/vendor/composer/installed.json\" ]; then\n\t\tcd \"${EMBED}\"\n\t\t# read the extensions using spc dump-extensions\n\t\tPHP_EXTENSIONS=$(${spcCommand} dump-extensions \"${EMBED}\" --format=text --no-dev --no-ext-output=\"${defaultExtensions}\")\n\telse\n\t\tPHP_EXTENSIONS=\"${defaultExtensions}\"\n\tfi\nfi\n\n# Additional libraries to build\nif [ -z \"${PHP_EXTENSION_LIBS}\" ]; then\n\tPHP_EXTENSION_LIBS=\"${defaultExtensionLibs}\"\nfi\n\n# The Brotli library must always be built as it is required by http://github.com/dunglas/caddy-cbrotli\nif ! echo \"${PHP_EXTENSION_LIBS}\" | grep -q \"\\bbrotli\\b\"; then\n\tPHP_EXTENSION_LIBS=\"${PHP_EXTENSION_LIBS},brotli\"\nfi\n\n# The mimalloc library must be built if MIMALLOC is true\nif [ -n \"${MIMALLOC}\" ]; then\n\tif ! echo \"${PHP_EXTENSION_LIBS}\" | grep -q \"\\bmimalloc\\b\"; then\n\t\tPHP_EXTENSION_LIBS=\"${PHP_EXTENSION_LIBS},mimalloc\"\n\tfi\nfi\n\n# Embed PHP app, if any\nif [ -n \"${EMBED}\" ] && [ -d \"${EMBED}\" ]; then\n\t# shellcheck disable=SC2089\n\tSPC_OPT_BUILD_ARGS=\"${SPC_OPT_BUILD_ARGS} --with-frankenphp-app='${EMBED}'\"\nfi\n\nSPC_OPT_INSTALL_ARGS=\"go-xcaddy\"\nif [ -z \"${DEBUG_SYMBOLS}\" ] && [ -z \"${NO_COMPRESS}\" ] && [ \"${os}\" = \"linux\" ]; then\n\tSPC_OPT_BUILD_ARGS=\"${SPC_OPT_BUILD_ARGS} --with-upx-pack\"\n\tSPC_OPT_INSTALL_ARGS=\"${SPC_OPT_INSTALL_ARGS} upx\"\nfi\n\nexport SPC_DEFAULT_C_FLAGS=\"-fPIC -O2\"\nif [ -n \"${DEBUG_SYMBOLS}\" ]; then\n\tSPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS=\"${SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS} -fPIE -g\"\nelse\n\tSPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS=\"${SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS} -fPIE -fstack-protector-strong -O2 -w -s\"\nfi\nexport SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS\nif [ -z \"$SPC_CMD_VAR_FRANKENPHP_XCADDY_MODULES\" ]; then\n\texport SPC_CMD_VAR_FRANKENPHP_XCADDY_MODULES=\"--with github.com/dunglas/mercure/caddy --with github.com/dunglas/vulcain/caddy --with github.com/dunglas/caddy-cbrotli\"\nfi\n\n# Build FrankenPHP\n${spcCommand} doctor --auto-fix\nfor pkg in ${SPC_OPT_INSTALL_ARGS}; do\n\t${spcCommand} install-pkg \"${pkg}\"\ndone\n# shellcheck disable=SC2086\n${spcCommand} download --with-php=\"${PHP_VERSION}\" --for-extensions=\"${PHP_EXTENSIONS}\" --for-libs=\"${PHP_EXTENSION_LIBS}\" ${SPC_OPT_DOWNLOAD_ARGS}\nexport FRANKENPHP_SOURCE_PATH=\"${CURRENT_DIR}\"\n# shellcheck disable=SC2086,SC2090\n${spcCommand} build --enable-zts --build-embed --build-frankenphp ${SPC_OPT_BUILD_ARGS} \"${PHP_EXTENSIONS}\" --with-libs=\"${PHP_EXTENSION_LIBS}\"\n\nif [ -n \"$CI\" ]; then\n\trm -rf ./downloads\n\trm -rf ./source\nfi\n\ncd ../..\n\nbin=\"dist/frankenphp-${os}-${arch}\"\ncp \"dist/static-php-cli/buildroot/bin/frankenphp\" \"${bin}\"\n\"${bin}\" version\n\"${bin}\" build-info\n\nif [ -n \"${RELEASE}\" ]; then\n\tgh release upload \"v${FRANKENPHP_VERSION}\" \"${bin}\" --repo dunglas/frankenphp --clobber\nfi\n\nif [ -n \"${CURRENT_REF}\" ]; then\n\tgit checkout \"${CURRENT_REF}\"\nfi\n"
  },
  {
    "path": "caddy/admin.go",
    "content": "package caddy\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"github.com/caddyserver/caddy/v2\"\n\t\"github.com/dunglas/frankenphp\"\n)\n\ntype FrankenPHPAdmin struct {\n}\n\n// if the id starts with \"admin.api\" the module will register AdminRoutes via module.Routes()\nfunc (FrankenPHPAdmin) CaddyModule() caddy.ModuleInfo {\n\treturn caddy.ModuleInfo{\n\t\tID:  \"admin.api.frankenphp\",\n\t\tNew: func() caddy.Module { return new(FrankenPHPAdmin) },\n\t}\n}\n\n// EXPERIMENTAL: These routes are not yet stable and may change in the future.\nfunc (admin FrankenPHPAdmin) Routes() []caddy.AdminRoute {\n\treturn []caddy.AdminRoute{\n\t\t{\n\t\t\tPattern: \"/frankenphp/workers/restart\",\n\t\t\tHandler: caddy.AdminHandlerFunc(admin.restartWorkers),\n\t\t},\n\t\t{\n\t\t\tPattern: \"/frankenphp/threads\",\n\t\t\tHandler: caddy.AdminHandlerFunc(admin.threads),\n\t\t},\n\t}\n}\n\nfunc (admin *FrankenPHPAdmin) restartWorkers(w http.ResponseWriter, r *http.Request) error {\n\tif r.Method != http.MethodPost {\n\t\treturn admin.error(http.StatusMethodNotAllowed, fmt.Errorf(\"method not allowed\"))\n\t}\n\n\tfrankenphp.RestartWorkers()\n\tcaddy.Log().Info(\"workers restarted from admin api\")\n\tadmin.success(w, \"workers restarted successfully\\n\")\n\n\treturn nil\n}\n\nfunc (admin *FrankenPHPAdmin) threads(w http.ResponseWriter, _ *http.Request) error {\n\tdebugState := frankenphp.DebugState()\n\tprettyJson, err := json.MarshalIndent(debugState, \"\", \"    \")\n\tif err != nil {\n\t\treturn admin.error(http.StatusInternalServerError, err)\n\t}\n\n\treturn admin.success(w, string(prettyJson))\n}\n\nfunc (admin *FrankenPHPAdmin) success(w http.ResponseWriter, message string) error {\n\tw.WriteHeader(http.StatusOK)\n\t_, err := w.Write([]byte(message))\n\treturn err\n}\n\nfunc (admin *FrankenPHPAdmin) error(statusCode int, err error) error {\n\treturn caddy.APIError{HTTPStatus: statusCode, Err: err}\n}\n"
  },
  {
    "path": "caddy/admin_test.go",
    "content": "package caddy_test\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"strings\"\n\t\"sync\"\n\t\"testing\"\n\n\t\"github.com/dunglas/frankenphp/internal/fastabs\"\n\n\t\"github.com/caddyserver/caddy/v2/caddytest\"\n\t\"github.com/dunglas/frankenphp\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestRestartWorkerViaAdminApi(t *testing.T) {\n\ttester := caddytest.NewTester(t)\n\ttester.InitServer(`\n\t\t{\n\t\t\tskip_install_trust\n\t\t\tadmin localhost:2999\n\t\t\thttp_port `+testPort+`\n\n\t\t\tfrankenphp {\n\t\t\t\tworker ../testdata/worker-with-counter.php 1\n\t\t\t}\n\t\t}\n\n\t\tlocalhost:`+testPort+` {\n\t\t\troute {\n\t\t\t\troot ../testdata\n\t\t\t\trewrite worker-with-counter.php\n\t\t\t\tphp\n\t\t\t}\n\t\t}\n\t\t`, \"caddyfile\")\n\n\ttester.AssertGetResponse(\"http://localhost:\"+testPort+\"/\", http.StatusOK, \"requests:1\")\n\ttester.AssertGetResponse(\"http://localhost:\"+testPort+\"/\", http.StatusOK, \"requests:2\")\n\n\tassertAdminResponse(t, tester, \"POST\", \"workers/restart\", http.StatusOK, \"workers restarted successfully\\n\")\n\n\ttester.AssertGetResponse(\"http://localhost:\"+testPort+\"/\", http.StatusOK, \"requests:1\")\n}\n\nfunc TestShowTheCorrectThreadDebugStatus(t *testing.T) {\n\ttester := caddytest.NewTester(t)\n\ttester.InitServer(`\n\t\t{\n\t\t\tskip_install_trust\n\t\t\tadmin localhost:2999\n\t\t\thttp_port `+testPort+`\n\n\t\t\tfrankenphp {\n\t\t\t\tnum_threads 3\n\t\t\t\tmax_threads 6\n\t\t\t\tworker ../testdata/worker-with-counter.php 1\n\t\t\t\tworker ../testdata/index.php 1\n\t\t\t}\n\t\t}\n\n\t\tlocalhost:`+testPort+` {\n\t\t\troute {\n\t\t\t\troot ../testdata\n\t\t\t\trewrite worker-with-counter.php\n\t\t\t\tphp\n\t\t\t}\n\t\t}\n\t\t`, \"caddyfile\")\n\n\tdebugState := getDebugState(t, tester)\n\n\t// assert that the correct threads are present in the thread info\n\tassert.Equal(t, debugState.ThreadDebugStates[0].State, \"ready\")\n\tassert.Contains(t, debugState.ThreadDebugStates[1].Name, \"worker-with-counter.php\")\n\tassert.Contains(t, debugState.ThreadDebugStates[2].Name, \"index.php\")\n\tassert.Equal(t, debugState.ReservedThreadCount, 3)\n\tassert.Len(t, debugState.ThreadDebugStates, 3)\n}\n\nfunc TestAutoScaleWorkerThreads(t *testing.T) {\n\twg := sync.WaitGroup{}\n\tmaxTries := 10\n\trequestsPerTry := 200\n\ttester := caddytest.NewTester(t)\n\ttester.InitServer(`\n\t\t{\n\t\t\tskip_install_trust\n\t\t\tadmin localhost:2999\n\t\t\thttp_port `+testPort+`\n\n\t\t\tfrankenphp {\n\t\t\t\tmax_threads 10\n\t\t\t\tnum_threads 2\n\t\t\t\tworker ../testdata/sleep.php {\n\t\t\t\t\tnum 1\n\t\t\t\t\tmax_threads 3\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tlocalhost:`+testPort+` {\n\t\t\troute {\n\t\t\t\troot ../testdata\n\t\t\t\trewrite sleep.php\n\t\t\t\tphp\n\t\t\t}\n\t\t}\n\t\t`, \"caddyfile\")\n\n\t// spam an endpoint that simulates IO\n\tendpoint := \"http://localhost:\" + testPort + \"/?sleep=2&work=1000\"\n\tamountOfThreads := getNumThreads(t, tester)\n\n\t// try to spawn the additional threads by spamming the server\n\tfor range maxTries {\n\t\twg.Add(requestsPerTry)\n\t\tfor range requestsPerTry {\n\t\t\tgo func() {\n\t\t\t\ttester.AssertGetResponse(endpoint, http.StatusOK, \"slept for 2 ms and worked for 1000 iterations\")\n\t\t\t\twg.Done()\n\t\t\t}()\n\t\t}\n\t\twg.Wait()\n\n\t\tamountOfThreads = getNumThreads(t, tester)\n\t\tif amountOfThreads > 2 {\n\t\t\tbreak\n\t\t}\n\t}\n\n\tassert.NotEqual(t, amountOfThreads, 2, \"at least one thread should have been auto-scaled\")\n\tassert.LessOrEqual(t, amountOfThreads, 4, \"at most 3 max_threads + 1 regular thread should be present\")\n}\n\n// Note this test requires at least 2x40MB available memory for the process\nfunc TestAutoScaleRegularThreadsOnAutomaticThreadLimit(t *testing.T) {\n\twg := sync.WaitGroup{}\n\tmaxTries := 10\n\trequestsPerTry := 200\n\ttester := caddytest.NewTester(t)\n\ttester.InitServer(`\n\t\t{\n\t\t\tskip_install_trust\n\t\t\tadmin localhost:2999\n\t\t\thttp_port `+testPort+`\n\n\t\t\tfrankenphp {\n\t\t\t\tmax_threads auto\n\t\t\t\tnum_threads 1\n\t\t\t\tphp_ini memory_limit 40M # a reasonable limit for the test\n\t\t\t}\n\t\t}\n\n\t\tlocalhost:`+testPort+` {\n\t\t\troute {\n\t\t\t\troot ../testdata\n\t\t\t\tphp\n\t\t\t}\n\t\t}\n\t\t`, \"caddyfile\")\n\n\t// spam an endpoint that simulates IO\n\tendpoint := \"http://localhost:\" + testPort + \"/sleep.php?sleep=2&work=1000\"\n\tamountOfThreads := getNumThreads(t, tester)\n\n\t// try to spawn the additional threads by spamming the server\n\tfor range maxTries {\n\t\twg.Add(requestsPerTry)\n\t\tfor range requestsPerTry {\n\t\t\tgo func() {\n\t\t\t\ttester.AssertGetResponse(endpoint, http.StatusOK, \"slept for 2 ms and worked for 1000 iterations\")\n\t\t\t\twg.Done()\n\t\t\t}()\n\t\t}\n\t\twg.Wait()\n\n\t\tamountOfThreads = getNumThreads(t, tester)\n\t\tif amountOfThreads > 1 {\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// assert that there are now more threads present\n\tassert.NotEqual(t, amountOfThreads, 1)\n}\n\nfunc assertAdminResponse(t *testing.T, tester *caddytest.Tester, method string, path string, expectedStatus int, expectedBody string) {\n\tadminUrl := \"http://localhost:2999/frankenphp/\"\n\tr, err := http.NewRequest(method, adminUrl+path, nil)\n\tassert.NoError(t, err)\n\tif expectedBody == \"\" {\n\t\t_ = tester.AssertResponseCode(r, expectedStatus)\n\t\treturn\n\t}\n\t_, _ = tester.AssertResponse(r, expectedStatus, expectedBody)\n}\n\nfunc getAdminResponseBody(t *testing.T, tester *caddytest.Tester, method string, path string) string {\n\tadminUrl := \"http://localhost:2999/frankenphp/\"\n\tr, err := http.NewRequest(method, adminUrl+path, nil)\n\tassert.NoError(t, err)\n\tresp := tester.AssertResponseCode(r, http.StatusOK)\n\tdefer resp.Body.Close()\n\tbytes, err := io.ReadAll(resp.Body)\n\tassert.NoError(t, err)\n\n\treturn string(bytes)\n}\n\nfunc getDebugState(t *testing.T, tester *caddytest.Tester) frankenphp.FrankenPHPDebugState {\n\tt.Helper()\n\tthreadStates := getAdminResponseBody(t, tester, \"GET\", \"threads\")\n\n\tvar debugStates frankenphp.FrankenPHPDebugState\n\terr := json.Unmarshal([]byte(threadStates), &debugStates)\n\tassert.NoError(t, err)\n\n\treturn debugStates\n}\n\nfunc getNumThreads(t *testing.T, tester *caddytest.Tester) int {\n\tt.Helper()\n\treturn len(getDebugState(t, tester).ThreadDebugStates)\n}\n\nfunc TestAddModuleWorkerViaAdminApi(t *testing.T) {\n\t// Initialize a server with admin API enabled\n\ttester := caddytest.NewTester(t)\n\ttester.InitServer(`\n\t\t{\n\t\t\tskip_install_trust\n\t\t\tadmin localhost:2999\n\t\t\thttp_port `+testPort+`\n\t\t}\n\n\t\tlocalhost:`+testPort+` {\n\t\t\troute {\n\t\t\t\troot ../testdata\n\t\t\t\tphp\n\t\t\t}\n\t\t}\n\t\t`, \"caddyfile\")\n\n\t// Get initial debug state to check number of workers\n\tinitialDebugState := getDebugState(t, tester)\n\tinitialWorkerCount := 0\n\tfor _, thread := range initialDebugState.ThreadDebugStates {\n\t\tif strings.HasPrefix(thread.Name, \"Worker PHP Thread\") {\n\t\t\tinitialWorkerCount++\n\t\t}\n\t}\n\n\t// Create a Caddyfile configuration with a module worker\n\tworkerConfig := `\n\t{\n\t\tskip_install_trust\n\t\tadmin localhost:2999\n\t\thttp_port ` + testPort + `\n\t}\n\n\tlocalhost:` + testPort + ` {\n\t\troute {\n\t\t\troot ../testdata\n\t\t\tphp {\n\t\t\t\tworker ../testdata/worker-with-counter.php 1\n\t\t\t}\n\t\t}\n\t}\n\t`\n\n\t// Send the configuration to the admin API\n\tadminUrl := \"http://localhost:2999/load\"\n\tr, err := http.NewRequest(\"POST\", adminUrl, bytes.NewBufferString(workerConfig))\n\tassert.NoError(t, err)\n\tr.Header.Set(\"Content-Type\", \"text/caddyfile\")\n\tresp := tester.AssertResponseCode(r, http.StatusOK)\n\tdefer resp.Body.Close()\n\n\t// Get the updated debug state to check if the worker was added\n\tupdatedDebugState := getDebugState(t, tester)\n\tupdatedWorkerCount := 0\n\tworkerFound := false\n\tfilename, _ := fastabs.FastAbs(\"../testdata/worker-with-counter.php\")\n\tfor _, thread := range updatedDebugState.ThreadDebugStates {\n\t\tif strings.HasPrefix(thread.Name, \"Worker PHP Thread\") {\n\t\t\tupdatedWorkerCount++\n\t\t\tif thread.Name == \"Worker PHP Thread - \"+filename {\n\t\t\t\tworkerFound = true\n\t\t\t}\n\t\t}\n\t}\n\n\t// Assert that the worker was added\n\tassert.Greater(t, updatedWorkerCount, initialWorkerCount, \"Worker count should have increased\")\n\tassert.True(t, workerFound, fmt.Sprintf(\"Worker with name %q should be found\", \"Worker PHP Thread - \"+filename))\n\n\t// Make a request to the worker to verify it's working\n\ttester.AssertGetResponse(\"http://localhost:\"+testPort+\"/worker-with-counter.php\", http.StatusOK, \"requests:1\")\n}\n"
  },
  {
    "path": "caddy/app.go",
    "content": "package caddy\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/caddyserver/caddy/v2\"\n\t\"github.com/caddyserver/caddy/v2/caddyconfig\"\n\t\"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile\"\n\t\"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile\"\n\t\"github.com/caddyserver/caddy/v2/modules/caddyhttp\"\n\t\"github.com/dunglas/frankenphp\"\n\t\"github.com/dunglas/frankenphp/internal/fastabs\"\n)\n\nvar (\n\toptions   []frankenphp.Option\n\toptionsMU sync.RWMutex\n)\n\n// EXPERIMENTAL: RegisterWorkers provides a way for extensions to register frankenphp.Workers\nfunc RegisterWorkers(name, fileName string, num int, wo ...frankenphp.WorkerOption) frankenphp.Workers {\n\tw, opt := frankenphp.WithExtensionWorkers(name, fileName, num, wo...)\n\n\toptionsMU.Lock()\n\toptions = append(options, opt)\n\toptionsMU.Unlock()\n\n\treturn w\n}\n\n// FrankenPHPApp represents the global \"frankenphp\" directive in the Caddyfile\n// it's responsible for starting up the global PHP instance and all threads\n//\n//\t{\n//\t\tfrankenphp {\n//\t\t\tnum_threads 20\n//\t\t}\n//\t}\ntype FrankenPHPApp struct {\n\t// NumThreads sets the number of PHP threads to start. Default: 2x the number of available CPUs.\n\tNumThreads int `json:\"num_threads,omitempty\"`\n\t// MaxThreads limits how many threads can be started at runtime. Default 2x NumThreads\n\tMaxThreads int `json:\"max_threads,omitempty\"`\n\t// Workers configures the worker scripts to start\n\tWorkers []workerConfig `json:\"workers,omitempty\"`\n\t// Overwrites the default php ini configuration\n\tPhpIni map[string]string `json:\"php_ini,omitempty\"`\n\t// The maximum amount of time a request may be stalled waiting for a thread\n\tMaxWaitTime time.Duration `json:\"max_wait_time,omitempty\"`\n\t// The maximum amount of time an autoscaled thread may be idle before being deactivated\n\tMaxIdleTime time.Duration `json:\"max_idle_time,omitempty\"`\n\n\topts    []frankenphp.Option\n\tmetrics frankenphp.Metrics\n\tctx     context.Context\n\tlogger  *slog.Logger\n}\n\nvar iniError = errors.New(`\"php_ini\" must be in the format: php_ini \"<key>\" \"<value>\"`)\n\n// CaddyModule returns the Caddy module information.\nfunc (f FrankenPHPApp) CaddyModule() caddy.ModuleInfo {\n\treturn caddy.ModuleInfo{\n\t\tID:  \"frankenphp\",\n\t\tNew: func() caddy.Module { return &f },\n\t}\n}\n\n// Provision sets up the module.\nfunc (f *FrankenPHPApp) Provision(ctx caddy.Context) error {\n\tf.ctx = ctx\n\tf.logger = ctx.Slogger()\n\n\t// We have at least 7 hardcoded options\n\tf.opts = make([]frankenphp.Option, 0, 7+len(options))\n\n\tif httpApp, err := ctx.AppIfConfigured(\"http\"); err == nil {\n\t\tif httpApp.(*caddyhttp.App).Metrics != nil {\n\t\t\tf.metrics = frankenphp.NewPrometheusMetrics(ctx.GetMetricsRegistry())\n\t\t}\n\t} else {\n\t\t// if the http module is not configured (this should never happen) then collect the metrics by default\n\t\tif errors.Is(err, caddy.ErrNotConfigured) {\n\t\t\tf.metrics = frankenphp.NewPrometheusMetrics(ctx.GetMetricsRegistry())\n\t\t} else {\n\t\t\t// the http module failed to provision due to invalid configuration\n\t\t\treturn fmt.Errorf(\"failed to provision caddy http: %w\", err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (f *FrankenPHPApp) generateUniqueModuleWorkerName(filepath string) string {\n\tvar i uint\n\tfilepath, _ = fastabs.FastAbs(filepath)\n\tname := \"m#\" + filepath\n\nretry:\n\tfor _, wc := range f.Workers {\n\t\tif wc.Name == name {\n\t\t\tname = fmt.Sprintf(\"m#%s_%d\", filepath, i)\n\t\t\ti++\n\n\t\t\tgoto retry\n\t\t}\n\t}\n\n\treturn name\n}\n\nfunc (f *FrankenPHPApp) addModuleWorkers(workers ...workerConfig) ([]workerConfig, error) {\n\tfor i := range workers {\n\t\tw := &workers[i]\n\n\t\tif frankenphp.EmbeddedAppPath != \"\" && filepath.IsLocal(w.FileName) {\n\t\t\tw.FileName = filepath.Join(frankenphp.EmbeddedAppPath, w.FileName)\n\t\t}\n\n\t\tif w.Name == \"\" {\n\t\t\tw.Name = f.generateUniqueModuleWorkerName(w.FileName)\n\t\t} else if !strings.HasPrefix(w.Name, \"m#\") {\n\t\t\tw.Name = \"m#\" + w.Name\n\t\t}\n\n\t\tf.Workers = append(f.Workers, *w)\n\t}\n\n\treturn workers, nil\n}\n\nfunc (f *FrankenPHPApp) Start() error {\n\trepl := caddy.NewReplacer()\n\n\toptionsMU.RLock()\n\tf.opts = append(f.opts, options...)\n\toptionsMU.RUnlock()\n\n\tf.opts = append(f.opts,\n\t\tfrankenphp.WithContext(f.ctx),\n\t\tfrankenphp.WithLogger(f.logger),\n\t\tfrankenphp.WithNumThreads(f.NumThreads),\n\t\tfrankenphp.WithMaxThreads(f.MaxThreads),\n\t\tfrankenphp.WithMetrics(f.metrics),\n\t\tfrankenphp.WithPhpIni(f.PhpIni),\n\t\tfrankenphp.WithMaxWaitTime(f.MaxWaitTime),\n\t\tfrankenphp.WithMaxIdleTime(f.MaxIdleTime),\n\t)\n\n\tfor _, w := range f.Workers {\n\t\tw.options = append(w.options,\n\t\t\tfrankenphp.WithWorkerEnv(w.Env),\n\t\t\tfrankenphp.WithWorkerWatchMode(w.Watch),\n\t\t\tfrankenphp.WithWorkerMaxFailures(w.MaxConsecutiveFailures),\n\t\t\tfrankenphp.WithWorkerMaxThreads(w.MaxThreads),\n\t\t\tfrankenphp.WithWorkerRequestOptions(w.requestOptions...),\n\t\t)\n\n\t\tf.opts = append(f.opts, frankenphp.WithWorkers(w.Name, repl.ReplaceKnown(w.FileName, \"\"), w.Num, w.options...))\n\t}\n\n\tfrankenphp.Shutdown()\n\tif err := frankenphp.Init(f.opts...); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (f *FrankenPHPApp) Stop() error {\n\tif f.logger.Enabled(f.ctx, slog.LevelInfo) {\n\t\tf.logger.LogAttrs(f.ctx, slog.LevelInfo, \"FrankenPHP stopped 🐘\")\n\t}\n\n\t// attempt a graceful shutdown if caddy is exiting\n\t// note: Exiting() is currently marked as 'experimental'\n\t// https://github.com/caddyserver/caddy/blob/e76405d55058b0a3e5ba222b44b5ef00516116aa/caddy.go#L810\n\tif caddy.Exiting() {\n\t\tfrankenphp.Shutdown()\n\t}\n\n\t// reset the configuration so it doesn't bleed into later tests\n\tf.Workers = nil\n\tf.NumThreads = 0\n\tf.MaxWaitTime = 0\n\tf.MaxIdleTime = 0\n\n\toptionsMU.Lock()\n\toptions = nil\n\toptionsMU.Unlock()\n\n\treturn nil\n}\n\n// UnmarshalCaddyfile implements caddyfile.Unmarshaler.\nfunc (f *FrankenPHPApp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {\n\tfor d.Next() {\n\t\tfor d.NextBlock(0) {\n\t\t\t// when adding a new directive, also update the allowedDirectives error message\n\t\t\tswitch d.Val() {\n\t\t\tcase \"num_threads\":\n\t\t\t\tif !d.NextArg() {\n\t\t\t\t\treturn d.ArgErr()\n\t\t\t\t}\n\n\t\t\t\tv, err := strconv.ParseUint(d.Val(), 10, 32)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tf.NumThreads = int(v)\n\t\t\tcase \"max_threads\":\n\t\t\t\tif !d.NextArg() {\n\t\t\t\t\treturn d.ArgErr()\n\t\t\t\t}\n\n\t\t\t\tif d.Val() == \"auto\" {\n\t\t\t\t\tf.MaxThreads = -1\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tv, err := strconv.ParseUint(d.Val(), 10, 32)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tf.MaxThreads = int(v)\n\t\t\tcase \"max_wait_time\":\n\t\t\t\tif !d.NextArg() {\n\t\t\t\t\treturn d.ArgErr()\n\t\t\t\t}\n\n\t\t\t\tv, err := time.ParseDuration(d.Val())\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn d.Err(\"max_wait_time must be a valid duration (example: 10s)\")\n\t\t\t\t}\n\n\t\t\t\tf.MaxWaitTime = v\n\t\t\tcase \"max_idle_time\":\n\t\t\t\tif !d.NextArg() {\n\t\t\t\t\treturn d.ArgErr()\n\t\t\t\t}\n\n\t\t\t\tv, err := time.ParseDuration(d.Val())\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn d.Err(\"max_idle_time must be a valid duration (example: 30s)\")\n\t\t\t\t}\n\n\t\t\t\tf.MaxIdleTime = v\n\t\t\tcase \"php_ini\":\n\t\t\t\tparseIniLine := func(d *caddyfile.Dispenser) error {\n\t\t\t\t\tkey := d.Val()\n\t\t\t\t\tif !d.NextArg() {\n\t\t\t\t\t\treturn d.WrapErr(iniError)\n\t\t\t\t\t}\n\t\t\t\t\tif f.PhpIni == nil {\n\t\t\t\t\t\tf.PhpIni = make(map[string]string)\n\t\t\t\t\t}\n\t\t\t\t\tf.PhpIni[key] = d.Val()\n\t\t\t\t\tif d.NextArg() {\n\t\t\t\t\t\treturn d.WrapErr(iniError)\n\t\t\t\t\t}\n\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tisBlock := false\n\t\t\t\tfor d.NextBlock(1) {\n\t\t\t\t\tisBlock = true\n\t\t\t\t\terr := parseIniLine(d)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif !isBlock {\n\t\t\t\t\tif !d.NextArg() {\n\t\t\t\t\t\treturn d.WrapErr(iniError)\n\t\t\t\t\t}\n\t\t\t\t\terr := parseIniLine(d)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\tcase \"worker\":\n\t\t\t\twc, err := unmarshalWorker(d)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tif frankenphp.EmbeddedAppPath != \"\" && filepath.IsLocal(wc.FileName) {\n\t\t\t\t\twc.FileName = filepath.Join(frankenphp.EmbeddedAppPath, wc.FileName)\n\t\t\t\t}\n\t\t\t\tif strings.HasPrefix(wc.Name, \"m#\") {\n\t\t\t\t\treturn d.Errf(`global worker names must not start with \"m#\": %q`, wc.Name)\n\t\t\t\t}\n\t\t\t\t// check for duplicate workers\n\t\t\t\tfor _, existingWorker := range f.Workers {\n\t\t\t\t\tif existingWorker.FileName == wc.FileName {\n\t\t\t\t\t\treturn d.Errf(\"global workers must not have duplicate filenames: %q\", wc.FileName)\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tf.Workers = append(f.Workers, wc)\n\t\t\tdefault:\n\t\t\t\treturn wrongSubDirectiveError(\"frankenphp\", \"num_threads, max_threads, php_ini, worker, max_wait_time, max_idle_time\", d.Val())\n\t\t\t}\n\t\t}\n\t}\n\n\tif f.MaxThreads > 0 && f.NumThreads > 0 && f.MaxThreads < f.NumThreads {\n\t\treturn d.Err(`\"max_threads\"\" must be greater than or equal to \"num_threads\"`)\n\t}\n\n\treturn nil\n}\n\nfunc parseGlobalOption(d *caddyfile.Dispenser, _ any) (any, error) {\n\tapp := &FrankenPHPApp{}\n\tif err := app.UnmarshalCaddyfile(d); err != nil {\n\t\treturn nil, err\n\t}\n\n\t// tell Caddyfile adapter that this is the JSON for an app\n\treturn httpcaddyfile.App{\n\t\tName:  \"frankenphp\",\n\t\tValue: caddyconfig.JSON(app, nil),\n\t}, nil\n}\n\nvar (\n\t_ caddy.App         = (*FrankenPHPApp)(nil)\n\t_ caddy.Provisioner = (*FrankenPHPApp)(nil)\n)\n"
  },
  {
    "path": "caddy/br-skip.go",
    "content": "//go:build nobrotli\n\npackage caddy\n\nvar brotli = false\n"
  },
  {
    "path": "caddy/br.go",
    "content": "//go:build !nobrotli\n\npackage caddy\n\nvar brotli = true\n"
  },
  {
    "path": "caddy/caddy.go",
    "content": "// Package caddy provides a PHP module for the Caddy web server.\n// FrankenPHP embeds the PHP interpreter directly in Caddy, giving it the ability to run your PHP scripts directly.\n// No PHP FPM required!\npackage caddy\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/caddyserver/caddy/v2\"\n\t\"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile\"\n)\n\nconst (\n\tdefaultDocumentRoot = \"public\"\n\tdefaultWatchPattern = \"./**/*.{env,php,twig,yaml,yml}\"\n)\n\nfunc init() {\n\tcaddy.RegisterModule(FrankenPHPApp{})\n\tcaddy.RegisterModule(FrankenPHPModule{})\n\tcaddy.RegisterModule(FrankenPHPAdmin{})\n\n\thttpcaddyfile.RegisterGlobalOption(\"frankenphp\", parseGlobalOption)\n\n\thttpcaddyfile.RegisterHandlerDirective(\"php\", parseCaddyfile)\n\thttpcaddyfile.RegisterDirectiveOrder(\"php\", \"before\", \"file_server\")\n\n\thttpcaddyfile.RegisterDirective(\"php_server\", parsePhpServer)\n\thttpcaddyfile.RegisterDirectiveOrder(\"php_server\", \"before\", \"file_server\")\n}\n\n// wrongSubDirectiveError returns a nice error message.\nfunc wrongSubDirectiveError(module string, allowedDirectives string, wrongValue string) error {\n\treturn fmt.Errorf(\"unknown %q subdirective: %s (allowed directives are: %s)\", module, wrongValue, allowedDirectives)\n}\n"
  },
  {
    "path": "caddy/caddy_test.go",
    "content": "package caddy_test\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/caddyserver/caddy/v2\"\n\t\"github.com/caddyserver/caddy/v2/caddytest\"\n\t\"github.com/dunglas/frankenphp/internal/fastabs\"\n\t\"github.com/prometheus/client_golang/prometheus/testutil\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// initServer initializes a Caddy test server and waits for it to be ready.\n// After InitServer, it polls the server to handle a race condition on macOS where\n// SO_REUSEPORT can briefly route connections to the old listener being shut down,\n// resulting in \"connection reset by peer\".\nfunc initServer(t *testing.T, tester *caddytest.Tester, config string, format string) {\n\tt.Helper()\n\ttester.InitServer(config, format)\n\n\tclient := &http.Client{Timeout: 1 * time.Second}\n\trequire.Eventually(t, func() bool {\n\t\tresp, err := client.Get(\"http://localhost:\" + testPort)\n\t\tif err != nil {\n\t\t\treturn false\n\t\t}\n\n\t\trequire.NoError(t, resp.Body.Close())\n\n\t\treturn true\n\t}, 5*time.Second, 100*time.Millisecond, \"server failed to become ready\")\n}\n\nvar testPort = \"9080\"\n\n// skipIfSymlinkNotValid skips the test if the given path is not a valid symlink\nfunc skipIfSymlinkNotValid(t *testing.T, path string) {\n\tt.Helper()\n\n\tinfo, err := os.Lstat(path)\n\tif err != nil {\n\t\tt.Skipf(\"symlink test skipped: cannot stat %s: %v\", path, err)\n\t}\n\n\tif info.Mode()&os.ModeSymlink == 0 {\n\t\tt.Skipf(\"symlink test skipped: %s is not a symlink (git may not support symlinks on this platform)\", path)\n\t}\n}\n\n// escapeMetricLabel escapes backslashes in label values for Prometheus text format\nfunc escapeMetricLabel(s string) string {\n\treturn strings.ReplaceAll(s, \"\\\\\", \"\\\\\\\\\")\n}\n\nfunc TestMain(m *testing.M) {\n\t// setup custom environment vars for TestOsEnv\n\tif os.Setenv(\"ENV1\", \"value1\") != nil || os.Setenv(\"ENV2\", \"value2\") != nil {\n\t\tfmt.Println(\"Failed to set environment variables for tests\")\n\t\tos.Exit(1)\n\t}\n\n\tos.Exit(m.Run())\n}\n\nfunc TestPHP(t *testing.T) {\n\tvar wg sync.WaitGroup\n\ttester := caddytest.NewTester(t)\n\tinitServer(t, tester, `\n\t\t{\n\t\t\tskip_install_trust\n\t\t\tadmin localhost:2999\n\t\t\thttp_port `+testPort+`\n\t\t\thttps_port 9443\n\t\t}\n\n\t\tlocalhost:`+testPort+` {\n\t\t\troute {\n\t\t\t\tphp {\n\t\t\t\t\troot ../testdata\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t`, \"caddyfile\")\n\n\tfor i := range 100 {\n\t\twg.Add(1)\n\n\t\tgo func(i int) {\n\t\t\ttester.AssertGetResponse(fmt.Sprintf(\"http://localhost:\"+testPort+\"/index.php?i=%d\", i), http.StatusOK, fmt.Sprintf(\"I am by birth a Genevese (%d)\", i))\n\t\t\twg.Done()\n\t\t}(i)\n\t}\n\twg.Wait()\n}\n\nfunc TestLargeRequest(t *testing.T) {\n\ttester := caddytest.NewTester(t)\n\tinitServer(t, tester, `\n\t\t{\n\t\t\tskip_install_trust\n\t\t\tadmin localhost:2999\n\t\t\thttp_port `+testPort+`\n\t\t\thttps_port 9443\n\t\t}\n\n\t\tlocalhost:`+testPort+` {\n\t\t\troute {\n\t\t\t\tphp {\n\t\t\t\t\troot ../testdata\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t`, \"caddyfile\")\n\n\ttester.AssertPostResponseBody(\n\t\t\"http://localhost:\"+testPort+\"/large-request.php\",\n\t\t[]string{},\n\t\tbytes.NewBufferString(strings.Repeat(\"f\", 1_048_576)),\n\t\thttp.StatusOK,\n\t\t\"Request body size: 1048576 (unknown)\",\n\t)\n}\n\nfunc TestWorker(t *testing.T) {\n\tvar wg sync.WaitGroup\n\ttester := caddytest.NewTester(t)\n\tinitServer(t, tester, `\n\t\t{\n\t\t\tskip_install_trust\n\t\t\tadmin localhost:2999\n\t\t\thttp_port `+testPort+`\n\t\t\thttps_port 9443\n\n\t\t\tfrankenphp {\n\t\t\t\tworker ../testdata/index.php 2\n\t\t\t}\n\t\t}\n\n\t\tlocalhost:`+testPort+` {\n\t\t\troute {\n\t\t\t\tphp {\n\t\t\t\t\troot ../testdata\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t`, \"caddyfile\")\n\n\tfor i := range 100 {\n\t\twg.Add(1)\n\n\t\tgo func(i int) {\n\t\t\ttester.AssertGetResponse(fmt.Sprintf(\"http://localhost:\"+testPort+\"/index.php?i=%d\", i), http.StatusOK, fmt.Sprintf(\"I am by birth a Genevese (%d)\", i))\n\t\t\twg.Done()\n\t\t}(i)\n\t}\n\twg.Wait()\n}\n\nfunc TestGlobalAndModuleWorker(t *testing.T) {\n\tvar wg sync.WaitGroup\n\ttestPortNum, _ := strconv.Atoi(testPort)\n\ttestPortTwo := strconv.Itoa(testPortNum + 1)\n\ttester := caddytest.NewTester(t)\n\tinitServer(t, tester, `\n\t\t{\n\t\t\tskip_install_trust\n\t\t\tadmin localhost:2999\n\n\t\t\tfrankenphp {\n\t\t\t\tworker {\n\t\t\t\t\tfile ../testdata/worker-with-env.php\n\t\t\t\t\tnum 1\n\t\t\t\t\tenv APP_ENV global\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\thttp://localhost:`+testPort+` {\n\t\t\troute {\n\t\t\t\tphp {\n\t\t\t\t\troot ../testdata\n\t\t\t\t\tworker {\n\t\t\t\t\t\tfile worker-with-env.php\n\t\t\t\t\t\tnum 2\n\t\t\t\t\t\tenv APP_ENV module\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\thttp://localhost:`+testPortTwo+` {\n\t\t\troute {\n\t\t\t\tphp {\n\t\t\t\t\troot ../testdata\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t`, \"caddyfile\")\n\n\tfor i := range 10 {\n\t\twg.Add(1)\n\n\t\tgo func(i int) {\n\t\t\ttester.AssertGetResponse(\"http://localhost:\"+testPort+\"/worker-with-env.php\", http.StatusOK, \"Worker has APP_ENV=module\")\n\t\t\ttester.AssertGetResponse(\"http://localhost:\"+testPortTwo+\"/worker-with-env.php\", http.StatusOK, \"Worker has APP_ENV=global\")\n\t\t\twg.Done()\n\t\t}(i)\n\t}\n\twg.Wait()\n}\n\nfunc TestModuleWorkerInheritsEnv(t *testing.T) {\n\ttester := caddytest.NewTester(t)\n\tinitServer(t, tester, `\n\t\t{\n\t\t\tskip_install_trust\n\t\t\tadmin localhost:2999\n\t\t}\n\n\t\thttp://localhost:`+testPort+` {\n\t\t\troute {\n\t\t\t\tphp {\n\t\t\t\t\troot ../testdata\n\t\t\t\t\tenv APP_ENV inherit_this\n\t\t\t\t\tworker worker-with-env.php\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t`, \"caddyfile\")\n\n\ttester.AssertGetResponse(\"http://localhost:\"+testPort+\"/worker-with-env.php\", http.StatusOK, \"Worker has APP_ENV=inherit_this\")\n}\n\nfunc TestNamedModuleWorkers(t *testing.T) {\n\tvar wg sync.WaitGroup\n\ttestPortNum, _ := strconv.Atoi(testPort)\n\ttestPortTwo := strconv.Itoa(testPortNum + 1)\n\ttester := caddytest.NewTester(t)\n\tinitServer(t, tester, `\n\t\t{\n\t\t\tskip_install_trust\n\t\t\tadmin localhost:2999\n\t\t}\n\n\t\thttp://localhost:`+testPort+` {\n\t\t\troute {\n\t\t\t\tphp {\n\t\t\t\t\troot ../testdata\n\t\t\t\t\tworker {\n\t\t\t\t\t\tfile worker-with-env.php\n\t\t\t\t\t\tnum 2\n\t\t\t\t\t\tenv APP_ENV one\n\t\t\t\t\t\tname module1\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\thttp://localhost:`+testPortTwo+` {\n\t\t\troute {\n\t\t\t\tphp {\n\t\t\t\t\troot ../testdata\n\t\t\t\t\tworker {\n\t\t\t\t\t\tfile worker-with-env.php\n\t\t\t\t\t\tnum 1\n\t\t\t\t\t\tenv APP_ENV two\n\t\t\t\t\t\tname module2\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t`, \"caddyfile\")\n\n\tfor i := range 10 {\n\t\twg.Add(1)\n\n\t\tgo func(i int) {\n\t\t\ttester.AssertGetResponse(\"http://localhost:\"+testPort+\"/worker-with-env.php\", http.StatusOK, \"Worker has APP_ENV=one\")\n\t\t\ttester.AssertGetResponse(\"http://localhost:\"+testPortTwo+\"/worker-with-env.php\", http.StatusOK, \"Worker has APP_ENV=two\")\n\t\t\twg.Done()\n\t\t}(i)\n\t}\n\twg.Wait()\n}\n\nfunc TestEnv(t *testing.T) {\n\ttester := caddytest.NewTester(t)\n\tinitServer(t, tester, `\n\t\t{\n\t\t\tskip_install_trust\n\t\t\tadmin localhost:2999\n\t\t\thttp_port `+testPort+`\n\t\t\thttps_port 9443\n\n\t\t\tfrankenphp {\n\t\t\t\tworker {\n\t\t\t\t\tfile ../testdata/worker-env.php\n\t\t\t\t\tnum 1\n\t\t\t\t\tenv FOO bar\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tlocalhost:`+testPort+` {\n\t\t\troute {\n\t\t\t\tphp {\n\t\t\t\t\troot ../testdata\n\t\t\t\t\tenv FOO baz\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t`, \"caddyfile\")\n\n\ttester.AssertGetResponse(\"http://localhost:\"+testPort+\"/worker-env.php\", http.StatusOK, \"bazbar\")\n}\n\nfunc TestJsonEnv(t *testing.T) {\n\ttester := caddytest.NewTester(t)\n\tinitServer(t, tester, `\n\t\t{\n\t\t\"admin\": {\n\t\t\t\"listen\": \"localhost:2999\"\n\t\t},\n\t\t\"apps\": {\n\t\t\t\"frankenphp\": {\n\t\t\t\"workers\": [\n\t\t\t\t{\n\t\t\t\t\"env\": {\n\t\t\t\t\t\"FOO\": \"bar\"\n\t\t\t\t},\n\t\t\t\t\"file_name\": \"../testdata/worker-env.php\",\n\t\t\t\t\"num\": 1\n\t\t\t\t}\n\t\t\t]\n\t\t\t},\n\t\t\t\"http\": {\n\t\t\t\"http_port\": `+testPort+`,\n\t\t\t\"https_port\": 9443,\n\t\t\t\"servers\": {\n\t\t\t\t\"srv0\": {\n\t\t\t\t\"listen\": [\n\t\t\t\t\t\":`+testPort+`\"\n\t\t\t\t],\n\t\t\t\t\"routes\": [\n\t\t\t\t\t{\n\t\t\t\t\t\"handle\": [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\"handler\": \"subroute\",\n\t\t\t\t\t\t\"routes\": [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"handle\": [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"handler\": \"subroute\",\n\t\t\t\t\t\t\t\t\"routes\": [\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"handle\": [\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\"env\": {\n\t\t\t\t\t\t\t\t\t\t\t\"FOO\": \"baz\"\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\"handler\": \"php\",\n\t\t\t\t\t\t\t\t\t\t\"root\": \"../testdata\"\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t]\n\t\t\t\t\t\t}\n\t\t\t\t\t],\n\t\t\t\t\t\"match\": [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\"localhost\"\n\t\t\t\t\t\t]\n\t\t\t\t\t\t}\n\t\t\t\t\t],\n\t\t\t\t\t\"terminal\": true\n\t\t\t\t\t}\n\t\t\t\t]\n\t\t\t\t}\n\t\t\t}\n\t\t\t},\n\t\t\t\"pki\": {\n\t\t\t\"certificate_authorities\": {\n\t\t\t\t\"local\": {\n\t\t\t\t\"install_trust\": false\n\t\t\t\t}\n\t\t\t}\n\t\t\t}\n\t\t}\n\t\t}\n\t\t`, \"json\")\n\n\ttester.AssertGetResponse(\"http://localhost:\"+testPort+\"/worker-env.php\", http.StatusOK, \"bazbar\")\n}\n\nfunc TestCustomCaddyVariablesInEnv(t *testing.T) {\n\ttester := caddytest.NewTester(t)\n\tinitServer(t, tester, `\n\t\t{\n\t\t\tskip_install_trust\n\t\t\tadmin localhost:2999\n\t\t\thttp_port `+testPort+`\n\t\t\thttps_port 9443\n\n\t\t\tfrankenphp {\n\t\t\t\tworker {\n\t\t\t\t\tfile ../testdata/worker-env.php\n\t\t\t\t\tnum 1\n\t\t\t\t\tenv FOO world\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tlocalhost:`+testPort+` {\n\t\t\troute {\n\t\t\t\tmap 1 {my_customvar} {\n\t\t\t\t\tdefault \"hello \"\n\t\t\t\t}\n\t\t\t\tphp {\n\t\t\t\t\troot ../testdata\n\t\t\t\t\tenv FOO {my_customvar}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t`, \"caddyfile\")\n\n\ttester.AssertGetResponse(\"http://localhost:\"+testPort+\"/worker-env.php\", http.StatusOK, \"hello world\")\n}\n\nfunc TestPHPServerDirective(t *testing.T) {\n\ttester := caddytest.NewTester(t)\n\tinitServer(t, tester, `\n\t\t{\n\t\t\tskip_install_trust\n\t\t\tadmin localhost:2999\n\t\t\thttp_port `+testPort+`\n\t\t\thttps_port 9443\n\t\t}\n\n\t\tlocalhost:`+testPort+` {\n\t\t\troot ../testdata\n\t\t\tphp_server\n\t\t}\n\t\t`, \"caddyfile\")\n\n\ttester.AssertGetResponse(\"http://localhost:\"+testPort, http.StatusOK, \"I am by birth a Genevese (i not set)\")\n\ttester.AssertGetResponse(\"http://localhost:\"+testPort+\"/hello.txt\", http.StatusOK, \"Hello\\n\")\n\ttester.AssertGetResponse(\"http://localhost:\"+testPort+\"/not-found.txt\", http.StatusOK, \"I am by birth a Genevese (i not set)\")\n}\n\nfunc TestPHPServerDirectiveDisableFileServer(t *testing.T) {\n\ttester := caddytest.NewTester(t)\n\tinitServer(t, tester, `\n\t\t{\n\t\t\tskip_install_trust\n\t\t\tadmin localhost:2999\n\t\t\thttp_port `+testPort+`\n\t\t\thttps_port 9443\n\t\t\torder php_server before respond\n\t\t}\n\n\t\tlocalhost:`+testPort+` {\n\t\t\troot ../testdata\n\t\t\tphp_server {\n\t\t\t\tfile_server off\n\t\t\t}\n\t\t\trespond \"Not found\" 404\n\t\t}\n\t\t`, \"caddyfile\")\n\n\ttester.AssertGetResponse(\"http://localhost:\"+testPort, http.StatusOK, \"I am by birth a Genevese (i not set)\")\n\ttester.AssertGetResponse(\"http://localhost:\"+testPort+\"/not-found.txt\", http.StatusOK, \"I am by birth a Genevese (i not set)\")\n}\n\nfunc TestMetrics(t *testing.T) {\n\tvar wg sync.WaitGroup\n\ttester := caddytest.NewTester(t)\n\tinitServer(t, tester, `\n\t{\n\t\tskip_install_trust\n\t\tadmin localhost:2999\n\t\thttp_port `+testPort+`\n\t\thttps_port 9443\n\t\tmetrics\n\t}\n\n\tlocalhost:`+testPort+` {\n\t\troute {\n\t\t\tmercure {\n\t\t\t\ttransport local\n\t\t\t\tanonymous\n\t\t\t\tpublisher_jwt !ChangeMe!\n\t\t\t}\n\n\t\t\tphp {\n\t\t\t\troot ../testdata\n\t\t\t}\n\t\t}\n\t}\n\n\texample.com:`+testPort+` {\n\t\troute {\n\t\t\tmercure {\n\t\t\t\ttransport local\n\t\t\t\tanonymous\n\t\t\t\tpublisher_jwt !ChangeMe!\n\t\t\t}\n\n\t\t\tphp {\n\t\t\t\troot ../testdata\n\t\t\t}\n\t\t}\n\t}\n\t`, \"caddyfile\")\n\n\t// Make some requests\n\tfor i := range 10 {\n\t\twg.Add(1)\n\t\tgo func(i int) {\n\t\t\ttester.AssertGetResponse(fmt.Sprintf(\"http://localhost:\"+testPort+\"/index.php?i=%d\", i), http.StatusOK, fmt.Sprintf(\"I am by birth a Genevese (%d)\", i))\n\t\t\twg.Done()\n\t\t}(i)\n\t}\n\twg.Wait()\n\n\t// Fetch metrics\n\tresp, err := http.Get(\"http://localhost:2999/metrics\")\n\trequire.NoError(t, err, \"failed to fetch metrics\")\n\tt.Cleanup(func() {\n\t\trequire.NoError(t, resp.Body.Close())\n\t})\n\n\t// Read and parse metrics\n\tmetrics := new(bytes.Buffer)\n\t_, err = metrics.ReadFrom(resp.Body)\n\trequire.NoError(t, err, \"failed to read metrics\")\n\n\tcpus := strconv.Itoa(getNumThreads(t, tester))\n\n\t// Check metrics\n\texpectedMetrics := `\n\t# HELP frankenphp_total_threads Total number of PHP threads\n\t# TYPE frankenphp_total_threads counter\n\tfrankenphp_total_threads ` + cpus + `\n\n\t# HELP frankenphp_busy_threads Number of busy PHP threads\n\t# TYPE frankenphp_busy_threads gauge\n\tfrankenphp_busy_threads 0\n\t`\n\n\tctx := caddy.ActiveContext()\n\n\trequire.NoError(t, testutil.GatherAndCompare(ctx.GetMetricsRegistry(), strings.NewReader(expectedMetrics), \"frankenphp_total_threads\", \"frankenphp_busy_threads\"))\n}\n\nfunc TestWorkerMetrics(t *testing.T) {\n\tvar wg sync.WaitGroup\n\ttester := caddytest.NewTester(t)\n\tinitServer(t, tester, `\n\t{\n\t\tskip_install_trust\n\t\tadmin localhost:2999\n\t\thttp_port `+testPort+`\n\t\thttps_port 9443\n\t\tmetrics\n\n\t\tfrankenphp {\n\t\t\tworker ../testdata/index.php 2\n\t\t}\n\t}\n\n\tlocalhost:`+testPort+` {\n\t\troute {\n\t\t\tphp {\n\t\t\t\troot ../testdata\n\t\t\t}\n\t\t}\n\t}\n\n\texample.com:`+testPort+` {\n\t\troute {\n\t\t\tphp {\n\t\t\t\troot ../testdata\n\t\t\t}\n\t\t}\n\t}\n\t`, \"caddyfile\")\n\n\tworkerName, _ := fastabs.FastAbs(\"../testdata/index.php\")\n\tworkerName = escapeMetricLabel(workerName)\n\n\t// Make some requests\n\tfor i := range 10 {\n\t\twg.Add(1)\n\t\tgo func(i int) {\n\t\t\ttester.AssertGetResponse(fmt.Sprintf(\"http://localhost:\"+testPort+\"/index.php?i=%d\", i), http.StatusOK, fmt.Sprintf(\"I am by birth a Genevese (%d)\", i))\n\t\t\twg.Done()\n\t\t}(i)\n\t}\n\twg.Wait()\n\n\t// Fetch metrics\n\tresp, err := http.Get(\"http://localhost:2999/metrics\")\n\trequire.NoError(t, err, \"failed to fetch metrics\")\n\tt.Cleanup(func() {\n\t\trequire.NoError(t, resp.Body.Close())\n\t})\n\n\t// Read and parse metrics\n\tmetrics := new(bytes.Buffer)\n\t_, err = metrics.ReadFrom(resp.Body)\n\trequire.NoError(t, err, \"failed to read metrics\")\n\n\tcpus := strconv.Itoa(getNumThreads(t, tester))\n\n\t// Check metrics\n\texpectedMetrics := `\n\t# HELP frankenphp_total_threads Total number of PHP threads\n\t# TYPE frankenphp_total_threads counter\n\tfrankenphp_total_threads ` + cpus + `\n\n\t# HELP frankenphp_busy_threads Number of busy PHP threads\n\t# TYPE frankenphp_busy_threads gauge\n\tfrankenphp_busy_threads 2\n\n\t# HELP frankenphp_busy_workers Number of busy PHP workers for this worker\n\t# TYPE frankenphp_busy_workers gauge\n\tfrankenphp_busy_workers{worker=\"` + workerName + `\"} 0\n\n\t# HELP frankenphp_total_workers Total number of PHP workers for this worker\n\t# TYPE frankenphp_total_workers gauge\n\tfrankenphp_total_workers{worker=\"` + workerName + `\"} 2\n\n\t# HELP frankenphp_worker_request_count\n\t# TYPE frankenphp_worker_request_count counter\n\tfrankenphp_worker_request_count{worker=\"` + workerName + `\"} 10\n\n\t# HELP frankenphp_ready_workers Running workers that have successfully called frankenphp_handle_request at least once\n\t# TYPE frankenphp_ready_workers gauge\n\tfrankenphp_ready_workers{worker=\"` + workerName + `\"} 2\n\t`\n\n\tctx := caddy.ActiveContext()\n\trequire.NoError(t,\n\t\ttestutil.GatherAndCompare(\n\t\t\tctx.GetMetricsRegistry(),\n\t\t\tstrings.NewReader(expectedMetrics),\n\t\t\t\"frankenphp_total_threads\",\n\t\t\t\"frankenphp_busy_threads\",\n\t\t\t\"frankenphp_busy_workers\",\n\t\t\t\"frankenphp_total_workers\",\n\t\t\t\"frankenphp_worker_request_count\",\n\t\t\t\"frankenphp_ready_workers\",\n\t\t))\n}\n\nfunc TestNamedWorkerMetrics(t *testing.T) {\n\tvar wg sync.WaitGroup\n\ttester := caddytest.NewTester(t)\n\tinitServer(t, tester, `\n\t{\n\t\tskip_install_trust\n\t\tadmin localhost:2999\n\t\thttp_port `+testPort+`\n\t\thttps_port 9443\n\t\tmetrics\n\n\t\tfrankenphp {\n\t\t\tworker {\n\t\t\t\tname my_app\n\t\t\t\tfile ../testdata/index.php\n\t\t\t\tnum 2\n\t\t\t}\n\t\t}\n\t}\n\n\tlocalhost:`+testPort+` {\n\t\troute {\n\t\t\tphp {\n\t\t\t\troot ../testdata\n\t\t\t}\n\t\t}\n\t}\n\t`, \"caddyfile\")\n\n\t// Make some requests\n\tfor i := range 10 {\n\t\twg.Add(1)\n\t\tgo func(i int) {\n\t\t\ttester.AssertGetResponse(fmt.Sprintf(\"http://localhost:\"+testPort+\"/index.php?i=%d\", i), http.StatusOK, fmt.Sprintf(\"I am by birth a Genevese (%d)\", i))\n\t\t\twg.Done()\n\t\t}(i)\n\t}\n\twg.Wait()\n\n\t// Fetch metrics\n\tresp, err := http.Get(\"http://localhost:2999/metrics\")\n\trequire.NoError(t, err, \"failed to fetch metrics\")\n\tt.Cleanup(func() {\n\t\trequire.NoError(t, resp.Body.Close())\n\t})\n\n\t// Read and parse metrics\n\tmetrics := new(bytes.Buffer)\n\t_, err = metrics.ReadFrom(resp.Body)\n\trequire.NoError(t, err, \"failed to read metrics\")\n\n\tcpus := strconv.Itoa(getNumThreads(t, tester))\n\n\t// Check metrics\n\texpectedMetrics := `\n\t# HELP frankenphp_total_threads Total number of PHP threads\n\t# TYPE frankenphp_total_threads counter\n\tfrankenphp_total_threads ` + cpus + `\n\n\t# HELP frankenphp_busy_threads Number of busy PHP threads\n\t# TYPE frankenphp_busy_threads gauge\n\tfrankenphp_busy_threads 2\n\n\t# HELP frankenphp_busy_workers Number of busy PHP workers for this worker\n        # TYPE frankenphp_busy_workers gauge\n        frankenphp_busy_workers{worker=\"my_app\"} 0\n\n\t# HELP frankenphp_total_workers Total number of PHP workers for this worker\n\t# TYPE frankenphp_total_workers gauge\n\tfrankenphp_total_workers{worker=\"my_app\"} 2\n\n\t# HELP frankenphp_worker_request_count\n\t# TYPE frankenphp_worker_request_count counter\n\tfrankenphp_worker_request_count{worker=\"my_app\"} 10\n\n\t# HELP frankenphp_ready_workers Running workers that have successfully called frankenphp_handle_request at least once\n\t# TYPE frankenphp_ready_workers gauge\n\tfrankenphp_ready_workers{worker=\"my_app\"} 2\n\t`\n\n\tctx := caddy.ActiveContext()\n\trequire.NoError(t,\n\t\ttestutil.GatherAndCompare(\n\t\t\tctx.GetMetricsRegistry(),\n\t\t\tstrings.NewReader(expectedMetrics),\n\t\t\t\"frankenphp_total_threads\",\n\t\t\t\"frankenphp_busy_threads\",\n\t\t\t\"frankenphp_busy_workers\",\n\t\t\t\"frankenphp_total_workers\",\n\t\t\t\"frankenphp_worker_request_count\",\n\t\t\t\"frankenphp_ready_workers\",\n\t\t),\n\t)\n}\n\nfunc TestAutoWorkerConfig(t *testing.T) {\n\tvar wg sync.WaitGroup\n\ttester := caddytest.NewTester(t)\n\tinitServer(t, tester, `\n\t{\n\t\tskip_install_trust\n\t\tadmin localhost:2999\n\t\thttp_port `+testPort+`\n\t\thttps_port 9443\n\t\tmetrics\n\n\t\tfrankenphp {\n\t\t\tworker ../testdata/index.php\n\t\t}\n\t}\n\n\tlocalhost:`+testPort+` {\n\t\troute {\n\t\t\tphp {\n\t\t\t\troot ../testdata\n\t\t\t}\n\t\t}\n\t}\n\t`, \"caddyfile\")\n\n\tworkerName, _ := fastabs.FastAbs(\"../testdata/index.php\")\n\tworkerName = escapeMetricLabel(workerName)\n\n\t// Make some requests\n\tfor i := range 10 {\n\t\twg.Add(1)\n\t\tgo func(i int) {\n\t\t\ttester.AssertGetResponse(fmt.Sprintf(\"http://localhost:\"+testPort+\"/index.php?i=%d\", i), http.StatusOK, fmt.Sprintf(\"I am by birth a Genevese (%d)\", i))\n\t\t\twg.Done()\n\t\t}(i)\n\t}\n\twg.Wait()\n\n\t// Fetch metrics\n\tresp, err := http.Get(\"http://localhost:2999/metrics\")\n\trequire.NoError(t, err, \"failed to fetch metrics\")\n\tt.Cleanup(func() {\n\t\trequire.NoError(t, resp.Body.Close())\n\t})\n\n\t// Read and parse metrics\n\tmetrics := new(bytes.Buffer)\n\t_, err = metrics.ReadFrom(resp.Body)\n\trequire.NoError(t, err, \"failed to read metrics\")\n\n\tnumThreads := getNumThreads(t, tester)\n\tcpus := strconv.Itoa(numThreads)\n\tworkers := strconv.Itoa(numThreads - 1)\n\n\t// Check metrics\n\texpectedMetrics := `\n\t# HELP frankenphp_total_threads Total number of PHP threads\n\t# TYPE frankenphp_total_threads counter\n\tfrankenphp_total_threads ` + cpus + `\n\n\t# HELP frankenphp_busy_threads Number of busy PHP threads\n\t# TYPE frankenphp_busy_threads gauge\n\tfrankenphp_busy_threads ` + workers + `\n\n\t# HELP frankenphp_busy_workers Number of busy PHP workers for this worker\n\t# TYPE frankenphp_busy_workers gauge\n\tfrankenphp_busy_workers{worker=\"` + workerName + `\"} 0\n\n\t# HELP frankenphp_total_workers Total number of PHP workers for this worker\n\t# TYPE frankenphp_total_workers gauge\n\tfrankenphp_total_workers{worker=\"` + workerName + `\"} ` + workers + `\n\n\t# HELP frankenphp_worker_request_count\n\t# TYPE frankenphp_worker_request_count counter\n\tfrankenphp_worker_request_count{worker=\"` + workerName + `\"} 10\n\n\t# HELP frankenphp_ready_workers Running workers that have successfully called frankenphp_handle_request at least once\n\t# TYPE frankenphp_ready_workers gauge\n\tfrankenphp_ready_workers{worker=\"` + workerName + `\"} ` + workers + `\n\t`\n\n\tctx := caddy.ActiveContext()\n\trequire.NoError(t,\n\t\ttestutil.GatherAndCompare(\n\t\t\tctx.GetMetricsRegistry(),\n\t\t\tstrings.NewReader(expectedMetrics),\n\t\t\t\"frankenphp_total_threads\",\n\t\t\t\"frankenphp_busy_threads\",\n\t\t\t\"frankenphp_busy_workers\",\n\t\t\t\"frankenphp_total_workers\",\n\t\t\t\"frankenphp_worker_request_count\",\n\t\t\t\"frankenphp_ready_workers\",\n\t\t))\n}\n\nfunc TestAllDefinedServerVars(t *testing.T) {\n\tdocumentRoot, _ := filepath.Abs(\"../testdata/\")\n\texpectedBodyFile, _ := os.ReadFile(\"../testdata/server-all-vars-ordered.txt\")\n\texpectedBody := string(expectedBodyFile)\n\texpectedBody = strings.ReplaceAll(expectedBody, \"{documentRoot}\", documentRoot)\n\texpectedBody = strings.ReplaceAll(expectedBody, \"\\r\\n\", \"\\n\")\n\texpectedBody = strings.ReplaceAll(expectedBody, \"{testPort}\", testPort)\n\texpectedBody = strings.ReplaceAll(expectedBody, documentRoot+\"/\", documentRoot+string(filepath.Separator))\n\ttester := caddytest.NewTester(t)\n\tinitServer(t, tester, `\n\t\t{\n\t\t\tskip_install_trust\n\t\t\tadmin localhost:2999\n\t\t\thttp_port `+testPort+`\n\t\t}\n\t\tlocalhost:`+testPort+` {\n\t\t\troute {\n\t\t\t    root ../testdata\n\t\t\t    # rewrite to test that the original path is passed as $REQUEST_URI\n\t\t\t    rewrite /server-all-vars-ordered.php/path\n\t\t\t\tphp\n\t\t\t}\n\t\t}\n\t\t`, \"caddyfile\")\n\ttester.AssertPostResponseBody(\n\t\t\"http://user@localhost:\"+testPort+\"/original-path?specialChars=%3E\\\\x00%00</>\",\n\t\t[]string{\n\t\t\t\"Content-Type: application/x-www-form-urlencoded\",\n\t\t\t\"Content-Length: 14\", // maliciously set to 14\n\t\t\t\"Special-Chars: <%00>\",\n\t\t\t\"Host: Malicious Host\",\n\t\t\t\"X-Empty-Header:\",\n\t\t},\n\t\tbytes.NewBufferString(\"foo=bar\"),\n\t\thttp.StatusOK,\n\t\texpectedBody,\n\t)\n}\n\nfunc TestPHPIniConfiguration(t *testing.T) {\n\ttester := caddytest.NewTester(t)\n\tinitServer(t, tester, `\n\t\t{\n\t\t\tskip_install_trust\n\t\t\tadmin localhost:2999\n\t\t\thttp_port `+testPort+`\n\n\t\t\tfrankenphp {\n\t\t\t\tnum_threads 2\n\t\t\t\tworker ../testdata/ini.php 1\n\t\t\t\tphp_ini upload_max_filesize 100M\n\t\t\t\tphp_ini memory_limit 10000000\n\t\t\t}\n\t\t}\n\n\t\tlocalhost:`+testPort+` {\n\t\t\troute {\n\t\t\t\troot ../testdata\n\t\t\t\tphp\n\t\t\t}\n\t\t}\n\t\t`, \"caddyfile\")\n\n\ttestSingleIniConfiguration(tester, \"upload_max_filesize\", \"100M\")\n\ttestSingleIniConfiguration(tester, \"memory_limit\", \"10000000\")\n}\n\nfunc TestPHPIniBlockConfiguration(t *testing.T) {\n\ttester := caddytest.NewTester(t)\n\tinitServer(t, tester, `\n\t\t{\n\t\t\tskip_install_trust\n\t\t\tadmin localhost:2999\n\t\t\thttp_port `+testPort+`\n\n\t\t\tfrankenphp {\n\t\t\t\tnum_threads 1\n\t\t\t\tphp_ini {\n\t\t\t\t\tupload_max_filesize 100M\n\t\t\t\t\tmemory_limit 20000000\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tlocalhost:`+testPort+` {\n\t\t\troute {\n\t\t\t\troot ../testdata\n\t\t\t\tphp\n\t\t\t}\n\t\t}\n\t\t`, \"caddyfile\")\n\n\ttestSingleIniConfiguration(tester, \"upload_max_filesize\", \"100M\")\n\ttestSingleIniConfiguration(tester, \"memory_limit\", \"20000000\")\n}\n\nfunc testSingleIniConfiguration(tester *caddytest.Tester, key string, value string) {\n\t// test twice to ensure the ini setting is not lost\n\tfor range 2 {\n\t\ttester.AssertGetResponse(\n\t\t\t\"http://localhost:\"+testPort+\"/ini.php?key=\"+key,\n\t\t\thttp.StatusOK,\n\t\t\tkey+\":\"+value,\n\t\t)\n\t}\n}\n\nfunc TestOsEnv(t *testing.T) {\n\ttester := caddytest.NewTester(t)\n\tinitServer(t, tester, `\n\t\t{\n\t\t\tskip_install_trust\n\t\t\tadmin localhost:2999\n\t\t\thttp_port `+testPort+`\n\n\t\t\tfrankenphp {\n\t\t\t\tnum_threads 2\n\t\t\t\tphp_ini variables_order \"EGPCS\"\n\t\t\t\tworker ../testdata/env/env.php 1\n\t\t\t}\n\t\t}\n\n\t\tlocalhost:`+testPort+` {\n\t\t\troute {\n\t\t\t\troot ../testdata\n\t\t\t\tphp\n\t\t\t}\n\t\t}\n\t\t`, \"caddyfile\")\n\n\ttester.AssertGetResponse(\n\t\t\"http://localhost:\"+testPort+\"/env/env.php?keys[]=ENV1&keys[]=ENV2\",\n\t\thttp.StatusOK,\n\t\t\"ENV1=value1,ENV2=value2\",\n\t)\n}\n\nfunc TestMaxWaitTime(t *testing.T) {\n\ttester := caddytest.NewTester(t)\n\tinitServer(t, tester, `\n\t\t{\n\t\t\tskip_install_trust\n\t\t\tadmin localhost:2999\n\t\t\thttp_port `+testPort+`\n\n\t\t\tfrankenphp {\n\t\t\t\tnum_threads 1\n\t\t\t\tmax_wait_time 1ns\n\t\t\t}\n\t\t}\n\n\t\tlocalhost:`+testPort+` {\n\t\t\troute {\n\t\t\t\troot ../testdata\n\t\t\t\tphp\n\t\t\t}\n\t\t}\n\t\t`, \"caddyfile\")\n\n\t// send 10 requests simultaneously, at least one request should be stalled longer than 1ns\n\t// since we only have 1 thread, this will cause a 504 Gateway Timeout\n\twg := sync.WaitGroup{}\n\tsuccess := atomic.Bool{}\n\twg.Add(10)\n\tfor range 10 {\n\t\tgo func() {\n\t\t\tstatusCode := getStatusCode(\"http://localhost:\"+testPort+\"/sleep.php?sleep=10\", t)\n\t\t\tif statusCode == http.StatusServiceUnavailable {\n\t\t\t\tsuccess.Store(true)\n\t\t\t}\n\t\t\twg.Done()\n\t\t}()\n\t}\n\twg.Wait()\n\n\trequire.True(t, success.Load(), \"At least one request should have failed with a 503 Service Unavailable status\")\n}\n\nfunc TestMaxWaitTimeWorker(t *testing.T) {\n\ttester := caddytest.NewTester(t)\n\tinitServer(t, tester, `\n\t\t{\n\t\t\tskip_install_trust\n\t\t\tadmin localhost:2999\n\t\t\thttp_port `+testPort+`\n\t\t\tmetrics\n\n\t\t\tfrankenphp {\n\t\t\t\tnum_threads 2\n\t\t\t\tmax_wait_time 1ns\n\t\t\t\tworker {\n\t\t\t\t\tnum 1\n\t\t\t\t\tname service\n\t\t\t\t\tfile ../testdata/sleep.php\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tlocalhost:`+testPort+` {\n\t\t\troute {\n\t\t\t\troot ../testdata\n\t\t\t\tphp\n\t\t\t}\n\t\t}\n\t\t`, \"caddyfile\")\n\n\t// send 10 requests simultaneously, at least one request should be stalled longer than 1ns\n\t// since we only have 1 thread, this will cause a 504 Gateway Timeout\n\twg := sync.WaitGroup{}\n\tsuccess := atomic.Bool{}\n\twg.Add(10)\n\tfor range 10 {\n\t\tgo func() {\n\t\t\tstatusCode := getStatusCode(\"http://localhost:\"+testPort+\"/sleep.php?sleep=10&iteration=1\", t)\n\t\t\tif statusCode == http.StatusServiceUnavailable {\n\t\t\t\tsuccess.Store(true)\n\t\t\t}\n\t\t\twg.Done()\n\t\t}()\n\t}\n\twg.Wait()\n\trequire.True(t, success.Load(), \"At least one request should have failed with a 503 Service Unavailable status\")\n\n\t// Fetch metrics\n\tresp, err := http.Get(\"http://localhost:2999/metrics\")\n\trequire.NoError(t, err, \"failed to fetch metrics\")\n\tt.Cleanup(func() {\n\t\trequire.NoError(t, resp.Body.Close())\n\t})\n\n\t// Read and parse metrics\n\tmetrics := new(bytes.Buffer)\n\t_, err = metrics.ReadFrom(resp.Body)\n\trequire.NoError(t, err)\n\n\texpectedMetrics := `\n\t# TYPE frankenphp_worker_queue_depth gauge\n\tfrankenphp_worker_queue_depth{worker=\"service\"} 0\n\t`\n\n\tctx := caddy.ActiveContext()\n\trequire.NoError(t,\n\t\ttestutil.GatherAndCompare(\n\t\t\tctx.GetMetricsRegistry(),\n\t\t\tstrings.NewReader(expectedMetrics),\n\t\t\t\"frankenphp_worker_queue_depth\",\n\t\t))\n}\n\nfunc getStatusCode(url string, t *testing.T) int {\n\treq, err := http.NewRequest(\"GET\", url, nil)\n\trequire.NoError(t, err)\n\n\tresp, err := http.DefaultClient.Do(req)\n\trequire.NoError(t, err)\n\trequire.NoError(t, resp.Body.Close())\n\n\treturn resp.StatusCode\n}\n\nfunc TestMultiWorkersMetrics(t *testing.T) {\n\tvar wg sync.WaitGroup\n\ttester := caddytest.NewTester(t)\n\tinitServer(t, tester, `\n\t{\n\t\tskip_install_trust\n\t\tadmin localhost:2999\n\t\thttp_port `+testPort+`\n\t\thttps_port 9443\n\t\tmetrics\n\n\t\tfrankenphp {\n\t\t\tworker {\n\t\t\t\tname service1\n\t\t\t\tfile ../testdata/index.php\n\t\t\t\tnum 2\n\t\t\t}\n\t\t\tworker {\n\t\t\t\tname service2\n\t\t\t\tfile ../testdata/ini.php\n\t\t\t\tnum 3\n\t\t\t}\n\t\t}\n\t}\n\n\tlocalhost:`+testPort+` {\n\t\troute {\n\t\t\tphp {\n\t\t\t\troot ../testdata\n\t\t\t}\n\t\t}\n\t}\n\n\texample.com:`+testPort+` {\n\t\troute {\n\t\t\tphp {\n\t\t\t\troot ../testdata\n\t\t\t}\n\t\t}\n\t}\n\t`, \"caddyfile\")\n\n\t// Make some requests\n\tfor i := range 10 {\n\t\twg.Add(1)\n\t\tgo func(i int) {\n\t\t\ttester.AssertGetResponse(fmt.Sprintf(\"http://localhost:\"+testPort+\"/index.php?i=%d\", i), http.StatusOK, fmt.Sprintf(\"I am by birth a Genevese (%d)\", i))\n\t\t\twg.Done()\n\t\t}(i)\n\t}\n\twg.Wait()\n\n\t// Fetch metrics\n\tresp, err := http.Get(\"http://localhost:2999/metrics\")\n\trequire.NoError(t, err, \"failed to fetch metrics\")\n\tt.Cleanup(func() {\n\t\trequire.NoError(t, resp.Body.Close())\n\t})\n\n\t// Read and parse metrics\n\tmetrics := new(bytes.Buffer)\n\t_, err = metrics.ReadFrom(resp.Body)\n\trequire.NoError(t, err, \"failed to read metrics\")\n\n\tcpus := strconv.Itoa(getNumThreads(t, tester))\n\n\t// Check metrics\n\texpectedMetrics := `\n\t# HELP frankenphp_total_threads Total number of PHP threads\n\t# TYPE frankenphp_total_threads counter\n\tfrankenphp_total_threads ` + cpus + `\n\n\t# HELP frankenphp_busy_threads Number of busy PHP threads\n\t# TYPE frankenphp_busy_threads gauge\n\tfrankenphp_busy_threads 5\n\n\t# HELP frankenphp_busy_workers Number of busy PHP workers for this worker\n\t# TYPE frankenphp_busy_workers gauge\n\tfrankenphp_busy_workers{worker=\"service1\"} 0\n\n\t# HELP frankenphp_total_workers Total number of PHP workers for this worker\n\t# TYPE frankenphp_total_workers gauge\n\tfrankenphp_total_workers{worker=\"service1\"} 2\n\tfrankenphp_total_workers{worker=\"service2\"} 3\n\n\t# HELP frankenphp_worker_request_count\n\t# TYPE frankenphp_worker_request_count counter\n\tfrankenphp_worker_request_count{worker=\"service1\"} 10\n\n\t# HELP frankenphp_ready_workers Running workers that have successfully called frankenphp_handle_request at least once\n\t# TYPE frankenphp_ready_workers gauge\n\tfrankenphp_ready_workers{worker=\"service1\"} 2\n\tfrankenphp_ready_workers{worker=\"service2\"} 3\n\t`\n\n\tctx := caddy.ActiveContext()\n\trequire.NoError(t,\n\t\ttestutil.GatherAndCompare(\n\t\t\tctx.GetMetricsRegistry(),\n\t\t\tstrings.NewReader(expectedMetrics),\n\t\t\t\"frankenphp_total_threads\",\n\t\t\t\"frankenphp_busy_threads\",\n\t\t\t\"frankenphp_busy_workers\",\n\t\t\t\"frankenphp_total_workers\",\n\t\t\t\"frankenphp_worker_request_count\",\n\t\t\t\"frankenphp_ready_workers\",\n\t\t))\n}\n\nfunc TestDisabledMetrics(t *testing.T) {\n\tvar wg sync.WaitGroup\n\ttester := caddytest.NewTester(t)\n\tinitServer(t, tester, `\n\t{\n\t\tskip_install_trust\n\t\tadmin localhost:2999\n\t\thttp_port `+testPort+`\n\t\thttps_port 9443\n\n\t\tfrankenphp {\n\t\t\tworker {\n\t\t\t\tname service1\n\t\t\t\tfile ../testdata/index.php\n\t\t\t\tnum 2\n\t\t\t}\n\t\t\tworker {\n\t\t\t\tname service2\n\t\t\t\tfile ../testdata/ini.php\n\t\t\t\tnum 3\n\t\t\t}\n\t\t}\n\t}\n\n\tlocalhost:`+testPort+` {\n\t\troute {\n\t\t\tphp {\n\t\t\t\troot ../testdata\n\t\t\t}\n\t\t}\n\t}\n\n\texample.com:`+testPort+` {\n\t\troute {\n\t\t\tphp {\n\t\t\t\troot ../testdata\n\t\t\t}\n\t\t}\n\t}\n\t`, \"caddyfile\")\n\n\t// Make some requests\n\tfor i := range 10 {\n\t\twg.Add(1)\n\t\tgo func(i int) {\n\t\t\ttester.AssertGetResponse(fmt.Sprintf(\"http://localhost:\"+testPort+\"/index.php?i=%d\", i), http.StatusOK, fmt.Sprintf(\"I am by birth a Genevese (%d)\", i))\n\t\t\twg.Done()\n\t\t}(i)\n\t}\n\twg.Wait()\n\n\t// Fetch metrics\n\tresp, err := http.Get(\"http://localhost:2999/metrics\")\n\trequire.NoError(t, err, \"failed to fetch metrics\")\n\tt.Cleanup(func() {\n\t\trequire.NoError(t, resp.Body.Close())\n\t})\n\n\t// Read and parse metrics\n\tmetrics := new(bytes.Buffer)\n\t_, err = metrics.ReadFrom(resp.Body)\n\trequire.NoError(t, err, \"failed to read metrics\")\n\n\tctx := caddy.ActiveContext()\n\tcount, err := testutil.GatherAndCount(\n\t\tctx.GetMetricsRegistry(),\n\t\t\"frankenphp_busy_threads\",\n\t\t\"frankenphp_busy_workers\",\n\t\t\"frankenphp_queue_depth\",\n\t\t\"frankenphp_ready_workers\",\n\t\t\"frankenphp_total_threads\",\n\t\t\"frankenphp_total_workers\",\n\t\t\"frankenphp_worker_request_count\",\n\t\t\"frankenphp_worker_request_time\",\n\t)\n\n\trequire.NoError(t, err, \"failed to count metrics\")\n\trequire.Zero(t, count, \"metrics should be missing\")\n}\n\nfunc TestWorkerRestart(t *testing.T) {\n\tvar wg sync.WaitGroup\n\ttester := caddytest.NewTester(t)\n\tinitServer(t, tester, `\n\t{\n\t\tskip_install_trust\n\t\tadmin localhost:2999\n\t\thttp_port `+testPort+`\n\t\thttps_port 9443\n\n\t\tmetrics\n\t\tfrankenphp {\n\t\t\tworker {\n\t\t\t\tname service\n\t\t\t\tfile ../testdata/worker-restart.php\n\t\t\t\tnum 1\n\t\t\t\t# restart every 3 requests\n\t\t\t\tenv EVERY 3\n\t\t\t}\n\t\t}\n\t}\n\n\tlocalhost:`+testPort+` {\n\t\troute {\n\t\t\tphp {\n\t\t\t\troot ../testdata\n\t\t\t}\n\t\t}\n\t}\n\t`, \"caddyfile\")\n\n\tctx := caddy.ActiveContext()\n\n\tresp, err := http.Get(\"http://localhost:2999/metrics\")\n\trequire.NoError(t, err, \"failed to fetch metrics\")\n\tt.Cleanup(func() {\n\t\trequire.NoError(t, resp.Body.Close())\n\t})\n\n\t// Read and parse metrics\n\tmetrics := new(bytes.Buffer)\n\t_, err = metrics.ReadFrom(resp.Body)\n\trequire.NoError(t, err, \"failed to read metrics\")\n\n\t// frankenphp_worker_restarts should be missing\n\tcount, err := testutil.GatherAndCount(\n\t\tctx.GetMetricsRegistry(),\n\t\t\"frankenphp_worker_restarts\",\n\t)\n\trequire.NoError(t, err, \"failed to count metrics\")\n\trequire.Zero(t, count, \"metrics should be missing\")\n\n\t// Check metrics\n\texpectedMetrics := `\n\t# HELP frankenphp_ready_workers Running workers that have successfully called frankenphp_handle_request at least once\n\t# TYPE frankenphp_ready_workers gauge\n\tfrankenphp_ready_workers{worker=\"service\"} 1\n\t# HELP frankenphp_total_workers Total number of PHP workers for this worker\n\t# TYPE frankenphp_total_workers gauge\n\tfrankenphp_total_workers{worker=\"service\"} 1\n\t`\n\n\trequire.NoError(t,\n\t\ttestutil.GatherAndCompare(\n\t\t\tctx.GetMetricsRegistry(),\n\t\t\tstrings.NewReader(expectedMetrics),\n\t\t\t\"frankenphp_total_workers\",\n\t\t\t\"frankenphp_ready_workers\",\n\t\t))\n\n\t// Make some requests\n\tfor i := range 10 {\n\t\twg.Add(1)\n\t\tgo func(i int) {\n\t\t\ttester.AssertGetResponse(fmt.Sprintf(\"http://localhost:\"+testPort+\"/worker-restart.php?i=%d\", i), http.StatusOK, fmt.Sprintf(\"Counter (%d)\", i))\n\t\t\twg.Done()\n\t\t}(i)\n\t}\n\twg.Wait()\n\n\t// frankenphp_ready_workers should be back to 1 even after worker restarts\n\texpectedMetrics = `\n\t# HELP frankenphp_ready_workers Running workers that have successfully called frankenphp_handle_request at least once\n\t# TYPE frankenphp_ready_workers gauge\n\tfrankenphp_ready_workers{worker=\"service\"} 1\n\t# HELP frankenphp_total_workers Total number of PHP workers for this worker\n\t# TYPE frankenphp_total_workers gauge\n\tfrankenphp_total_workers{worker=\"service\"} 1\n\t# HELP frankenphp_worker_restarts Number of PHP worker restarts for this worker\n\t# TYPE frankenphp_worker_restarts counter\n\tfrankenphp_worker_restarts{worker=\"service\"} 3\n\t`\n\n\trequire.NoError(t,\n\t\ttestutil.GatherAndCompare(\n\t\t\tctx.GetMetricsRegistry(),\n\t\t\tstrings.NewReader(expectedMetrics),\n\t\t\t\"frankenphp_total_workers\",\n\t\t\t\"frankenphp_ready_workers\",\n\t\t\t\"frankenphp_worker_restarts\",\n\t\t))\n}\n\nfunc TestWorkerMatchDirective(t *testing.T) {\n\ttester := caddytest.NewTester(t)\n\tinitServer(t, tester, `\n\t\t{\n\t\t\tskip_install_trust\n\t\t\tadmin localhost:2999\n\t\t}\n\n\t\thttp://localhost:`+testPort+` {\n\t\t\tphp_server {\n\t\t\t\troot ../testdata/files\n\t\t\t\tworker {\n\t\t\t\t\tfile ../worker-with-counter.php\n\t\t\t\t\tmatch /matched-path*\n\t\t\t\t\tnum 1\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t`, \"caddyfile\")\n\n\t// worker is outside public directory, match anyway\n\ttester.AssertGetResponse(\"http://localhost:\"+testPort+\"/matched-path\", http.StatusOK, \"requests:1\")\n\ttester.AssertGetResponse(\"http://localhost:\"+testPort+\"/matched-path/anywhere\", http.StatusOK, \"requests:2\")\n\n\t// 404 on unmatched paths\n\ttester.AssertGetResponse(\"http://localhost:\"+testPort+\"/elsewhere\", http.StatusNotFound, \"\")\n\n\t// static file will be served by the fileserver\n\texpectedFileResponse, err := os.ReadFile(\"../testdata/files/static.txt\")\n\trequire.NoError(t, err, \"static.txt file must be readable for this test\")\n\ttester.AssertGetResponse(\"http://localhost:\"+testPort+\"/static.txt\", http.StatusOK, string(expectedFileResponse))\n}\n\nfunc TestWorkerMatchDirectiveWithMultipleWorkers(t *testing.T) {\n\ttester := caddytest.NewTester(t)\n\tinitServer(t, tester, `\n\t\t{\n\t\t\tskip_install_trust\n\t\t\tadmin localhost:2999\n\t\t}\n\t\thttp://localhost:`+testPort+` {\n\t\t\tphp_server {\n\t\t\t\troot ../testdata\n\t\t\t\tworker {\n\t\t\t\t\tfile worker-with-counter.php\n\t\t\t\t\tmatch /counter/*\n\t\t\t\t\tnum 1\n\t\t\t\t}\n\t\t\t\tworker {\n\t\t\t\t\tfile index.php\n\t\t\t\t\tmatch /index/*\n\t\t\t\t\tnum 1\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t`, \"caddyfile\")\n\n\t// match 2 workers respectively (in the public directory)\n\ttester.AssertGetResponse(\"http://localhost:\"+testPort+\"/counter/sub-path\", http.StatusOK, \"requests:1\")\n\ttester.AssertGetResponse(\"http://localhost:\"+testPort+\"/index/sub-path\", http.StatusOK, \"I am by birth a Genevese (i not set)\")\n\n\t// static file will be served by the fileserver\n\texpectedFileResponse, err := os.ReadFile(\"../testdata/files/static.txt\")\n\trequire.NoError(t, err, \"static.txt file must be readable for this test\")\n\ttester.AssertGetResponse(\"http://localhost:\"+testPort+\"/files/static.txt\", http.StatusOK, string(expectedFileResponse))\n\n\t// serve php file directly as fallback\n\ttester.AssertGetResponse(\"http://localhost:\"+testPort+\"/hello.php\", http.StatusOK, \"Hello from PHP\")\n\n\t// serve index.php file directly as fallback\n\ttester.AssertGetResponse(\"http://localhost:\"+testPort+\"/index.php\", http.StatusOK, \"I am by birth a Genevese (i not set)\")\n\ttester.AssertGetResponse(\"http://localhost:\"+testPort+\"/not-matched\", http.StatusOK, \"I am by birth a Genevese (i not set)\")\n}\n\nfunc TestWorkerMatchDirectiveWithoutFileServer(t *testing.T) {\n\ttester := caddytest.NewTester(t)\n\tinitServer(t, tester, `\n\t\t{\n\t\t\tskip_install_trust\n\t\t\tadmin localhost:2999\n\t\t}\n\n\t\thttp://localhost:`+testPort+` {\n\t\t\troute {\n\t\t\t\tphp_server {\n\t\t\t\t\tindex off\n\t\t\t\t\tfile_server off\n\t\t\t\t\troot ../testdata/files\n\t\t\t\t\tworker {\n\t\t\t\t\t\tfile ../worker-with-counter.php\n\t\t\t\t\t\tmatch /some-path\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\trespond \"Request falls through\" 404\n\t\t\t}\n\t\t}\n\t\t`, \"caddyfile\")\n\n\t// find the worker at some-path\n\ttester.AssertGetResponse(\"http://localhost:\"+testPort+\"/some-path\", http.StatusOK, \"requests:1\")\n\n\t// do not find the file at static.txt\n\t// the request should completely fall through the php_server module\n\ttester.AssertGetResponse(\"http://localhost:\"+testPort+\"/static.txt\", http.StatusNotFound, \"Request falls through\")\n}\n\nfunc TestDd(t *testing.T) {\n\ttester := caddytest.NewTester(t)\n\tinitServer(t, tester, `\n\t\t{\n\t\t\tskip_install_trust\n\t\t\tadmin localhost:2999\n\t\t}\n\n\t\thttp://localhost:`+testPort+` {\n\t\t\tphp {\n\t\t\t\tworker ../testdata/dd.php 1 {\n\t\t\t\t\tmatch *\n\t\t\t\t}\n\t\t\t}\n\t\t`, \"caddyfile\")\n\n\t// simulate Symfony's dd()\n\ttester.AssertGetResponse(\n\t\t\"http://localhost:\"+testPort+\"/some-path?output=dump123\",\n\t\thttp.StatusInternalServerError,\n\t\t\"dump123\",\n\t)\n}\n\nfunc TestLog(t *testing.T) {\n\ttester := caddytest.NewTester(t)\n\tinitServer(t, tester, `\n\t\t{\n\t\t\tskip_install_trust\n\t\t\tadmin localhost:2999\n\t\t}\n\n\t\thttp://localhost:`+testPort+` {\n\t\t\tlog {\n\t\t\t\toutput stdout\n\t\t\t\tformat json\n\t\t\t}\n\n\t\t\troot ../testdata\n\t\t\tphp_server {\n\t\t\t\tworker ../testdata/log-frankenphp_log.php\n\t\t\t}\n\t\t}\n\t\t`, \"caddyfile\")\n\n\ttester.AssertGetResponse(\n\t\t\"http://localhost:\"+testPort+\"/log-frankenphp_log.php?i=0\",\n\t\thttp.StatusOK,\n\t\t\"\",\n\t)\n}\n\n// TestSymlinkWorkerPaths tests different ways to reference worker scripts in symlinked directories\nfunc TestSymlinkWorkerPaths(t *testing.T) {\n\tcwd, _ := os.Getwd()\n\tpublicDir := filepath.Join(cwd, \"..\", \"testdata\", \"symlinks\", \"public\")\n\tskipIfSymlinkNotValid(t, publicDir)\n\n\tt.Run(\"NeighboringWorkerScript\", func(t *testing.T) {\n\t\t// Scenario: neighboring worker script\n\t\t// Given frankenphp located in the test folder\n\t\t// When I execute `frankenphp php-server --listen localhost:8080 -w index.php` from `public`\n\t\t// Then I expect to see the worker script executed successfully\n\t\ttester := caddytest.NewTester(t)\n\t\tinitServer(t, tester, `\n\t\t\t{\n\t\t\t\tskip_install_trust\n\t\t\t\tadmin localhost:2999\n\t\t\t\thttp_port `+testPort+`\n\n\t\t\t\tfrankenphp {\n\t\t\t\t\tworker `+publicDir+`/index.php 1\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tlocalhost:`+testPort+` {\n\t\t\t\troute {\n\t\t\t\t\tphp {\n\t\t\t\t\t\troot `+publicDir+`\n\t\t\t\t\t\tresolve_root_symlink true\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t`, \"caddyfile\")\n\n\t\ttester.AssertGetResponse(\"http://localhost:\"+testPort+\"/index.php\", http.StatusOK, \"Request: 0\\n\")\n\t})\n\n\tt.Run(\"NestedWorkerScript\", func(t *testing.T) {\n\t\t// Scenario: nested worker script\n\t\t// Given frankenphp located in the test folder\n\t\t// When I execute `frankenphp --listen localhost:8080 -w nested/index.php` from `public`\n\t\t// Then I expect to see the worker script executed successfully\n\t\ttester := caddytest.NewTester(t)\n\t\tinitServer(t, tester, `\n\t\t\t{\n\t\t\t\tskip_install_trust\n\t\t\t\tadmin localhost:2999\n\t\t\t\thttp_port `+testPort+`\n\n\t\t\t\tfrankenphp {\n\t\t\t\t\tworker `+publicDir+`/nested/index.php 1\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tlocalhost:`+testPort+` {\n\t\t\t\troute {\n\t\t\t\t\tphp {\n\t\t\t\t\t\troot `+publicDir+`\n\t\t\t\t\t\tresolve_root_symlink true\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t`, \"caddyfile\")\n\n\t\ttester.AssertGetResponse(\"http://localhost:\"+testPort+\"/nested/index.php\", http.StatusOK, \"Nested request: 0\\n\")\n\t})\n\n\tt.Run(\"OutsideSymlinkedFolder\", func(t *testing.T) {\n\t\t// Scenario: outside the symlinked folder\n\t\t// Given frankenphp located in the root folder\n\t\t// When I execute `frankenphp --listen localhost:8080 -w public/index.php` from the root folder\n\t\t// Then I expect to see the worker script executed successfully\n\t\ttester := caddytest.NewTester(t)\n\t\tinitServer(t, tester, `\n\t\t\t{\n\t\t\t\tskip_install_trust\n\t\t\t\tadmin localhost:2999\n\t\t\t\thttp_port `+testPort+`\n\n\t\t\t\tfrankenphp {\n\t\t\t\t\tworker {\n\t\t\t\t\t\tname outside_worker\n\t\t\t\t\t\tfile `+publicDir+`/index.php\n\t\t\t\t\t\tnum 1\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tlocalhost:`+testPort+` {\n\t\t\t\troute {\n\t\t\t\t\tphp {\n\t\t\t\t\t\troot `+publicDir+`\n\t\t\t\t\t\tresolve_root_symlink true\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t`, \"caddyfile\")\n\n\t\ttester.AssertGetResponse(\"http://localhost:\"+testPort+\"/index.php\", http.StatusOK, \"Request: 0\\n\")\n\t})\n\n\tt.Run(\"SpecifiedRootDirectory\", func(t *testing.T) {\n\t\t// Scenario: specified root directory\n\t\t// Given frankenphp located in the root folder\n\t\t// When I execute `frankenphp --listen localhost:8080 -w public/index.php -r public` from the root folder\n\t\t// Then I expect to see the worker script executed successfully\n\t\ttester := caddytest.NewTester(t)\n\t\tinitServer(t, tester, `\n\t\t\t{\n\t\t\t\tskip_install_trust\n\t\t\t\tadmin localhost:2999\n\t\t\t\thttp_port `+testPort+`\n\n\t\t\t\tfrankenphp {\n\t\t\t\t\tworker {\n\t\t\t\t\t\tname specified_root_worker\n\t\t\t\t\t\tfile `+publicDir+`/index.php\n\t\t\t\t\t\tnum 1\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tlocalhost:`+testPort+` {\n\t\t\t\troute {\n\t\t\t\t\tphp {\n\t\t\t\t\t\troot `+publicDir+`\n\t\t\t\t\t\tresolve_root_symlink true\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t`, \"caddyfile\")\n\n\t\ttester.AssertGetResponse(\"http://localhost:\"+testPort+\"/index.php\", http.StatusOK, \"Request: 0\\n\")\n\t})\n}\n\n// TestSymlinkResolveRoot tests the resolve_root_symlink directive behavior\nfunc TestSymlinkResolveRoot(t *testing.T) {\n\tcwd, _ := os.Getwd()\n\ttestDir := filepath.Join(cwd, \"..\", \"testdata\", \"symlinks\", \"test\")\n\tpublicDir := filepath.Join(cwd, \"..\", \"testdata\", \"symlinks\", \"public\")\n\tskipIfSymlinkNotValid(t, publicDir)\n\n\tt.Run(\"ResolveRootSymlink\", func(t *testing.T) {\n\t\t// Tests that resolve_root_symlink directive works correctly\n\t\ttester := caddytest.NewTester(t)\n\t\tinitServer(t, tester, `\n\t\t\t{\n\t\t\t\tskip_install_trust\n\t\t\t\tadmin localhost:2999\n\t\t\t\thttp_port `+testPort+`\n\n\t\t\t\tfrankenphp {\n\t\t\t\t\tworker `+publicDir+`/document-root.php 1\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tlocalhost:`+testPort+` {\n\t\t\t\troute {\n\t\t\t\t\tphp {\n\t\t\t\t\t\troot `+publicDir+`\n\t\t\t\t\t\tresolve_root_symlink true\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t`, \"caddyfile\")\n\n\t\t// DOCUMENT_ROOT should be the resolved path (testDir)\n\t\ttester.AssertGetResponse(\"http://localhost:\"+testPort+\"/document-root.php\", http.StatusOK, \"DOCUMENT_ROOT=\"+testDir+\"\\n\")\n\t})\n\n\tt.Run(\"NoResolveRootSymlink\", func(t *testing.T) {\n\t\t// Tests that symlinks are preserved when resolve_root_symlink is false (non-worker mode)\n\t\ttester := caddytest.NewTester(t)\n\t\tinitServer(t, tester, `\n\t\t\t{\n\t\t\t\tskip_install_trust\n\t\t\t\tadmin localhost:2999\n\t\t\t\thttp_port `+testPort+`\n\t\t\t}\n\n\t\t\tlocalhost:`+testPort+` {\n\t\t\t\troute {\n\t\t\t\t\tphp {\n\t\t\t\t\t\troot `+publicDir+`\n\t\t\t\t\t\tresolve_root_symlink false\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t`, \"caddyfile\")\n\n\t\t// DOCUMENT_ROOT should be the symlink path (publicDir) when resolve_root_symlink is false\n\t\ttester.AssertGetResponse(\"http://localhost:\"+testPort+\"/document-root.php\", http.StatusOK, \"DOCUMENT_ROOT=\"+publicDir+\"\\n\")\n\t})\n}\n\n// TestSymlinkWorkerBehavior tests worker behavior with symlinked directories\nfunc TestSymlinkWorkerBehavior(t *testing.T) {\n\tcwd, _ := os.Getwd()\n\tpublicDir := filepath.Join(cwd, \"..\", \"testdata\", \"symlinks\", \"public\")\n\tskipIfSymlinkNotValid(t, publicDir)\n\n\tt.Run(\"WorkerScriptFailsWithoutWorkerMode\", func(t *testing.T) {\n\t\t// Tests that accessing a worker-only script without configuring it as a worker actually results in an error\n\t\ttester := caddytest.NewTester(t)\n\t\tinitServer(t, tester, `\n\t\t\t{\n\t\t\t\tskip_install_trust\n\t\t\t\tadmin localhost:2999\n\t\t\t\thttp_port `+testPort+`\n\t\t\t}\n\n\t\t\tlocalhost:`+testPort+` {\n\t\t\t\troute {\n\t\t\t\t\tphp {\n\t\t\t\t\t\troot `+publicDir+`\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t`, \"caddyfile\")\n\n\t\t// Accessing the worker script without worker configuration MUST fail\n\t\t// The script checks $_SERVER['FRANKENPHP_WORKER'] and dies if not set\n\t\ttester.AssertGetResponse(\"http://localhost:\"+testPort+\"/index.php\", http.StatusOK, \"Error: This script must be run in worker mode (FRANKENPHP_WORKER not set to '1')\\n\")\n\t})\n\n\tt.Run(\"MultipleRequests\", func(t *testing.T) {\n\t\t// Tests that symlinked workers handle multiple requests correctly\n\t\ttester := caddytest.NewTester(t)\n\t\tinitServer(t, tester, `\n\t\t\t{\n\t\t\t\tskip_install_trust\n\t\t\t\tadmin localhost:2999\n\t\t\t\thttp_port `+testPort+`\n\t\t\t}\n\n\t\t\tlocalhost:`+testPort+` {\n\t\t\t\troute {\n\t\t\t\t\tphp {\n\t\t\t\t\t\troot `+publicDir+`\n\t\t\t\t\t\tresolve_root_symlink true\n\t\t\t\t\t\tworker index.php 1\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t`, \"caddyfile\")\n\n\t\t// Make multiple requests - each should increment the counter\n\t\tfor i := 0; i < 5; i++ {\n\t\t\ttester.AssertGetResponse(\"http://localhost:\"+testPort+\"/index.php\", http.StatusOK, fmt.Sprintf(\"Request: %d\\n\", i))\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "caddy/config_test.go",
    "content": "package caddy\n\nimport (\n\t\"testing\"\n\n\t\"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestModuleWorkerDuplicateFilenamesFail(t *testing.T) {\n\t// Create a test configuration with duplicate worker filenames\n\tconfigWithDuplicateFilenames := `\n\t{\n\t\tphp {\n\t\t\tworker {\n\t\t\t\tfile worker-with-env.php\n\t\t\t\tnum 1\n\t\t\t}\n\t\t\tworker {\n\t\t\t\tfile worker-with-env.php\n\t\t\t\tnum 2\n\t\t\t}\n\t\t}\n\t}`\n\n\t// Parse the configuration\n\td := caddyfile.NewTestDispenser(configWithDuplicateFilenames)\n\tmodule := &FrankenPHPModule{}\n\n\t// Unmarshal the configuration\n\terr := module.UnmarshalCaddyfile(d)\n\n\t// Verify that an error was returned\n\trequire.Error(t, err, \"Expected an error when two workers in the same module have the same filename\")\n\trequire.Contains(t, err.Error(), \"must not have duplicate filenames\", \"Error message should mention duplicate filenames\")\n}\n\nfunc TestModuleWorkersWithDifferentFilenames(t *testing.T) {\n\t// Create a test configuration with different worker filenames\n\tconfigWithDifferentFilenames := `\n\t{\n\t\tphp {\n\t\t\tworker ../testdata/worker-with-env.php\n\t\t\tworker ../testdata/worker-with-counter.php\n\t\t}\n\t}`\n\n\t// Parse the configuration\n\td := caddyfile.NewTestDispenser(configWithDifferentFilenames)\n\tmodule := &FrankenPHPModule{}\n\n\t// Unmarshal the configuration\n\terr := module.UnmarshalCaddyfile(d)\n\n\t// Verify that no error was returned\n\trequire.NoError(t, err, \"Expected no error when two workers in the same module have different filenames\")\n\n\t// Verify that both workers were added to the module\n\trequire.Len(t, module.Workers, 2, \"Expected two workers to be added to the module\")\n\trequire.Equal(t, \"../testdata/worker-with-env.php\", module.Workers[0].FileName, \"First worker should have the correct filename\")\n\trequire.Equal(t, \"../testdata/worker-with-counter.php\", module.Workers[1].FileName, \"Second worker should have the correct filename\")\n}\n\nfunc TestModuleWorkersDifferentNamesSucceed(t *testing.T) {\n\t// Create a test configuration with a worker name\n\tconfigWithWorkerName1 := `\n\t{\n\t\tphp_server {\n\t\t\tworker {\n\t\t\t\tname test-worker-1\n\t\t\t\tfile ../testdata/worker-with-env.php\n\t\t\t\tnum 1\n\t\t\t}\n\t\t}\n\t}`\n\n\t// Parse the first configuration\n\td1 := caddyfile.NewTestDispenser(configWithWorkerName1)\n\tapp := &FrankenPHPApp{}\n\tmodule1 := &FrankenPHPModule{}\n\n\t// Unmarshal the first configuration\n\terr := module1.UnmarshalCaddyfile(d1)\n\trequire.NoError(t, err, \"First module should be configured without errors\")\n\n\t// Create a second test configuration with a different worker name\n\tconfigWithWorkerName2 := `\n\t{\n\t\tphp_server {\n\t\t\tworker {\n\t\t\t\tname test-worker-2\n\t\t\t\tfile ../testdata/worker-with-env.php\n\t\t\t\tnum 1\n\t\t\t}\n\t\t}\n\t}`\n\n\t// Parse the second configuration\n\td2 := caddyfile.NewTestDispenser(configWithWorkerName2)\n\tmodule2 := &FrankenPHPModule{}\n\n\t// Unmarshal the second configuration\n\terr = module2.UnmarshalCaddyfile(d2)\n\n\t// Verify that no error was returned\n\trequire.NoError(t, err, \"Expected no error when two workers have different names\")\n\n\t_, err = app.addModuleWorkers(module1.Workers...)\n\trequire.NoError(t, err, \"Expected no error when adding the first module workers\")\n\t_, err = app.addModuleWorkers(module2.Workers...)\n\trequire.NoError(t, err, \"Expected no error when adding the second module workers\")\n\n\t// Verify that both workers were added\n\trequire.Len(t, app.Workers, 2, \"Expected two workers in the app\")\n\trequire.Equal(t, \"m#test-worker-1\", app.Workers[0].Name, \"First worker should have the correct name\")\n\trequire.Equal(t, \"m#test-worker-2\", app.Workers[1].Name, \"Second worker should have the correct name\")\n}\n\nfunc TestModuleWorkerWithEnvironmentVariables(t *testing.T) {\n\t// Create a test configuration with environment variables\n\tconfigWithEnv := `\n\t{\n\t\tphp {\n\t\t\tworker {\n\t\t\t\tfile ../testdata/worker-with-env.php\n\t\t\t\tnum 1\n\t\t\t\tenv APP_ENV production\n\t\t\t\tenv DEBUG true\n\t\t\t}\n\t\t}\n\t}`\n\n\t// Parse the configuration\n\td := caddyfile.NewTestDispenser(configWithEnv)\n\tmodule := &FrankenPHPModule{}\n\n\t// Unmarshal the configuration\n\terr := module.UnmarshalCaddyfile(d)\n\n\t// Verify that no error was returned\n\trequire.NoError(t, err, \"Expected no error when configuring a worker with environment variables\")\n\n\t// Verify that the worker was added to the module\n\trequire.Len(t, module.Workers, 1, \"Expected one worker to be added to the module\")\n\trequire.Equal(t, \"../testdata/worker-with-env.php\", module.Workers[0].FileName, \"Worker should have the correct filename\")\n\n\t// Verify that the environment variables were set correctly\n\trequire.Len(t, module.Workers[0].Env, 2, \"Expected two environment variables\")\n\trequire.Equal(t, \"production\", module.Workers[0].Env[\"APP_ENV\"], \"APP_ENV should be set to production\")\n\trequire.Equal(t, \"true\", module.Workers[0].Env[\"DEBUG\"], \"DEBUG should be set to true\")\n}\n\nfunc TestModuleWorkerWithWatchConfiguration(t *testing.T) {\n\t// Create a test configuration with watch directories\n\tconfigWithWatch := `\n\t{\n\t\tphp {\n\t\t\tworker {\n\t\t\t\tfile ../testdata/worker-with-env.php\n\t\t\t\tnum 1\n\t\t\t\twatch\n\t\t\t\twatch ./src/**/*.php\n\t\t\t\twatch ./config/**/*.yaml\n\t\t\t}\n\t\t}\n\t}`\n\n\t// Parse the configuration\n\td := caddyfile.NewTestDispenser(configWithWatch)\n\tmodule := &FrankenPHPModule{}\n\n\t// Unmarshal the configuration\n\terr := module.UnmarshalCaddyfile(d)\n\n\t// Verify that no error was returned\n\trequire.NoError(t, err, \"Expected no error when configuring a worker with watch directories\")\n\n\t// Verify that the worker was added to the module\n\trequire.Len(t, module.Workers, 1, \"Expected one worker to be added to the module\")\n\trequire.Equal(t, \"../testdata/worker-with-env.php\", module.Workers[0].FileName, \"Worker should have the correct filename\")\n\n\t// Verify that the watch directories were set correctly\n\trequire.Len(t, module.Workers[0].Watch, 3, \"Expected three watch patterns\")\n\trequire.Equal(t, defaultWatchPattern, module.Workers[0].Watch[0], \"First watch pattern should be the default\")\n\trequire.Equal(t, \"./src/**/*.php\", module.Workers[0].Watch[1], \"Second watch pattern should match the configuration\")\n\trequire.Equal(t, \"./config/**/*.yaml\", module.Workers[0].Watch[2], \"Third watch pattern should match the configuration\")\n}\n\nfunc TestModuleWorkerWithCustomName(t *testing.T) {\n\t// Create a test configuration with a custom worker name\n\tconfigWithCustomName := `\n\t{\n\t\tphp {\n\t\t\tworker {\n\t\t\t\tfile ../testdata/worker-with-env.php\n\t\t\t\tnum 1\n\t\t\t\tname custom-worker-name\n\t\t\t}\n\t\t}\n\t}`\n\n\t// Parse the configuration\n\td := caddyfile.NewTestDispenser(configWithCustomName)\n\tmodule := &FrankenPHPModule{}\n\tapp := &FrankenPHPApp{}\n\n\t// Unmarshal the configuration\n\terr := module.UnmarshalCaddyfile(d)\n\n\t// Verify that no error was returned\n\trequire.NoError(t, err, \"Expected no error when configuring a worker with a custom name\")\n\n\t// Verify that the worker was added to the module\n\trequire.Len(t, module.Workers, 1, \"Expected one worker to be added to the module\")\n\trequire.Equal(t, \"../testdata/worker-with-env.php\", module.Workers[0].FileName, \"Worker should have the correct filename\")\n\n\t// Verify that the worker was added to app.Workers with the m# prefix\n\tmodule.Workers, err = app.addModuleWorkers(module.Workers...)\n\trequire.NoError(t, err, \"Expected no error when adding the worker to the app\")\n\trequire.Equal(t, \"m#custom-worker-name\", module.Workers[0].Name, \"Worker should have the custom name, prefixed with m#\")\n\trequire.Equal(t, \"m#custom-worker-name\", app.Workers[0].Name, \"Worker should have the custom name, prefixed with m#\")\n}\n"
  },
  {
    "path": "caddy/extinit.go",
    "content": "package caddy\n\nimport (\n\t\"errors\"\n\t\"log\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/dunglas/frankenphp/internal/extgen\"\n\n\tcaddycmd \"github.com/caddyserver/caddy/v2/cmd\"\n\t\"github.com/spf13/cobra\"\n)\n\nfunc init() {\n\tcaddycmd.RegisterCommand(caddycmd.Command{\n\t\tName:  \"extension-init\",\n\t\tUsage: \"go_extension.go [--verbose]\",\n\t\tShort: \"Initializes a PHP extension from a Go file (EXPERIMENTAL)\",\n\t\tLong: `\nInitializes a PHP extension from a Go file. This command generates the necessary C files for the extension, including the header and source files, as well as the arginfo file.`,\n\t\tCobraFunc: func(cmd *cobra.Command) {\n\t\t\tcmd.Flags().BoolP(\"debug\", \"v\", false, \"Enable verbose debug logs\")\n\n\t\t\tcmd.RunE = caddycmd.WrapCommandFuncForCobra(cmdInitExtension)\n\t\t},\n\t})\n}\n\nfunc cmdInitExtension(_ caddycmd.Flags) (int, error) {\n\tif len(os.Args) < 3 {\n\t\treturn 1, errors.New(\"the path to the Go source is required\")\n\t}\n\n\tsourceFile := os.Args[2]\n\tbaseName := extgen.SanitizePackageName(strings.TrimSuffix(filepath.Base(sourceFile), \".go\"))\n\n\tgenerator := extgen.Generator{BaseName: baseName, SourceFile: sourceFile, BuildDir: filepath.Dir(sourceFile)}\n\n\tif err := generator.Generate(); err != nil {\n\t\treturn 1, err\n\t}\n\n\tlog.Printf(\"PHP extension %q initialized successfully in directory %q\", baseName, generator.BuildDir)\n\n\treturn 0, nil\n}\n"
  },
  {
    "path": "caddy/frankenphp/Caddyfile",
    "content": "# The Caddyfile is an easy way to configure FrankenPHP and the Caddy web server.\n#\n# https://frankenphp.dev/docs/config\n# https://caddyserver.com/docs/caddyfile\n\n{\n\tskip_install_trust\n\n\t{$CADDY_GLOBAL_OPTIONS}\n\n\tfrankenphp {\n\t\t{$FRANKENPHP_CONFIG}\n\t}\n}\n\n{$CADDY_EXTRA_CONFIG}\n\n{$SERVER_NAME:localhost} {\n\t#log {\n\t#\t# Redact the authorization query parameter that can be set by Mercure\n\t#\tformat filter {\n\t#\t\trequest>uri query {\n\t#\t\t\treplace authorization REDACTED\n\t#\t\t}\n\t#\t}\n\t#}\n\n\troot {$SERVER_ROOT:public/}\n\tencode zstd br gzip\n\n\t# Uncomment the following lines to enable Mercure and Vulcain modules\n\t#mercure {\n\t#\t# Publisher JWT key\n\t#\tpublisher_jwt {env.MERCURE_PUBLISHER_JWT_KEY} {env.MERCURE_PUBLISHER_JWT_ALG}\n\t#\t# Subscriber JWT key\n\t#\tsubscriber_jwt {env.MERCURE_SUBSCRIBER_JWT_KEY} {env.MERCURE_SUBSCRIBER_JWT_ALG}\n\t#\t# Allow anonymous subscribers (double-check that it's what you want)\n\t#\tanonymous\n\t#\t# Enable the subscription API (double-check that it's what you want)\n\t#\tsubscriptions\n\t#\t# Extra directives\n\t#\t{$MERCURE_EXTRA_DIRECTIVES}\n\t#}\n\t#vulcain\n\n\t{$CADDY_SERVER_EXTRA_DIRECTIVES}\n\n\tphp_server {\n\t\t#worker /path/to/your/worker.php\n\t}\n}\n\n# As an alternative to editing the above site block, you can add your own site\n# block files in the Caddyfile.d directory, and they will be included as long\n# as they use the .caddyfile extension.\n\nimport Caddyfile.d/*.caddyfile\n"
  },
  {
    "path": "caddy/frankenphp/cbrotli.go",
    "content": "//go:build !nobrotli\n\npackage main\n\nimport _ \"github.com/dunglas/caddy-cbrotli\"\n"
  },
  {
    "path": "caddy/frankenphp/main.go",
    "content": "package main\n\nimport (\n\tcaddycmd \"github.com/caddyserver/caddy/v2/cmd\"\n\n\t// plug in Caddy modules here.\n\t_ \"github.com/caddyserver/caddy/v2/modules/standard\"\n\t_ \"github.com/dunglas/frankenphp/caddy\"\n\t_ \"github.com/dunglas/mercure/caddy\"\n\t_ \"github.com/dunglas/vulcain/caddy\"\n)\n\nfunc main() {\n\tcaddycmd.Main()\n}\n"
  },
  {
    "path": "caddy/go.mod",
    "content": "module github.com/dunglas/frankenphp/caddy\n\ngo 1.26.0\n\nreplace github.com/dunglas/frankenphp => ../\n\nretract v1.0.0-rc.1 // Human error\n\nrequire (\n\tgithub.com/caddyserver/caddy/v2 v2.11.2\n\tgithub.com/caddyserver/certmagic v0.25.2\n\tgithub.com/dunglas/caddy-cbrotli v1.0.1\n\tgithub.com/dunglas/frankenphp v1.12.1\n\tgithub.com/dunglas/mercure v0.21.11\n\tgithub.com/dunglas/mercure/caddy v0.21.11\n\tgithub.com/dunglas/vulcain/caddy v1.4.0\n\tgithub.com/prometheus/client_golang v1.23.2\n\tgithub.com/spf13/cobra v1.10.2\n\tgithub.com/stretchr/testify v1.11.1\n)\n\nrequire github.com/smallstep/go-attestation v0.4.4-0.20241119153605-2306d5b464ca // indirect\n\nrequire (\n\tcel.dev/expr v0.25.1 // indirect\n\tcloud.google.com/go/auth v0.18.2 // indirect\n\tcloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect\n\tcloud.google.com/go/compute/metadata v0.9.0 // indirect\n\tdario.cat/mergo v1.0.2 // indirect\n\tfilippo.io/bigmod v0.1.0 // indirect\n\tfilippo.io/edwards25519 v1.2.0 // indirect\n\tgithub.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect\n\tgithub.com/BurntSushi/toml v1.6.0 // indirect\n\tgithub.com/DeRuina/timberjack v1.3.9 // indirect\n\tgithub.com/KimMachineGun/automemlimit v0.7.5 // indirect\n\tgithub.com/Masterminds/goutils v1.1.1 // indirect\n\tgithub.com/Masterminds/semver/v3 v3.4.0 // indirect\n\tgithub.com/Masterminds/sprig/v3 v3.3.0 // indirect\n\tgithub.com/MauriceGit/skiplist v0.0.0-20211105230623-77f5c8d3e145 // indirect\n\tgithub.com/MicahParks/jwkset v0.11.0 // indirect\n\tgithub.com/MicahParks/keyfunc/v3 v3.8.0 // indirect\n\tgithub.com/RoaringBitmap/roaring/v2 v2.15.0 // indirect\n\tgithub.com/alecthomas/chroma/v2 v2.23.1 // indirect\n\tgithub.com/antlr4-go/antlr/v4 v4.13.1 // indirect\n\tgithub.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b // indirect\n\tgithub.com/beorn7/perks v1.0.1 // indirect\n\tgithub.com/bits-and-blooms/bitset v1.24.4 // indirect\n\tgithub.com/caddyserver/zerossl v0.1.5 // indirect\n\tgithub.com/ccoveille/go-safecast/v2 v2.0.0 // indirect\n\tgithub.com/cenkalti/backoff/v5 v5.0.3 // indirect\n\tgithub.com/cespare/xxhash v1.1.0 // indirect\n\tgithub.com/cespare/xxhash/v2 v2.3.0 // indirect\n\tgithub.com/chzyer/readline v1.5.1 // indirect\n\tgithub.com/cloudflare/circl v1.6.3 // indirect\n\tgithub.com/coreos/go-oidc/v3 v3.17.0 // indirect\n\tgithub.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect\n\tgithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect\n\tgithub.com/dgraph-io/badger v1.6.2 // indirect\n\tgithub.com/dgraph-io/badger/v2 v2.2007.4 // indirect\n\tgithub.com/dgraph-io/ristretto v0.2.0 // indirect\n\tgithub.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da // indirect\n\tgithub.com/dlclark/regexp2 v1.11.5 // indirect\n\tgithub.com/dunglas/httpsfv v1.1.0 // indirect\n\tgithub.com/dunglas/skipfilter v1.0.0 // indirect\n\tgithub.com/dunglas/vulcain v1.4.0 // indirect\n\tgithub.com/dustin/go-humanize v1.0.1 // indirect\n\tgithub.com/e-dant/watcher v0.0.0-20260223030516-06f84a1314be // indirect\n\tgithub.com/felixge/httpsnoop v1.0.4 // indirect\n\tgithub.com/fsnotify/fsnotify v1.9.0 // indirect\n\tgithub.com/fxamacker/cbor/v2 v2.9.0 // indirect\n\tgithub.com/getkin/kin-openapi v0.133.0 // indirect\n\tgithub.com/go-chi/chi/v5 v5.2.5 // indirect\n\tgithub.com/go-jose/go-jose/v3 v3.0.4 // indirect\n\tgithub.com/go-jose/go-jose/v4 v4.1.3 // indirect\n\tgithub.com/go-logr/logr v1.4.3 // indirect\n\tgithub.com/go-logr/stdr v1.2.2 // indirect\n\tgithub.com/go-openapi/jsonpointer v0.22.5 // indirect\n\tgithub.com/go-openapi/swag/jsonname v0.25.5 // indirect\n\tgithub.com/go-sql-driver/mysql v1.9.3 // indirect\n\tgithub.com/go-viper/mapstructure/v2 v2.5.0 // indirect\n\tgithub.com/gofrs/uuid v4.4.0+incompatible // indirect\n\tgithub.com/gofrs/uuid/v5 v5.4.0 // indirect\n\tgithub.com/golang-jwt/jwt/v5 v5.3.1 // indirect\n\tgithub.com/golang/protobuf v1.5.4 // indirect\n\tgithub.com/golang/snappy v1.0.0 // indirect\n\tgithub.com/google/brotli/go/cbrotli v1.1.0 // indirect\n\tgithub.com/google/cel-go v0.27.0 // indirect\n\tgithub.com/google/certificate-transparency-go v1.3.3 // indirect\n\tgithub.com/google/go-tpm v0.9.8 // indirect\n\tgithub.com/google/go-tspi v0.3.0 // indirect\n\tgithub.com/google/s2a-go v0.1.9 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect\n\tgithub.com/googleapis/gax-go/v2 v2.17.0 // indirect\n\tgithub.com/gorilla/handlers v1.5.2 // indirect\n\tgithub.com/gorilla/mux v1.8.1 // indirect\n\tgithub.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect\n\tgithub.com/huandu/xstrings v1.5.0 // indirect\n\tgithub.com/inconshreveable/mousetrap v1.1.0 // indirect\n\tgithub.com/jackc/pgpassfile v1.0.0 // indirect\n\tgithub.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect\n\tgithub.com/jackc/pgx/v5 v5.8.0 // indirect\n\tgithub.com/jackc/puddle/v2 v2.2.2 // indirect\n\tgithub.com/josharian/intern v1.0.0 // indirect\n\tgithub.com/klauspost/compress v1.18.4 // indirect\n\tgithub.com/klauspost/cpuid/v2 v2.3.0 // indirect\n\tgithub.com/kylelemons/godebug v1.1.0 // indirect\n\tgithub.com/libdns/libdns v1.1.1 // indirect\n\tgithub.com/mailru/easyjson v0.9.1 // indirect\n\tgithub.com/manifoldco/promptui v0.9.0 // indirect\n\tgithub.com/mattn/go-colorable v0.1.14 // indirect\n\tgithub.com/mattn/go-isatty v0.0.20 // indirect\n\tgithub.com/maypok86/otter/v2 v2.3.0 // indirect\n\tgithub.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect\n\tgithub.com/mholt/acmez/v3 v3.1.6 // indirect\n\tgithub.com/miekg/dns v1.1.72 // indirect\n\tgithub.com/mitchellh/copystructure v1.2.0 // indirect\n\tgithub.com/mitchellh/go-ps v1.0.0 // indirect\n\tgithub.com/mitchellh/reflectwalk v1.0.2 // indirect\n\tgithub.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect\n\tgithub.com/mschoch/smat v0.2.0 // indirect\n\tgithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect\n\tgithub.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect\n\tgithub.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect\n\tgithub.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect\n\tgithub.com/pelletier/go-toml/v2 v2.2.4 // indirect\n\tgithub.com/perimeterx/marshmallow v1.1.5 // indirect\n\tgithub.com/pires/go-proxyproto v0.11.0 // indirect\n\tgithub.com/pkg/errors v0.9.1 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect\n\tgithub.com/prometheus/client_model v0.6.2 // indirect\n\tgithub.com/prometheus/common v0.67.5 // indirect\n\tgithub.com/prometheus/otlptranslator v1.0.0 // indirect\n\tgithub.com/prometheus/procfs v0.20.1 // indirect\n\tgithub.com/quic-go/qpack v0.6.0 // indirect\n\tgithub.com/quic-go/quic-go v0.59.0 // indirect\n\tgithub.com/rs/cors v1.11.1 // indirect\n\tgithub.com/rs/xid v1.6.0 // indirect\n\tgithub.com/russross/blackfriday/v2 v2.1.0 // indirect\n\tgithub.com/sagikazarmark/locafero v0.12.0 // indirect\n\tgithub.com/shopspring/decimal v1.4.0 // indirect\n\tgithub.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect\n\tgithub.com/sirupsen/logrus v1.9.4 // indirect\n\tgithub.com/slackhq/nebula v1.10.3 // indirect\n\tgithub.com/smallstep/certificates v0.30.0-rc3 // indirect\n\tgithub.com/smallstep/cli-utils v0.12.2 // indirect\n\tgithub.com/smallstep/linkedca v0.25.0 // indirect\n\tgithub.com/smallstep/nosql v0.7.0 // indirect\n\tgithub.com/smallstep/pkcs7 v0.2.1 // indirect\n\tgithub.com/smallstep/scep v0.0.0-20250318231241-a25cabb69492 // indirect\n\tgithub.com/smallstep/truststore v0.13.0 // indirect\n\tgithub.com/spf13/afero v1.15.0 // indirect\n\tgithub.com/spf13/cast v1.10.0 // indirect\n\tgithub.com/spf13/pflag v1.0.10 // indirect\n\tgithub.com/spf13/viper v1.21.0 // indirect\n\tgithub.com/subosito/gotenv v1.6.0 // indirect\n\tgithub.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect\n\tgithub.com/tailscale/tscert v0.0.0-20251216020129-aea342f6d747 // indirect\n\tgithub.com/tidwall/gjson v1.18.0 // indirect\n\tgithub.com/tidwall/match v1.2.0 // indirect\n\tgithub.com/tidwall/pretty v1.2.1 // indirect\n\tgithub.com/tidwall/sjson v1.2.5 // indirect\n\tgithub.com/unrolled/secure v1.17.0 // indirect\n\tgithub.com/urfave/cli v1.22.17 // indirect\n\tgithub.com/woodsbury/decimal128 v1.4.0 // indirect\n\tgithub.com/x448/float16 v0.8.4 // indirect\n\tgithub.com/yosida95/uritemplate/v3 v3.0.2 // indirect\n\tgithub.com/yuin/goldmark v1.7.16 // indirect\n\tgithub.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc // indirect\n\tgithub.com/zeebo/blake3 v0.2.4 // indirect\n\tgo.etcd.io/bbolt v1.4.3 // indirect\n\tgo.opentelemetry.io/auto/sdk v1.2.1 // indirect\n\tgo.opentelemetry.io/contrib/bridges/prometheus v0.67.0 // indirect\n\tgo.opentelemetry.io/contrib/exporters/autoexport v0.67.0 // indirect\n\tgo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect\n\tgo.opentelemetry.io/contrib/propagators/autoprop v0.67.0 // indirect\n\tgo.opentelemetry.io/contrib/propagators/aws v1.42.0 // indirect\n\tgo.opentelemetry.io/contrib/propagators/b3 v1.42.0 // indirect\n\tgo.opentelemetry.io/contrib/propagators/jaeger v1.42.0 // indirect\n\tgo.opentelemetry.io/contrib/propagators/ot v1.42.0 // indirect\n\tgo.opentelemetry.io/otel v1.42.0 // indirect\n\tgo.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.18.0 // indirect\n\tgo.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.18.0 // indirect\n\tgo.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0 // indirect\n\tgo.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.42.0 // indirect\n\tgo.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 // indirect\n\tgo.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 // indirect\n\tgo.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0 // indirect\n\tgo.opentelemetry.io/otel/exporters/prometheus v0.64.0 // indirect\n\tgo.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.18.0 // indirect\n\tgo.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.42.0 // indirect\n\tgo.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.42.0 // indirect\n\tgo.opentelemetry.io/otel/log v0.18.0 // indirect\n\tgo.opentelemetry.io/otel/metric v1.42.0 // indirect\n\tgo.opentelemetry.io/otel/sdk v1.42.0 // indirect\n\tgo.opentelemetry.io/otel/sdk/log v0.18.0 // indirect\n\tgo.opentelemetry.io/otel/sdk/metric v1.42.0 // indirect\n\tgo.opentelemetry.io/otel/trace v1.42.0 // indirect\n\tgo.opentelemetry.io/proto/otlp v1.9.0 // indirect\n\tgo.step.sm/crypto v0.76.2 // indirect\n\tgo.uber.org/automaxprocs v1.6.0 // indirect\n\tgo.uber.org/multierr v1.11.0 // indirect\n\tgo.uber.org/zap v1.27.1 // indirect\n\tgo.uber.org/zap/exp v0.3.0 // indirect\n\tgo.yaml.in/yaml/v2 v2.4.4 // indirect\n\tgo.yaml.in/yaml/v3 v3.0.4 // indirect\n\tgolang.org/x/crypto v0.48.0 // indirect\n\tgolang.org/x/crypto/x509roots/fallback v0.0.0-20260213171211-a408498e5541 // indirect\n\tgolang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa // indirect\n\tgolang.org/x/mod v0.33.0 // indirect\n\tgolang.org/x/net v0.51.0 // indirect\n\tgolang.org/x/oauth2 v0.36.0 // indirect\n\tgolang.org/x/sync v0.20.0 // indirect\n\tgolang.org/x/sys v0.42.0 // indirect\n\tgolang.org/x/term v0.40.0 // indirect\n\tgolang.org/x/text v0.34.0 // indirect\n\tgolang.org/x/time v0.15.0 // indirect\n\tgolang.org/x/tools v0.42.0 // indirect\n\tgoogle.golang.org/api v0.270.0 // indirect\n\tgoogle.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171 // indirect\n\tgoogle.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect\n\tgoogle.golang.org/grpc v1.79.2 // indirect\n\tgoogle.golang.org/grpc/cmd/protoc-gen-go-grpc v1.6.1 // indirect\n\tgoogle.golang.org/protobuf v1.36.11 // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n\thowett.net/plist v1.0.1 // indirect\n)\n"
  },
  {
    "path": "caddy/go.sum",
    "content": "cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=\ncel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=\ncloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=\ncloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=\ncloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM=\ncloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M=\ncloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=\ncloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=\ncloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=\ncloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=\ncloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc=\ncloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU=\ncloud.google.com/go/kms v1.25.0 h1:gVqvGGUmz0nYCmtoxWmdc1wli2L1apgP8U4fghPGSbQ=\ncloud.google.com/go/kms v1.25.0/go.mod h1:XIdHkzfj0bUO3E+LvwPg+oc7s58/Ns8Nd8Sdtljihbk=\ncloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8=\ncloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk=\ncode.pfad.fr/check v1.1.0 h1:GWvjdzhSEgHvEHe2uJujDcpmZoySKuHQNrZMfzfO0bE=\ncode.pfad.fr/check v1.1.0/go.mod h1:NiUH13DtYsb7xp5wll0U4SXx7KhXQVCtRgdC96IPfoM=\ndario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=\ndario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=\nfilippo.io/bigmod v0.1.0 h1:UNzDk7y9ADKST+axd9skUpBQeW7fG2KrTZyOE4uGQy8=\nfilippo.io/bigmod v0.1.0/go.mod h1:OjOXDNlClLblvXdwgFFOQFJEocLhhtai8vGLy0JCZlI=\nfilippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=\nfilippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=\ngithub.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIoKjsnZuH8vjyaysT/ses3EvZeaV/1UkF2M=\ngithub.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=\ngithub.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=\ngithub.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=\ngithub.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=\ngithub.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=\ngithub.com/DeRuina/timberjack v1.3.9 h1:6UXZ1I7ExPGTX/1UNYawR58LlOJUHKBPiYC7WQ91eBo=\ngithub.com/DeRuina/timberjack v1.3.9/go.mod h1:RLoeQrwrCGIEF8gO5nV5b/gMD0QIy7bzQhBUgpp1EqE=\ngithub.com/KimMachineGun/automemlimit v0.7.5 h1:RkbaC0MwhjL1ZuBKunGDjE/ggwAX43DwZrJqVwyveTk=\ngithub.com/KimMachineGun/automemlimit v0.7.5/go.mod h1:QZxpHaGOQoYvFhv/r4u3U0JTC2ZcOwbSr11UZF46UBM=\ngithub.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=\ngithub.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=\ngithub.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=\ngithub.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=\ngithub.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=\ngithub.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=\ngithub.com/MauriceGit/skiplist v0.0.0-20211105230623-77f5c8d3e145 h1:1yw6O62BReQ+uA1oyk9XaQTvLhcoHWmoQAgXmDFXpIY=\ngithub.com/MauriceGit/skiplist v0.0.0-20211105230623-77f5c8d3e145/go.mod h1:877WBceefKn14QwVVn4xRFUsHsZb9clICgdeTj4XsUg=\ngithub.com/MicahParks/jwkset v0.11.0 h1:yc0zG+jCvZpWgFDFmvs8/8jqqVBG9oyIbmBtmjOhoyQ=\ngithub.com/MicahParks/jwkset v0.11.0/go.mod h1:U2oRhRaLgDCLjtpGL2GseNKGmZtLs/3O7p+OZaL5vo0=\ngithub.com/MicahParks/keyfunc/v3 v3.8.0 h1:Hx2dgIjAXGk9slakM6rV9BOeaWDPEXXZ4Us8guNBfds=\ngithub.com/MicahParks/keyfunc/v3 v3.8.0/go.mod h1:z66bkCviwqfg2YUp+Jcc/xRE9IXLcMq6DrgV/+Htru0=\ngithub.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=\ngithub.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=\ngithub.com/RoaringBitmap/roaring/v2 v2.15.0 h1:gCbixa3UiG7g6WUZNVOfEEg2HTc1vR4OVdMkX8t1ZFc=\ngithub.com/RoaringBitmap/roaring/v2 v2.15.0/go.mod h1:eq4wdNXxtJIS/oikeCzdX1rBzek7ANzbth041hrU8Q4=\ngithub.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=\ngithub.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=\ngithub.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=\ngithub.com/alecthomas/chroma/v2 v2.23.1 h1:nv2AVZdTyClGbVQkIzlDm/rnhk1E9bU9nXwmZ/Vk/iY=\ngithub.com/alecthomas/chroma/v2 v2.23.1/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o=\ngithub.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=\ngithub.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=\ngithub.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=\ngithub.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=\ngithub.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=\ngithub.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=\ngithub.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b h1:uUXgbcPDK3KpW29o4iy7GtuappbWT0l5NaMo9H9pJDw=\ngithub.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=\ngithub.com/aws/aws-sdk-go-v2 v1.41.1 h1:ABlyEARCDLN034NhxlRUSZr4l71mh+T5KAeGh6cerhU=\ngithub.com/aws/aws-sdk-go-v2 v1.41.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0=\ngithub.com/aws/aws-sdk-go-v2/config v1.32.7 h1:vxUyWGUwmkQ2g19n7JY/9YL8MfAIl7bTesIUykECXmY=\ngithub.com/aws/aws-sdk-go-v2/config v1.32.7/go.mod h1:2/Qm5vKUU/r7Y+zUk/Ptt2MDAEKAfUtKc1+3U1Mo3oY=\ngithub.com/aws/aws-sdk-go-v2/credentials v1.19.7 h1:tHK47VqqtJxOymRrNtUXN5SP/zUTvZKeLx4tH6PGQc8=\ngithub.com/aws/aws-sdk-go-v2/credentials v1.19.7/go.mod h1:qOZk8sPDrxhf+4Wf4oT2urYJrYt3RejHSzgAquYeppw=\ngithub.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 h1:I0GyV8wiYrP8XpA70g1HBcQO1JlQxCMTW9npl5UbDHY=\ngithub.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17/go.mod h1:tyw7BOl5bBe/oqvoIeECFJjMdzXoa/dfVz3QQ5lgHGA=\ngithub.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 h1:xOLELNKGp2vsiteLsvLPwxC+mYmO6OZ8PYgiuPJzF8U=\ngithub.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17/go.mod h1:5M5CI3D12dNOtH3/mk6minaRwI2/37ifCURZISxA/IQ=\ngithub.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 h1:WWLqlh79iO48yLkj1v3ISRNiv+3KdQoZ6JWyfcsyQik=\ngithub.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17/go.mod h1:EhG22vHRrvF8oXSTYStZhJc1aUgKtnJe+aOiFEV90cM=\ngithub.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=\ngithub.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=\ngithub.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E=\ngithub.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow=\ngithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 h1:RuNSMoozM8oXlgLG/n6WLaFGoea7/CddrCfIiSA+xdY=\ngithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17/go.mod h1:F2xxQ9TZz5gDWsclCtPQscGpP0VUOc8RqgFM3vDENmU=\ngithub.com/aws/aws-sdk-go-v2/service/kms v1.49.5 h1:DKibav4XF66XSeaXcrn9GlWGHos6D/vJ4r7jsK7z5CE=\ngithub.com/aws/aws-sdk-go-v2/service/kms v1.49.5/go.mod h1:1SdcmEGUEQE1mrU2sIgeHtcMSxHuybhPvuEPANzIDfI=\ngithub.com/aws/aws-sdk-go-v2/service/signin v1.0.5 h1:VrhDvQib/i0lxvr3zqlUwLwJP4fpmpyD9wYG1vfSu+Y=\ngithub.com/aws/aws-sdk-go-v2/service/signin v1.0.5/go.mod h1:k029+U8SY30/3/ras4G/Fnv/b88N4mAfliNn08Dem4M=\ngithub.com/aws/aws-sdk-go-v2/service/sso v1.30.9 h1:v6EiMvhEYBoHABfbGB4alOYmCIrcgyPPiBE1wZAEbqk=\ngithub.com/aws/aws-sdk-go-v2/service/sso v1.30.9/go.mod h1:yifAsgBxgJWn3ggx70A3urX2AN49Y5sJTD1UQFlfqBw=\ngithub.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13 h1:gd84Omyu9JLriJVCbGApcLzVR3XtmC4ZDPcAI6Ftvds=\ngithub.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13/go.mod h1:sTGThjphYE4Ohw8vJiRStAcu3rbjtXRsdNB0TvZ5wwo=\ngithub.com/aws/aws-sdk-go-v2/service/sts v1.41.6 h1:5fFjR/ToSOzB2OQ/XqWpZBmNvmP/pJ1jOWYlFDJTjRQ=\ngithub.com/aws/aws-sdk-go-v2/service/sts v1.41.6/go.mod h1:qgFDZQSD/Kys7nJnVqYlWKnh0SSdMjAi0uSwON4wgYQ=\ngithub.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk=\ngithub.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=\ngithub.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=\ngithub.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=\ngithub.com/bits-and-blooms/bitset v1.24.4 h1:95H15Og1clikBrKr/DuzMXkQzECs1M6hhoGXLwLQOZE=\ngithub.com/bits-and-blooms/bitset v1.24.4/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=\ngithub.com/caddyserver/caddy/v2 v2.11.2 h1:iOlpsSiSKqEW+SIXrcZsZ/NO74SzB/ycqqvAIEfIm64=\ngithub.com/caddyserver/caddy/v2 v2.11.2/go.mod h1:ASNYYmKhIVWWMGPfNxclI5DqKEgU3FhmL+6NZWzQEag=\ngithub.com/caddyserver/certmagic v0.25.2 h1:D7xcS7ggX/WEY54x0czj7ioTkmDWKIgxtIi2OcQclUc=\ngithub.com/caddyserver/certmagic v0.25.2/go.mod h1:llW/CvsNmza8S6hmsuggsZeiX+uS27dkqY27wDIuBWg=\ngithub.com/caddyserver/zerossl v0.1.5 h1:dkvOjBAEEtY6LIGAHei7sw2UgqSD6TrWweXpV7lvEvE=\ngithub.com/caddyserver/zerossl v0.1.5/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4=\ngithub.com/ccoveille/go-safecast/v2 v2.0.0 h1:+5eyITXAUj3wMjad6cRVJKGnC7vDS55zk0INzJagub0=\ngithub.com/ccoveille/go-safecast/v2 v2.0.0/go.mod h1:JIYA4CAR33blIDuE6fSwCp2sz1oOBahXnvmdBhOAABs=\ngithub.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=\ngithub.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=\ngithub.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=\ngithub.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=\ngithub.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=\ngithub.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=\ngithub.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=\ngithub.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=\ngithub.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=\ngithub.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI=\ngithub.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=\ngithub.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=\ngithub.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=\ngithub.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=\ngithub.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=\ngithub.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=\ngithub.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=\ngithub.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=\ngithub.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc=\ngithub.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=\ngithub.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=\ngithub.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/dgraph-io/badger v1.6.2 h1:mNw0qs90GVgGGWylh0umH5iag1j6n/PeJtNvL6KY/x8=\ngithub.com/dgraph-io/badger v1.6.2/go.mod h1:JW2yswe3V058sS0kZ2h/AXeDSqFjxnZcRrVH//y2UQE=\ngithub.com/dgraph-io/badger/v2 v2.2007.4 h1:TRWBQg8UrlUhaFdco01nO2uXwzKS7zd+HVdwV/GHc4o=\ngithub.com/dgraph-io/badger/v2 v2.2007.4/go.mod h1:vSw/ax2qojzbN6eXHIx6KPKtCSHJN/Uz0X0VPruTIhk=\ngithub.com/dgraph-io/ristretto v0.0.2/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E=\ngithub.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E=\ngithub.com/dgraph-io/ristretto v0.2.0 h1:XAfl+7cmoUDWW/2Lx8TGZQjjxIQ2Ley9DSf52dru4WE=\ngithub.com/dgraph-io/ristretto v0.2.0/go.mod h1:8uBHCU/PBV4Ag0CJrP47b9Ofby5dqWNh4FicAdoqFNU=\ngithub.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=\ngithub.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38=\ngithub.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=\ngithub.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=\ngithub.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=\ngithub.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=\ngithub.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=\ngithub.com/dunglas/caddy-cbrotli v1.0.1 h1:mkg7EB1GmoyfBt3kY3mq4o/0bfnBeq7ZLQjmVmdBE3Y=\ngithub.com/dunglas/caddy-cbrotli v1.0.1/go.mod h1:uXABy3tjy1FABF+3JWKVh1ajFvIO/kfpwHaeZGSBaAY=\ngithub.com/dunglas/httpsfv v1.1.0 h1:Jw76nAyKWKZKFrpMMcL76y35tOpYHqQPzHQiwDvpe54=\ngithub.com/dunglas/httpsfv v1.1.0/go.mod h1:zID2mqw9mFsnt7YC3vYQ9/cjq30q41W+1AnDwH8TiMg=\ngithub.com/dunglas/mercure v0.21.11 h1:4Sd/Q77j8uh9SI5D9ZMg5sePlWs336+9CKxDQC1FV34=\ngithub.com/dunglas/mercure v0.21.11/go.mod h1:WPMgfqonUiO1qB+W8Tya63Ngag9ZwplGMXSOy8P/uMg=\ngithub.com/dunglas/mercure/caddy v0.21.11 h1:WnasC7EiqBPAB0CpBEPrm7vLiuL7o3BOVmfGDghnyVM=\ngithub.com/dunglas/mercure/caddy v0.21.11/go.mod h1:MlGm4jbpBV+9nizn03PDejTEM916z3WDP9zO/Yw8OYQ=\ngithub.com/dunglas/skipfilter v1.0.0 h1:JG9SgGg4n6BlFwuTYzb9RIqjH7PfwszvWehanrYWPF4=\ngithub.com/dunglas/skipfilter v1.0.0/go.mod h1:ryhr8j7CAHSjzeN7wI6YEuwoArQ3OQmRqWWVCEAfb9w=\ngithub.com/dunglas/vulcain v1.4.0 h1:uGMTLKmw53yJNKBwCtD3GOmnmGw4SfsIqYfb3NEKvbA=\ngithub.com/dunglas/vulcain v1.4.0/go.mod h1:WJjUJ/anaMlV4JWUCLNTNkviPFQL817yaVW5ErfiaMY=\ngithub.com/dunglas/vulcain/caddy v1.4.0 h1:u377qYQwDKRA2/CcZ7yIbRehuY+AjQM5xpkIyynsn1c=\ngithub.com/dunglas/vulcain/caddy v1.4.0/go.mod h1:qu6VV33pP42D8tIZDURE0ngt2MBR9OE34lCeMBo8caM=\ngithub.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=\ngithub.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=\ngithub.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=\ngithub.com/e-dant/watcher v0.0.0-20260223030516-06f84a1314be h1:vqHrvilasyJcnru/0Z4FoojsQJUIfXGVplte7JtupfY=\ngithub.com/e-dant/watcher v0.0.0-20260223030516-06f84a1314be/go.mod h1:PmV4IVmBJVqT2NcfTGN4+sZ+qGe3PA0qkphAtOHeFG0=\ngithub.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=\ngithub.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=\ngithub.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=\ngithub.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=\ngithub.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=\ngithub.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=\ngithub.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=\ngithub.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=\ngithub.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=\ngithub.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=\ngithub.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=\ngithub.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ=\ngithub.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE=\ngithub.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=\ngithub.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=\ngithub.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY=\ngithub.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=\ngithub.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=\ngithub.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=\ngithub.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=\ngithub.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=\ngithub.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=\ngithub.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=\ngithub.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=\ngithub.com/go-openapi/jsonpointer v0.22.5 h1:8on/0Yp4uTb9f4XvTrM2+1CPrV05QPZXu+rvu2o9jcA=\ngithub.com/go-openapi/jsonpointer v0.22.5/go.mod h1:gyUR3sCvGSWchA2sUBJGluYMbe1zazrYWIkWPjjMUY0=\ngithub.com/go-openapi/swag/jsonname v0.25.5 h1:8p150i44rv/Drip4vWI3kGi9+4W9TdI3US3uUYSFhSo=\ngithub.com/go-openapi/swag/jsonname v0.25.5/go.mod h1:jNqqikyiAK56uS7n8sLkdaNY/uq6+D2m2LANat09pKU=\ngithub.com/go-openapi/testify/v2 v2.4.0 h1:8nsPrHVCWkQ4p8h1EsRVymA2XABB4OT40gcvAu+voFM=\ngithub.com/go-openapi/testify/v2 v2.4.0/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=\ngithub.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=\ngithub.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=\ngithub.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=\ngithub.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=\ngithub.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=\ngithub.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=\ngithub.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=\ngithub.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=\ngithub.com/gofrs/uuid/v5 v5.4.0 h1:EfbpCTjqMuGyq5ZJwxqzn3Cbr2d0rUZU7v5ycAk/e/0=\ngithub.com/gofrs/uuid/v5 v5.4.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=\ngithub.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=\ngithub.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=\ngithub.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=\ngithub.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=\ngithub.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=\ngithub.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=\ngithub.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=\ngithub.com/google/brotli/go/cbrotli v1.1.0 h1:YwHD/rwSgUSL4b2S3ZM2jnNymm+tmwKQqjUIC63nmHU=\ngithub.com/google/brotli/go/cbrotli v1.1.0/go.mod h1:nOPhAkwVliJdNTkj3gXpljmWhjc4wCaVqbMJcPKWP4s=\ngithub.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=\ngithub.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=\ngithub.com/google/cel-go v0.27.0 h1:e7ih85+4qVrBuqQWTW4FKSqZYokVuc3HnhH5keboFTo=\ngithub.com/google/cel-go v0.27.0/go.mod h1:tTJ11FWqnhw5KKpnWpvW9CJC3Y9GK4EIS0WXnBbebzw=\ngithub.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg=\ngithub.com/google/certificate-transparency-go v1.3.3 h1:hq/rSxztSkXN2tx/3jQqF6Xc0O565UQPdHrOWvZwybo=\ngithub.com/google/certificate-transparency-go v1.3.3/go.mod h1:iR17ZgSaXRzSa5qvjFl8TnVD5h8ky2JMVio+dzoKMgA=\ngithub.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=\ngithub.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=\ngithub.com/google/go-tpm-tools v0.4.7 h1:J3ycC8umYxM9A4eF73EofRZu4BxY0jjQnUnkhIBbvws=\ngithub.com/google/go-tpm-tools v0.4.7/go.mod h1:gSyXTZHe3fgbzb6WEGd90QucmsnT1SRdlye82gH8QjQ=\ngithub.com/google/go-tspi v0.3.0 h1:ADtq8RKfP+jrTyIWIZDIYcKOMecRqNJFOew2IT0Inus=\ngithub.com/google/go-tspi v0.3.0/go.mod h1:xfMGI3G0PhxCdNVcYr1C4C+EizojDg/TXuX5by8CiHI=\ngithub.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=\ngithub.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8=\ngithub.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg=\ngithub.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc=\ngithub.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY=\ngithub.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=\ngithub.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w=\ngithub.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=\ngithub.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=\ngithub.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=\ngithub.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=\ngithub.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=\ngithub.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=\ngithub.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=\ngithub.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=\ngithub.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=\ngithub.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=\ngithub.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=\ngithub.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=\ngithub.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=\ngithub.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=\ngithub.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=\ngithub.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=\ngithub.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo=\ngithub.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw=\ngithub.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=\ngithub.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=\ngithub.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=\ngithub.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=\ngithub.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=\ngithub.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=\ngithub.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=\ngithub.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=\ngithub.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=\ngithub.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=\ngithub.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=\ngithub.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=\ngithub.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=\ngithub.com/letsencrypt/challtestsrv v1.4.2 h1:0ON3ldMhZyWlfVNYYpFuWRTmZNnyfiL9Hh5YzC3JVwU=\ngithub.com/letsencrypt/challtestsrv v1.4.2/go.mod h1:GhqMqcSoeGpYd5zX5TgwA6er/1MbWzx/o7yuuVya+Wk=\ngithub.com/letsencrypt/pebble/v2 v2.10.0 h1:Wq6gYXlsY6ubqI3hhxsTzdyotvfdjFBxuwYqCLCnj/U=\ngithub.com/letsencrypt/pebble/v2 v2.10.0/go.mod h1:Sk8cmUIPcIdv2nINo+9PB4L+ZBhzY+F9A1a/h/xmWiQ=\ngithub.com/libdns/libdns v1.1.1 h1:wPrHrXILoSHKWJKGd0EiAVmiJbFShguILTg9leS/P/U=\ngithub.com/libdns/libdns v1.1.1/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=\ngithub.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=\ngithub.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8=\ngithub.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=\ngithub.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=\ngithub.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=\ngithub.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=\ngithub.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=\ngithub.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=\ngithub.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/maypok86/otter/v2 v2.3.0 h1:8H8AVVFUSzJwIegKwv1uF5aGitTY+AIrtktg7OcLs8w=\ngithub.com/maypok86/otter/v2 v2.3.0/go.mod h1:XgIdlpmL6jYz882/CAx1E4C1ukfgDKSaw4mWq59+7l8=\ngithub.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=\ngithub.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=\ngithub.com/mholt/acmez/v3 v3.1.6 h1:eGVQNObP0pBN4sxqrXeg7MYqTOWyoiYpQqITVWlrevk=\ngithub.com/mholt/acmez/v3 v3.1.6/go.mod h1:5nTPosTGosLxF3+LU4ygbgMRFDhbAVpqMI4+a4aHLBY=\ngithub.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=\ngithub.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=\ngithub.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=\ngithub.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=\ngithub.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=\ngithub.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=\ngithub.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=\ngithub.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=\ngithub.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=\ngithub.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=\ngithub.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=\ngithub.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=\ngithub.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM=\ngithub.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw=\ngithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=\ngithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=\ngithub.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY=\ngithub.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw=\ngithub.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c=\ngithub.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o=\ngithub.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0=\ngithub.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y=\ngithub.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=\ngithub.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=\ngithub.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=\ngithub.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s=\ngithub.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=\ngithub.com/peterbourgon/diskv/v3 v3.0.1 h1:x06SQA46+PKIUftmEujdwSEpIx8kR+M9eLYsUxeYveU=\ngithub.com/peterbourgon/diskv/v3 v3.0.1/go.mod h1:kJ5Ny7vLdARGU3WUuy6uzO6T0nb/2gWcT1JiBvRmb5o=\ngithub.com/pires/go-proxyproto v0.11.0 h1:gUQpS85X/VJMdUsYyEgyn59uLJvGqPhJV5YvG68wXH4=\ngithub.com/pires/go-proxyproto v0.11.0/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=\ngithub.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=\ngithub.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=\ngithub.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=\ngithub.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=\ngithub.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=\ngithub.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=\ngithub.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=\ngithub.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=\ngithub.com/prometheus/otlptranslator v1.0.0 h1:s0LJW/iN9dkIH+EnhiD3BlkkP5QVIUVEoIwkU+A6qos=\ngithub.com/prometheus/otlptranslator v1.0.0/go.mod h1:vRYWnXvI6aWGpsdY/mOT/cbeVRBlPWtBNDb7kGR3uKM=\ngithub.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=\ngithub.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=\ngithub.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=\ngithub.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=\ngithub.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=\ngithub.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=\ngithub.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=\ngithub.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=\ngithub.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=\ngithub.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=\ngithub.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=\ngithub.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=\ngithub.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=\ngithub.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=\ngithub.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4=\ngithub.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=\ngithub.com/schollz/jsonstore v1.1.0 h1:WZBDjgezFS34CHI+myb4s8GGpir3UMpy7vWoCeO0n6E=\ngithub.com/schollz/jsonstore v1.1.0/go.mod h1:15c6+9guw8vDRyozGjN3FoILt0wpruJk9Pi66vjaZfg=\ngithub.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=\ngithub.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=\ngithub.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=\ngithub.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=\ngithub.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=\ngithub.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=\ngithub.com/slackhq/nebula v1.10.3 h1:EstYj8ODEcv6T0R9X5BVq1zgWZnyU5gtPzk99QF1PMU=\ngithub.com/slackhq/nebula v1.10.3/go.mod h1:IL5TUQm4x9IFx2kCKPYm1gP47pwd5b8QGnnBH2RHnvs=\ngithub.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 h1:unQFBIznI+VYD1/1fApl1A+9VcBk+9dcqGfnePY87LY=\ngithub.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262/go.mod h1:MyOHs9Po2fbM1LHej6sBUT8ozbxmMOFG+E+rx/GSGuc=\ngithub.com/smallstep/certificates v0.30.0-rc3 h1:Lx/NNJ4n+L3Pyx5NtVRGXeqviPPXTFFGLRiC1fCwU50=\ngithub.com/smallstep/certificates v0.30.0-rc3/go.mod h1:e5/ylYYpvnjCVZz6RpyOkpTe73EGPYoL+8TZZ5EtLjI=\ngithub.com/smallstep/cli-utils v0.12.2 h1:lGzM9PJrH/qawbzMC/s2SvgLdJPKDWKwKzx9doCVO+k=\ngithub.com/smallstep/cli-utils v0.12.2/go.mod h1:uCPqefO29goHLGqFnwk0i8W7XJu18X3WHQFRtOm/00Y=\ngithub.com/smallstep/go-attestation v0.4.4-0.20241119153605-2306d5b464ca h1:VX8L0r8vybH0bPeaIxh4NQzafKQiqvlOn8pmOXbFLO4=\ngithub.com/smallstep/go-attestation v0.4.4-0.20241119153605-2306d5b464ca/go.mod h1:vNAduivU014fubg6ewygkAvQC0IQVXqdc8vaGl/0er4=\ngithub.com/smallstep/linkedca v0.25.0 h1:txT9QHGbCsJq0MhAghBq7qhurGY727tQuqUi+n4BVBo=\ngithub.com/smallstep/linkedca v0.25.0/go.mod h1:Q3jVAauFKNlF86W5/RFtgQeyDKz98GL/KN3KG4mJOvc=\ngithub.com/smallstep/nosql v0.7.0 h1:YiWC9ZAHcrLCrayfaF+QJUv16I2bZ7KdLC3RpJcnAnE=\ngithub.com/smallstep/nosql v0.7.0/go.mod h1:H5VnKMCbeq9QA6SRY5iqPylfxLfYcLwvUff3onQ8+HU=\ngithub.com/smallstep/pkcs7 v0.2.1 h1:6Kfzr/QizdIuB6LSv8y1LJdZ3aPSfTNhTLqAx9CTLfA=\ngithub.com/smallstep/pkcs7 v0.2.1/go.mod h1:RcXHsMfL+BzH8tRhmrF1NkkpebKpq3JEM66cOFxanf0=\ngithub.com/smallstep/scep v0.0.0-20250318231241-a25cabb69492 h1:k23+s51sgYix4Zgbvpmy+1ZgXLjr4ZTkBTqXmpnImwA=\ngithub.com/smallstep/scep v0.0.0-20250318231241-a25cabb69492/go.mod h1:QQhwLqCS13nhv8L5ov7NgusowENUtXdEzdytjmJHdZQ=\ngithub.com/smallstep/truststore v0.13.0 h1:90if9htAOblavbMeWlqNLnO9bsjjgVv2hQeQJCi/py4=\ngithub.com/smallstep/truststore v0.13.0/go.mod h1:3tmMp2aLKZ/OA/jnFUB0cYPcho402UG2knuJoPh4j7A=\ngithub.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=\ngithub.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=\ngithub.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=\ngithub.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=\ngithub.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=\ngithub.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=\ngithub.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=\ngithub.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=\ngithub.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=\ngithub.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=\ngithub.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=\ngithub.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=\ngithub.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=\ngithub.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=\ngithub.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=\ngithub.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=\ngithub.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=\ngithub.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=\ngithub.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=\ngithub.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=\ngithub.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=\ngithub.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngithub.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=\ngithub.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=\ngithub.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 h1:Gzfnfk2TWrk8Jj4P4c1a3CtQyMaTVCznlkLZI++hok4=\ngithub.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55/go.mod h1:4k4QO+dQ3R5FofL+SanAUZe+/QfeK0+OIuwDIRu2vSg=\ngithub.com/tailscale/tscert v0.0.0-20251216020129-aea342f6d747 h1:RnBbFMmodYzhC6adOjTbtUQXyzV8dcvKYbolzs6Qch0=\ngithub.com/tailscale/tscert v0.0.0-20251216020129-aea342f6d747/go.mod h1:ejPAJui3kVK4u5TgMtqtXlWf5HnKh9fLy5kvpaeuas0=\ngithub.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=\ngithub.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=\ngithub.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=\ngithub.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=\ngithub.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM=\ngithub.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=\ngithub.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=\ngithub.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=\ngithub.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=\ngithub.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=\ngithub.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=\ngithub.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=\ngithub.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=\ngithub.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=\ngithub.com/unrolled/secure v1.17.0 h1:Io7ifFgo99Bnh0J7+Q+qcMzWM6kaDPCA5FroFZEdbWU=\ngithub.com/unrolled/secure v1.17.0/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40=\ngithub.com/urfave/cli v1.22.17 h1:SYzXoiPfQjHBbkYxbew5prZHS1TOLT3ierW8SYLqtVQ=\ngithub.com/urfave/cli v1.22.17/go.mod h1:b0ht0aqgH/6pBYzzxURyrM4xXNgsoT/n2ZzwQiEhNVo=\ngithub.com/woodsbury/decimal128 v1.4.0 h1:xJATj7lLu4f2oObouMt2tgGiElE5gO6mSWUjQsBgUlc=\ngithub.com/woodsbury/decimal128 v1.4.0/go.mod h1:BP46FUrVjVhdTbKT+XuQh2xfQaGki9LMIRJSFuh6THU=\ngithub.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=\ngithub.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=\ngithub.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=\ngithub.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=\ngithub.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=\ngithub.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=\ngithub.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=\ngithub.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=\ngithub.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=\ngithub.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ=\ngithub.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I=\ngithub.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY=\ngithub.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=\ngithub.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI=\ngithub.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE=\ngithub.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=\ngithub.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=\ngo.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=\ngo.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=\ngo.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=\ngo.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=\ngo.opentelemetry.io/contrib/bridges/prometheus v0.67.0 h1:dkBzNEAIKADEaFnuESzcXvpd09vxvDZsOjx11gjUqLk=\ngo.opentelemetry.io/contrib/bridges/prometheus v0.67.0/go.mod h1:Z5RIwRkZgauOIfnG5IpidvLpERjhTninpP1dTG2jTl4=\ngo.opentelemetry.io/contrib/exporters/autoexport v0.67.0 h1:4fnRcNpc6YFtG3zsFw9achKn3XgmxPxuMuqIL5rE8e8=\ngo.opentelemetry.io/contrib/exporters/autoexport v0.67.0/go.mod h1:qTvIHMFKoxW7HXg02gm6/Wofhq5p3Ib/A/NNt1EoBSQ=\ngo.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ=\ngo.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg=\ngo.opentelemetry.io/contrib/propagators/autoprop v0.67.0 h1:XhcQRf4MeqwQw96FcnatDAj6gwE19SUrWZ1VwNg77iE=\ngo.opentelemetry.io/contrib/propagators/autoprop v0.67.0/go.mod h1:7OK06SuNIBIlc5Uq3JGQEsKHuXw29t9OJemvDYyP1dk=\ngo.opentelemetry.io/contrib/propagators/aws v1.42.0 h1:Kbr3xDxs6kcxp5ThXTKWK2OtwLhNoXBVtqguNYcsZL0=\ngo.opentelemetry.io/contrib/propagators/aws v1.42.0/go.mod h1:Jzw9hZHtxdpCN7x8S17UH59X/EiFivp6VXLs9bdM1OQ=\ngo.opentelemetry.io/contrib/propagators/b3 v1.42.0 h1:B2Pew5ufEtgkjLF+tSkXjgYZXQr9m7aCm1wLKB0URbU=\ngo.opentelemetry.io/contrib/propagators/b3 v1.42.0/go.mod h1:iPgUcSEF5DORW6+yNbdw/YevUy+QqJ508ncjhrRSCjc=\ngo.opentelemetry.io/contrib/propagators/jaeger v1.42.0 h1:jP8unWI6q5kcb3gpGLjKDGaUa+JW+nHKWvpS/q+YuWA=\ngo.opentelemetry.io/contrib/propagators/jaeger v1.42.0/go.mod h1:xd89e/pUyPatUP1C4z1UknD9jHptESO99tWyvd4mWD4=\ngo.opentelemetry.io/contrib/propagators/ot v1.42.0 h1:uQjD1NNqX1+DfcAoWParPt1egNg9vC9gH4xarJ9Khxo=\ngo.opentelemetry.io/contrib/propagators/ot v1.42.0/go.mod h1:yw/c2TCmQLIv109HBOCn6NlJ8Dp7MNfjMcqQZRnAMmg=\ngo.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=\ngo.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc=\ngo.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.18.0 h1:deI9UQMoGFgrg5iLPgzueqFPHevDl+28YKfSpPTI6rY=\ngo.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.18.0/go.mod h1:PFx9NgpNUKXdf7J4Q3agRxMs3Y07QhTCVipKmLsMKnU=\ngo.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.18.0 h1:icqq3Z34UrEFk2u+HMhTtRsvo7Ues+eiJVjaJt62njs=\ngo.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.18.0/go.mod h1:W2m8P+d5Wn5kipj4/xmbt9uMqezEKfBjzVJadfABSBE=\ngo.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0 h1:MdKucPl/HbzckWWEisiNqMPhRrAOQX8r4jTuGr636gk=\ngo.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0/go.mod h1:RolT8tWtfHcjajEH5wFIZ4Dgh5jpPdFXYV9pTAk/qjc=\ngo.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.42.0 h1:H7O6RlGOMTizyl3R08Kn5pdM06bnH8oscSj7o11tmLA=\ngo.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.42.0/go.mod h1:mBFWu/WOVDkWWsR7Tx7h6EpQB8wsv7P0Yrh0Pb7othc=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 h1:THuZiwpQZuHPul65w4WcwEnkX2QIuMT+UFoOrygtoJw=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0/go.mod h1:J2pvYM5NGHofZ2/Ru6zw/TNWnEQp5crgyDeSrYpXkAw=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 h1:zWWrB1U6nqhS/k6zYB74CjRpuiitRtLLi68VcgmOEto=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0/go.mod h1:2qXPNBX1OVRC0IwOnfo1ljoid+RD0QK3443EaqVlsOU=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0 h1:uLXP+3mghfMf7XmV4PkGfFhFKuNWoCvvx5wP/wOXo0o=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0/go.mod h1:v0Tj04armyT59mnURNUJf7RCKcKzq+lgJs6QSjHjaTc=\ngo.opentelemetry.io/otel/exporters/prometheus v0.64.0 h1:g0LRDXMX/G1SEZtK8zl8Chm4K6GBwRkjPKE36LxiTYs=\ngo.opentelemetry.io/otel/exporters/prometheus v0.64.0/go.mod h1:UrgcjnarfdlBDP3GjDIJWe6HTprwSazNjwsI+Ru6hro=\ngo.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.18.0 h1:KJVjPD3rcPb98rIs3HznyJlrfx9ge5oJvxxlGR+P/7s=\ngo.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.18.0/go.mod h1:K3kRa2ckmHWQaTWQdPRHc7qGXASuVuoEQXzrvlA98Ws=\ngo.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.42.0 h1:lSZHgNHfbmQTPfuTmWVkEu8J8qXaQwuV30pjCcAUvP8=\ngo.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.42.0/go.mod h1:so9ounLcuoRDu033MW/E0AD4hhUjVqswrMF5FoZlBcw=\ngo.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.42.0 h1:s/1iRkCKDfhlh1JF26knRneorus8aOwVIDhvYx9WoDw=\ngo.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.42.0/go.mod h1:UI3wi0FXg1Pofb8ZBiBLhtMzgoTm1TYkMvn71fAqDzs=\ngo.opentelemetry.io/otel/log v0.18.0 h1:XgeQIIBjZZrliksMEbcwMZefoOSMI1hdjiLEiiB0bAg=\ngo.opentelemetry.io/otel/log v0.18.0/go.mod h1:KEV1kad0NofR3ycsiDH4Yjcoj0+8206I6Ox2QYFSNgI=\ngo.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4=\ngo.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI=\ngo.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo=\ngo.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts=\ngo.opentelemetry.io/otel/sdk/log v0.18.0 h1:n8OyZr7t7otkeTnPTbDNom6rW16TBYGtvyy2Gk6buQw=\ngo.opentelemetry.io/otel/sdk/log v0.18.0/go.mod h1:C0+wxkTwKpOCZLrlJ3pewPiiQwpzycPI/u6W0Z9fuYk=\ngo.opentelemetry.io/otel/sdk/log/logtest v0.18.0 h1:l3mYuPsuBx6UKE47BVcPrZoZ0q/KER57vbj2qkgDLXA=\ngo.opentelemetry.io/otel/sdk/log/logtest v0.18.0/go.mod h1:7cHtiVJpZebB3wybTa4NG+FUo5NPe3PROz1FqB0+qdw=\ngo.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA=\ngo.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc=\ngo.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=\ngo.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc=\ngo.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=\ngo.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=\ngo.step.sm/crypto v0.76.2 h1:JJ/yMcs/rmcCAwlo+afrHjq74XBFRTJw5B2y4Q4Z4c4=\ngo.step.sm/crypto v0.76.2/go.mod h1:m6KlB/HzIuGFep0UWI5e0SYi38UxpoKeCg6qUaHV6/Q=\ngo.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=\ngo.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=\ngo.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=\ngo.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=\ngo.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=\ngo.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=\ngo.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=\ngo.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=\ngo.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=\ngo.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=\ngo.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U=\ngo.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ=\ngo.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=\ngo.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=\ngo.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=\ngo.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=\ngolang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=\ngolang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=\ngolang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=\ngolang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=\ngolang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=\ngolang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=\ngolang.org/x/crypto/x509roots/fallback v0.0.0-20260213171211-a408498e5541 h1:FmKxj9ocLKn45jiR2jQMwCVhDvaK7fKQFzfuT9GvyK8=\ngolang.org/x/crypto/x509roots/fallback v0.0.0-20260213171211-a408498e5541/go.mod h1:+UoQFNBq2p2wO+Q6ddVtYc25GZ6VNdOMyyrd4nrqrKs=\ngolang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0=\ngolang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=\ngolang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=\ngolang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=\ngolang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=\ngolang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=\ngolang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=\ngolang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=\ngolang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\ngolang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=\ngolang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=\ngolang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=\ngolang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=\ngolang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=\ngolang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=\ngolang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=\ngolang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=\ngolang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=\ngolang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=\ngolang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=\ngolang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=\ngolang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=\ngolang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=\ngolang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=\ngolang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=\ngolang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=\ngolang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=\ngolang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=\ngolang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=\ngolang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=\ngolang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=\ngolang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=\ngolang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=\ngolang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=\ngolang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=\ngolang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=\ngolang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=\ngolang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=\ngolang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=\ngolang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=\ngolang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=\ngolang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=\ngolang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=\ngolang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=\ngolang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=\ngolang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=\ngolang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=\ngolang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=\ngonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=\ngoogle.golang.org/api v0.270.0 h1:4rJZbIuWSTohczG9mG2ukSDdt9qKx4sSSHIydTN26L4=\ngoogle.golang.org/api v0.270.0/go.mod h1:5+H3/8DlXpQWrSz4RjGGwz5HfJAQSEI8Bc6JqQNH77U=\ngoogle.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH80teYd2em3xtIkkHd7ZhqfH2N9CsM=\ngoogle.golang.org/genproto v0.0.0-20260128011058-8636f8732409/go.mod h1:rxKD3IEILWEu3P44seeNOAwZN4SaoKaQ/2eTg4mM6EM=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171 h1:tu/dtnW1o3wfaxCOjSLn5IRX4YDcJrtlpzYkhHhGaC4=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171/go.mod h1:M5krXqk4GhBKvB596udGL3UyjL4I1+cTbK0orROM9ng=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=\ngoogle.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU=\ngoogle.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=\ngoogle.golang.org/grpc/cmd/protoc-gen-go-grpc v1.6.1 h1:/WILD1UcXj/ujCxgoL/DvRgt2CP3txG8+FwkUbb9110=\ngoogle.golang.org/grpc/cmd/protoc-gen-go-grpc v1.6.1/go.mod h1:YNKnb2OAApgYn2oYY47Rn7alMr1zWjb2U8Q0aoGWiNc=\ngoogle.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=\ngoogle.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\nhowett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM=\nhowett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=\n"
  },
  {
    "path": "caddy/hotreload-skip.go",
    "content": "//go:build nowatcher || nomercure\n\npackage caddy\n\nimport (\n\t\"errors\"\n\n\t\"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile\"\n)\n\ntype hotReloadContext struct {\n}\n\nfunc (_ *FrankenPHPModule) configureHotReload(_ *FrankenPHPApp) error {\n\treturn nil\n}\n\nfunc (_ *FrankenPHPModule) unmarshalHotReload(d *caddyfile.Dispenser) error {\n\treturn errors.New(\"hot reload support disabled\")\n}\n"
  },
  {
    "path": "caddy/hotreload.go",
    "content": "//go:build !nowatcher && !nomercure\n\npackage caddy\n\nimport (\n\t\"bytes\"\n\t\"encoding/gob\"\n\t\"errors\"\n\t\"fmt\"\n\t\"hash/fnv\"\n\t\"net/url\"\n\n\t\"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile\"\n\t\"github.com/dunglas/frankenphp\"\n)\n\nconst defaultHotReloadPattern = \"./**/*.{css,env,gif,htm,html,jpg,jpeg,js,mjs,php,png,svg,twig,webp,xml,yaml,yml}\"\n\ntype hotReloadContext struct {\n\t// HotReload specifies files to watch for file changes to trigger hot reloads updates. Supports the glob syntax.\n\tHotReload *hotReloadConfig `json:\"hot_reload,omitempty\"`\n}\n\ntype hotReloadConfig struct {\n\tTopic string   `json:\"topic\"`\n\tWatch []string `json:\"watch\"`\n}\n\nfunc (f *FrankenPHPModule) configureHotReload(app *FrankenPHPApp) error {\n\tif f.HotReload == nil {\n\t\treturn nil\n\t}\n\n\tif f.mercureHub == nil {\n\t\treturn errors.New(\"unable to enable hot reloading: no Mercure hub configured\")\n\t}\n\n\tif len(f.HotReload.Watch) == 0 {\n\t\tf.HotReload.Watch = []string{defaultHotReloadPattern}\n\t}\n\n\tif f.HotReload.Topic == \"\" {\n\t\tuid, err := uniqueID(f)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tf.HotReload.Topic = \"https://frankenphp.dev/hot-reload/\" + uid\n\t}\n\n\tapp.opts = append(app.opts, frankenphp.WithHotReload(f.HotReload.Topic, f.mercureHub, f.HotReload.Watch))\n\tf.preparedEnv[\"FRANKENPHP_HOT_RELOAD\\x00\"] = \"/.well-known/mercure?topic=\" + url.QueryEscape(f.HotReload.Topic)\n\n\treturn nil\n}\n\nfunc (f *FrankenPHPModule) unmarshalHotReload(d *caddyfile.Dispenser) error {\n\tf.HotReload = &hotReloadConfig{\n\t\tWatch: d.RemainingArgs(),\n\t}\n\n\tfor d.NextBlock(1) {\n\t\tswitch v := d.Val(); v {\n\t\tcase \"topic\":\n\t\t\tif !d.NextArg() {\n\t\t\t\treturn d.ArgErr()\n\t\t\t}\n\n\t\t\tif f.HotReload == nil {\n\t\t\t\tf.HotReload = &hotReloadConfig{}\n\t\t\t}\n\n\t\t\tf.HotReload.Topic = d.Val()\n\n\t\tcase \"watch\":\n\t\t\tpatterns := d.RemainingArgs()\n\t\t\tif len(patterns) == 0 {\n\t\t\t\treturn d.ArgErr()\n\t\t\t}\n\n\t\t\tf.HotReload.Watch = append(f.HotReload.Watch, patterns...)\n\n\t\tdefault:\n\t\t\treturn wrongSubDirectiveError(\"hot_reload\", \"topic, watch\", v)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc uniqueID(s any) (string, error) {\n\tvar b bytes.Buffer\n\n\tif err := gob.NewEncoder(&b).Encode(s); err != nil {\n\t\treturn \"\", fmt.Errorf(\"unable to generate unique name: %w\", err)\n\t}\n\n\th := fnv.New64a()\n\tif _, err := h.Write(b.Bytes()); err != nil {\n\t\treturn \"\", fmt.Errorf(\"unable to generate unique name: %w\", err)\n\t}\n\n\treturn fmt.Sprintf(\"%016x\", h.Sum64()), nil\n}\n"
  },
  {
    "path": "caddy/hotreload_test.go",
    "content": "//go:build !nowatcher && !nomercure\n\npackage caddy_test\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"sync\"\n\t\"testing\"\n\n\t\"github.com/caddyserver/caddy/v2/caddytest\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestHotReload(t *testing.T) {\n\tconst topic = \"https://frankenphp.dev/hot-reload/test\"\n\n\tu := \"/.well-known/mercure?topic=\" + url.QueryEscape(topic)\n\n\ttmpDir := t.TempDir()\n\tindexFile := filepath.Join(tmpDir, \"index.php\")\n\n\ttester := caddytest.NewTester(t)\n\ttester.InitServer(`\n\t\t{\n\t\t\tdebug\n\t\t\tskip_install_trust\n\t\t\tadmin localhost:2999\n\t\t}\n\n\t\thttp://localhost:`+testPort+` {\n\t\t\tmercure {\n\t\t\t\ttransport local\n\t\t\t\tsubscriber_jwt TestKey \n\t\t\t\tanonymous\n\t\t\t}\n\n\t\t\tphp_server {\n\t\t\t\troot `+tmpDir+`\n\t\t\t\thot_reload {\n\t\t\t\t\ttopic `+topic+`\n\t\t\t\t\twatch `+tmpDir+`/*.php\n\t\t\t\t}\n\t\t\t}\n\t\t`, \"caddyfile\")\n\n\tvar connected, received sync.WaitGroup\n\n\tconnected.Add(1)\n\treceived.Go(func() {\n\t\tcx, cancel := context.WithCancel(t.Context())\n\t\treq, _ := http.NewRequest(http.MethodGet, \"http://localhost:\"+testPort+u, nil)\n\t\treq = req.WithContext(cx)\n\t\tresp := tester.AssertResponseCode(req, http.StatusOK)\n\n\t\tconnected.Done()\n\n\t\tvar receivedBody strings.Builder\n\n\t\tbuf := make([]byte, 1024)\n\t\tfor {\n\t\t\t_, err := resp.Body.Read(buf)\n\t\t\trequire.NoError(t, err)\n\n\t\t\treceivedBody.Write(buf)\n\n\t\t\tif strings.Contains(receivedBody.String(), \"index.php\") {\n\t\t\t\tcancel()\n\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\trequire.NoError(t, resp.Body.Close())\n\t})\n\n\tconnected.Wait()\n\n\trequire.NoError(t, os.WriteFile(indexFile, []byte(\"<?=$_SERVER['FRANKENPHP_HOT_RELOAD'];\"), 0644))\n\n\treceived.Wait()\n\n\ttester.AssertGetResponse(\"http://localhost:\"+testPort+\"/index.php\", http.StatusOK, u)\n}\n"
  },
  {
    "path": "caddy/mercure-skip.go",
    "content": "//go:build nomercure\n\npackage caddy\n\nimport (\n\t\"github.com/caddyserver/caddy/v2\"\n\t\"github.com/caddyserver/caddy/v2/modules/caddyhttp\"\n)\n\ntype mercureContext struct {\n}\n\nfunc (f *FrankenPHPModule) assignMercureHub(_ caddy.Context) {\n}\n\nfunc createMercureRoute() (caddyhttp.Route, error) {\n\treturn caddyhttp.Route{}, nil\n}\n"
  },
  {
    "path": "caddy/mercure.go",
    "content": "//go:build !nomercure\n\npackage caddy\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"github.com/caddyserver/caddy/v2\"\n\t\"github.com/caddyserver/caddy/v2/caddyconfig\"\n\t\"github.com/caddyserver/caddy/v2/modules/caddyhttp\"\n\t\"github.com/dunglas/frankenphp\"\n\t\"github.com/dunglas/mercure\"\n\tmercureCaddy \"github.com/dunglas/mercure/caddy\"\n\t\"os\"\n)\n\nfunc init() {\n\tmercureCaddy.AllowNoPublish = true\n}\n\ntype mercureContext struct {\n\tmercureHub *mercure.Hub\n}\n\nfunc (f *FrankenPHPModule) assignMercureHub(ctx caddy.Context) {\n\tif f.mercureHub = mercureCaddy.FindHub(ctx.Modules()); f.mercureHub == nil {\n\t\treturn\n\t}\n\n\tf.requestOptions = append(f.requestOptions, frankenphp.WithMercureHub(f.mercureHub))\n\n\tfor i, wc := range f.Workers {\n\t\twc.mercureHub = f.mercureHub\n\t\twc.options = append(wc.options, frankenphp.WithWorkerMercureHub(wc.mercureHub))\n\n\t\tf.Workers[i] = wc\n\t}\n}\n\nfunc createMercureRoute() (caddyhttp.Route, error) {\n\tmercurePublisherJwtKey := os.Getenv(\"MERCURE_PUBLISHER_JWT_KEY\")\n\tif mercurePublisherJwtKey == \"\" {\n\t\treturn caddyhttp.Route{}, errors.New(`The \"MERCURE_PUBLISHER_JWT_KEY\" environment variable must be set to use the Mercure.rocks hub`)\n\t}\n\n\tmercureSubscriberJwtKey := os.Getenv(\"MERCURE_SUBSCRIBER_JWT_KEY\")\n\tif mercureSubscriberJwtKey == \"\" {\n\t\treturn caddyhttp.Route{}, errors.New(`The \"MERCURE_SUBSCRIBER_JWT_KEY\" environment variable must be set to use the Mercure.rocks hub`)\n\t}\n\n\tmercureRoute := caddyhttp.Route{\n\t\tHandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(\n\t\t\tmercureCaddy.Mercure{\n\t\t\t\tPublisherJWT: mercureCaddy.JWTConfig{\n\t\t\t\t\tAlg: os.Getenv(\"MERCURE_PUBLISHER_JWT_ALG\"),\n\t\t\t\t\tKey: mercurePublisherJwtKey,\n\t\t\t\t},\n\t\t\t\tSubscriberJWT: mercureCaddy.JWTConfig{\n\t\t\t\t\tAlg: os.Getenv(\"MERCURE_SUBSCRIBER_JWT_ALG\"),\n\t\t\t\t\tKey: mercureSubscriberJwtKey,\n\t\t\t\t},\n\t\t\t},\n\t\t\t\"handler\",\n\t\t\t\"mercure\",\n\t\t\tnil,\n\t\t),\n\t\t},\n\t}\n\n\treturn mercureRoute, nil\n}\n"
  },
  {
    "path": "caddy/module.go",
    "content": "package caddy\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/caddyserver/caddy/v2\"\n\t\"github.com/caddyserver/caddy/v2/caddyconfig\"\n\t\"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile\"\n\t\"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile\"\n\t\"github.com/caddyserver/caddy/v2/modules/caddyhttp\"\n\t\"github.com/caddyserver/caddy/v2/modules/caddyhttp/fileserver\"\n\t\"github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite\"\n\t\"github.com/dunglas/frankenphp\"\n\t\"github.com/dunglas/frankenphp/internal/fastabs\"\n)\n\n// FrankenPHPModule represents the \"php_server\" and \"php\" directives in the Caddyfile\n// they are responsible for forwarding requests to FrankenPHP via \"ServeHTTP\"\n//\n//\texample.com {\n//\t\tphp_server {\n//\t\t\troot /var/www/html\n//\t\t}\n//\t}\ntype FrankenPHPModule struct {\n\tmercureContext\n\thotReloadContext\n\n\t// Root sets the root folder to the site. Default: `root` directive, or the path of the public directory of the embed app it exists.\n\tRoot string `json:\"root,omitempty\"`\n\t// SplitPath sets the substrings for splitting the URI into two parts. The first matching substring will be used to split the \"path info\" from the path. The first piece is suffixed with the matching substring and will be assumed as the actual resource (CGI script) name. The second piece will be set to PATH_INFO for the CGI script to use. Default: `.php`.\n\tSplitPath []string `json:\"split_path,omitempty\"`\n\t// ResolveRootSymlink enables resolving the `root` directory to its actual value by evaluating a symbolic link, if one exists.\n\tResolveRootSymlink *bool `json:\"resolve_root_symlink,omitempty\"`\n\t// Env sets an extra environment variable to the given value. Can be specified more than once for multiple environment variables.\n\tEnv map[string]string `json:\"env,omitempty\"`\n\t// Workers configures the worker scripts to start.\n\tWorkers []workerConfig `json:\"workers,omitempty\"`\n\n\tresolvedDocumentRoot        string\n\tpreparedEnv                 frankenphp.PreparedEnv\n\tpreparedEnvNeedsReplacement bool\n\tlogger                      *slog.Logger\n\trequestOptions              []frankenphp.RequestOption\n}\n\n// CaddyModule returns the Caddy module information.\nfunc (FrankenPHPModule) CaddyModule() caddy.ModuleInfo {\n\treturn caddy.ModuleInfo{\n\t\tID:  \"http.handlers.php\",\n\t\tNew: func() caddy.Module { return new(FrankenPHPModule) },\n\t}\n}\n\n// Provision sets up the module.\nfunc (f *FrankenPHPModule) Provision(ctx caddy.Context) error {\n\tf.logger = ctx.Slogger()\n\tapp, err := ctx.App(\"frankenphp\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tfapp, ok := app.(*FrankenPHPApp)\n\tif !ok {\n\t\treturn fmt.Errorf(`expected ctx.App(\"frankenphp\") to return *FrankenPHPApp, got %T`, app)\n\t}\n\tif fapp == nil {\n\t\treturn fmt.Errorf(`expected ctx.App(\"frankenphp\") to return *FrankenPHPApp, got nil`)\n\t}\n\n\tf.assignMercureHub(ctx)\n\n\tloggerOpt := frankenphp.WithRequestLogger(f.logger)\n\tfor i, wc := range f.Workers {\n\t\t// make the file path absolute from the public directory\n\t\t// this can only be done if the root is defined inside php_server\n\t\tif !filepath.IsAbs(wc.FileName) && f.Root != \"\" {\n\t\t\twc.FileName = filepath.Join(f.Root, wc.FileName)\n\t\t}\n\n\t\t// Inherit environment variables from the parent php_server directive\n\t\tif f.Env != nil {\n\t\t\twc.inheritEnv(f.Env)\n\t\t}\n\n\t\twc.requestOptions = append(wc.requestOptions, loggerOpt)\n\t\tf.Workers[i] = wc\n\t}\n\n\tworkers, err := fapp.addModuleWorkers(f.Workers...)\n\tif err != nil {\n\t\treturn err\n\t}\n\tf.Workers = workers\n\n\tif f.Root == \"\" {\n\t\tif frankenphp.EmbeddedAppPath == \"\" {\n\t\t\tf.Root = \"{http.vars.root}\"\n\t\t} else {\n\t\t\tf.Root = filepath.Join(frankenphp.EmbeddedAppPath, defaultDocumentRoot)\n\n\t\t\tf.ResolveRootSymlink = new(false)\n\t\t}\n\t} else if frankenphp.EmbeddedAppPath != \"\" && filepath.IsLocal(f.Root) {\n\t\tf.Root = filepath.Join(frankenphp.EmbeddedAppPath, f.Root)\n\t}\n\n\tif len(f.SplitPath) == 0 {\n\t\tf.SplitPath = []string{\".php\"}\n\t}\n\n\tif opt, err := frankenphp.WithRequestSplitPath(f.SplitPath); err == nil {\n\t\tf.requestOptions = append(f.requestOptions, opt)\n\t} else {\n\t\tf.requestOptions = append(f.requestOptions, opt)\n\t}\n\n\tif f.ResolveRootSymlink == nil {\n\t\tf.ResolveRootSymlink = new(true)\n\t}\n\n\t// Always pre-compute absolute file names for fallback matching\n\tfor i := range f.Workers {\n\t\tf.Workers[i].absFileName, _ = fastabs.FastAbs(f.Workers[i].FileName)\n\t}\n\n\tif !needReplacement(f.Root) {\n\t\troot, err := fastabs.FastAbs(f.Root)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"unable to make the root path absolute: %w\", err)\n\t\t}\n\t\tf.resolvedDocumentRoot = root\n\n\t\tif *f.ResolveRootSymlink {\n\t\t\troot, err := filepath.EvalSymlinks(root)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"unable to resolve root symlink: %w\", err)\n\t\t\t}\n\n\t\t\tf.resolvedDocumentRoot = root\n\n\t\t\t// Resolve symlinks in worker file paths\n\t\t\tfor i, wc := range f.Workers {\n\t\t\t\tif filepath.IsAbs(wc.FileName) {\n\t\t\t\t\tresolvedPath, _ := filepath.EvalSymlinks(wc.FileName)\n\t\t\t\t\tf.Workers[i].FileName = resolvedPath\n\t\t\t\t\tf.Workers[i].absFileName = resolvedPath\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Pre-compute relative match paths for all workers (requires resolved document root)\n\t\tdocRootWithSep := f.resolvedDocumentRoot + string(filepath.Separator)\n\t\tfor i := range f.Workers {\n\t\t\tif strings.HasPrefix(f.Workers[i].absFileName, docRootWithSep) {\n\t\t\t\tf.Workers[i].matchRelPath = filepath.ToSlash(f.Workers[i].absFileName[len(f.resolvedDocumentRoot):])\n\t\t\t}\n\t\t}\n\n\t\tf.requestOptions = append(f.requestOptions, frankenphp.WithRequestResolvedDocumentRoot(f.resolvedDocumentRoot))\n\t}\n\n\tif f.preparedEnv == nil {\n\t\tf.preparedEnv = frankenphp.PrepareEnv(f.Env)\n\n\t\tfor _, e := range f.preparedEnv {\n\t\t\tif needReplacement(e) {\n\t\t\t\tf.preparedEnvNeedsReplacement = true\n\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\tif !f.preparedEnvNeedsReplacement {\n\t\tf.requestOptions = append(f.requestOptions, frankenphp.WithRequestPreparedEnv(f.preparedEnv))\n\t}\n\n\tif err := f.configureHotReload(fapp); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// needReplacement checks if a string contains placeholders.\nfunc needReplacement(s string) bool {\n\treturn strings.ContainsAny(s, \"{}\")\n}\n\n// ServeHTTP implements caddyhttp.MiddlewareHandler.\nfunc (f *FrankenPHPModule) ServeHTTP(w http.ResponseWriter, r *http.Request, _ caddyhttp.Handler) error {\n\tctx := r.Context()\n\trepl := ctx.Value(caddy.ReplacerCtxKey).(*caddy.Replacer)\n\n\tdocumentRoot := f.resolvedDocumentRoot\n\n\topts := make([]frankenphp.RequestOption, 0, len(f.requestOptions)+4)\n\topts = append(opts, f.requestOptions...)\n\n\tif documentRoot == \"\" {\n\t\tdocumentRoot = repl.ReplaceKnown(f.Root, \"\")\n\t\tif documentRoot == \"\" && frankenphp.EmbeddedAppPath != \"\" {\n\t\t\tdocumentRoot = frankenphp.EmbeddedAppPath\n\t\t}\n\n\t\t// If we do not have a resolved document root, then we cannot resolve the symlink of our cwd because it may\n\t\t// resolve to a different directory than the one we are currently in.\n\t\t// This is especially important if there are workers running.\n\t\topts = append(opts, frankenphp.WithRequestDocumentRoot(documentRoot, false))\n\t}\n\n\tif f.preparedEnvNeedsReplacement {\n\t\tenv := make(frankenphp.PreparedEnv, len(f.Env))\n\t\tfor k, v := range f.preparedEnv {\n\t\t\tenv[k] = repl.ReplaceKnown(v, \"\")\n\t\t}\n\n\t\topts = append(opts, frankenphp.WithRequestPreparedEnv(env))\n\t}\n\n\tworkerName := \"\"\n\tfor _, w := range f.Workers {\n\t\tif w.matchesPath(r, documentRoot) {\n\t\t\tworkerName = w.Name\n\t\t\tbreak\n\t\t}\n\t}\n\n\tfr, err := frankenphp.NewRequestWithContext(\n\t\tr,\n\t\tappend(\n\t\t\topts,\n\t\t\tfrankenphp.WithOriginalRequest(new(ctx.Value(caddyhttp.OriginalRequestCtxKey).(http.Request))),\n\t\t\tfrankenphp.WithWorkerName(workerName),\n\t\t)...,\n\t)\n\n\tif err != nil {\n\t\treturn caddyhttp.Error(http.StatusInternalServerError, err)\n\t}\n\n\tif err = frankenphp.ServeHTTP(w, fr); err != nil && !errors.As(err, &frankenphp.ErrRejected{}) {\n\t\treturn caddyhttp.Error(http.StatusInternalServerError, err)\n\t}\n\n\treturn nil\n}\n\n// UnmarshalCaddyfile implements caddyfile.Unmarshaler.\nfunc (f *FrankenPHPModule) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {\n\tfor d.Next() {\n\t\tfor d.NextBlock(0) {\n\t\t\tswitch d.Val() {\n\t\t\tcase \"root\":\n\t\t\t\tif !d.NextArg() {\n\t\t\t\t\treturn d.ArgErr()\n\t\t\t\t}\n\t\t\t\tf.Root = d.Val()\n\n\t\t\tcase \"split\":\n\t\t\t\tf.SplitPath = d.RemainingArgs()\n\t\t\t\tif len(f.SplitPath) == 0 {\n\t\t\t\t\treturn d.ArgErr()\n\t\t\t\t}\n\n\t\t\tcase \"env\":\n\t\t\t\targs := d.RemainingArgs()\n\t\t\t\tif len(args) != 2 {\n\t\t\t\t\treturn d.ArgErr()\n\t\t\t\t}\n\t\t\t\tif f.Env == nil {\n\t\t\t\t\tf.Env = make(map[string]string)\n\t\t\t\t\tf.preparedEnv = make(frankenphp.PreparedEnv)\n\t\t\t\t}\n\t\t\t\tf.Env[args[0]] = args[1]\n\t\t\t\tf.preparedEnv[args[0]+\"\\x00\"] = args[1]\n\n\t\t\tcase \"resolve_root_symlink\":\n\t\t\t\tif !d.NextArg() {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tv, err := strconv.ParseBool(d.Val())\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tif d.NextArg() {\n\t\t\t\t\treturn d.ArgErr()\n\t\t\t\t}\n\t\t\t\tf.ResolveRootSymlink = &v\n\n\t\t\tcase \"worker\":\n\t\t\t\twc, err := unmarshalWorker(d)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tf.Workers = append(f.Workers, wc)\n\n\t\t\tcase \"hot_reload\":\n\t\t\t\tif err := f.unmarshalHotReload(d); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\tdefault:\n\t\t\t\treturn wrongSubDirectiveError(\"php or php_server\", \"hot_reload, name, root, split, env, resolve_root_symlink, worker\", d.Val())\n\t\t\t}\n\t\t}\n\t}\n\n\t// Check if a worker with this filename already exists in this module\n\tfileNames := make(map[string]struct{}, len(f.Workers))\n\tfor _, w := range f.Workers {\n\t\tif _, ok := fileNames[w.FileName]; ok {\n\t\t\treturn fmt.Errorf(`workers in a single \"php\" or \"php_server\" block must not have duplicate filenames: %q`, w.FileName)\n\t\t}\n\n\t\tif len(w.MatchPath) == 0 {\n\t\t\tfileNames[w.FileName] = struct{}{}\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// parseCaddyfile unmarshals tokens from h into a new Middleware.\nfunc parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {\n\tm := &FrankenPHPModule{}\n\terr := m.UnmarshalCaddyfile(h.Dispenser)\n\n\treturn m, err\n}\n\n// parsePhpServer parses the php_server directive, which has a similar syntax\n// to the php_fastcgi directive. A line such as this:\n//\n//\tphp_server\n//\n// is equivalent to a route consisting of:\n//\n//\t\t# Add trailing slash for directory requests\n//\t\t@canonicalPath {\n//\t\t    file {path}/index.php\n//\t\t    not path */\n//\t\t}\n//\t\tredir @canonicalPath {path}/ 308\n//\n//\t\t# If the requested file does not exist, try index files\n//\t\t@indexFiles file {\n//\t\t    try_files {path} {path}/index.php index.php\n//\t\t    split_path .php\n//\t\t}\n//\t\trewrite @indexFiles {http.matchers.file.relative}\n//\n//\t\t# FrankenPHP!\n//\t\t@phpFiles path *.php\n//\t \tphp @phpFiles\n//\t\tfile_server\n//\n// parsePhpServer is freely inspired from the php_fastgci directive of the Caddy server (Apache License 2.0, Matthew Holt and The Caddy Authors)\nfunc parsePhpServer(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error) {\n\tif !h.Next() {\n\t\treturn nil, h.ArgErr()\n\t}\n\n\t// set up FrankenPHP\n\tphpsrv := FrankenPHPModule{}\n\n\t// set up file server\n\tfsrv := fileserver.FileServer{}\n\tdisableFsrv := false\n\n\t// set up the set of file extensions allowed to execute PHP code\n\textensions := []string{\".php\"}\n\n\t// set the default index file for the try_files rewrites\n\tindexFile := \"index.php\"\n\n\t// set up for explicitly overriding try_files\n\tvar tryFiles []string\n\n\t// if the user specified a matcher token, use that\n\t// matcher in a route that wraps both of our routes;\n\t// either way, strip the matcher token and pass\n\t// the remaining tokens to the unmarshaler so that\n\t// we can gain the rest of the directive syntax\n\tuserMatcherSet, err := h.ExtractMatcherSet()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// make a new dispenser from the remaining tokens so that we\n\t// can reset the dispenser back to this point for the\n\t// php unmarshaler to read from it as well\n\tdispenser := h.NewFromNextSegment()\n\n\t// read the subdirectives that we allow as overrides to\n\t// the php_server shortcut\n\t// NOTE: we delete the tokens as we go so that the php\n\t// unmarshal doesn't see these subdirectives which it cannot handle\n\tfor dispenser.Next() {\n\t\tfor dispenser.NextBlock(0) {\n\t\t\t// ignore any sub-subdirectives that might\n\t\t\t// have the same name somewhere within\n\t\t\t// the php passthrough tokens\n\t\t\tif dispenser.Nesting() != 1 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// parse the php_server subdirectives\n\t\t\tswitch dispenser.Val() {\n\t\t\tcase \"root\":\n\t\t\t\tif !dispenser.NextArg() {\n\t\t\t\t\treturn nil, dispenser.ArgErr()\n\t\t\t\t}\n\t\t\t\tphpsrv.Root = dispenser.Val()\n\t\t\t\tfsrv.Root = phpsrv.Root\n\t\t\t\tdispenser.DeleteN(2)\n\n\t\t\tcase \"split\":\n\t\t\t\textensions = dispenser.RemainingArgs()\n\t\t\t\tdispenser.DeleteN(len(extensions) + 1)\n\t\t\t\tif len(extensions) == 0 {\n\t\t\t\t\treturn nil, dispenser.ArgErr()\n\t\t\t\t}\n\n\t\t\tcase \"index\":\n\t\t\t\targs := dispenser.RemainingArgs()\n\t\t\t\tdispenser.DeleteN(len(args) + 1)\n\t\t\t\tif len(args) != 1 {\n\t\t\t\t\treturn nil, dispenser.ArgErr()\n\t\t\t\t}\n\t\t\t\tindexFile = args[0]\n\n\t\t\tcase \"try_files\":\n\t\t\t\targs := dispenser.RemainingArgs()\n\t\t\t\tdispenser.DeleteN(len(args) + 1)\n\t\t\t\tif len(args) < 1 {\n\t\t\t\t\treturn nil, dispenser.ArgErr()\n\t\t\t\t}\n\t\t\t\ttryFiles = args\n\n\t\t\tcase \"file_server\":\n\t\t\t\targs := dispenser.RemainingArgs()\n\t\t\t\tdispenser.DeleteN(len(args) + 1)\n\t\t\t\tif len(args) < 1 || args[0] != \"off\" {\n\t\t\t\t\treturn nil, dispenser.ArgErr()\n\t\t\t\t}\n\t\t\t\tdisableFsrv = true\n\t\t\t}\n\t\t}\n\t}\n\n\t// reset the dispenser after we're done so that the frankenphp\n\t// unmarshaler can read it from the start\n\tdispenser.Reset()\n\n\t// the rest of the config is specified by the user\n\t// using the php directive syntax\n\tdispenser.Next() // consume the directive name\n\tif err := phpsrv.UnmarshalCaddyfile(dispenser); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif frankenphp.EmbeddedAppPath != \"\" {\n\t\tif phpsrv.Root == \"\" {\n\t\t\tphpsrv.Root = filepath.Join(frankenphp.EmbeddedAppPath, defaultDocumentRoot)\n\t\t\tfsrv.Root = phpsrv.Root\n\t\t\tphpsrv.ResolveRootSymlink = new(false)\n\t\t} else if filepath.IsLocal(fsrv.Root) {\n\t\t\tphpsrv.Root = filepath.Join(frankenphp.EmbeddedAppPath, phpsrv.Root)\n\t\t\tfsrv.Root = phpsrv.Root\n\t\t}\n\t}\n\n\t// set up a route list that we'll append to\n\troutes := caddyhttp.RouteList{}\n\n\t// prepend routes from the 'worker match *' directives\n\troutes = prependWorkerRoutes(routes, h, phpsrv, fsrv, disableFsrv)\n\n\t// set the list of allowed path segments on which to split\n\tphpsrv.SplitPath = extensions\n\n\t// if the index is turned off, we skip the redirect and try_files\n\tif indexFile != \"off\" {\n\t\tdirRedir := false\n\t\tdirIndex := \"{http.request.uri.path}/\" + indexFile\n\t\t// On Windows, first_exist_fallback doesn't work correctly because\n\t\t// glob is skipped and patterns are returned as-is without checking existence.\n\t\t// Use first_exist instead to ensure all files are checked.\n\t\ttryPolicy := \"first_exist_fallback\"\n\t\tif runtime.GOOS == \"windows\" {\n\t\t\ttryPolicy = \"first_exist\"\n\t\t}\n\n\t\t// if tryFiles wasn't overridden, use a reasonable default\n\t\tif len(tryFiles) == 0 {\n\t\t\tif disableFsrv {\n\t\t\t\ttryFiles = []string{dirIndex, indexFile}\n\t\t\t} else {\n\t\t\t\ttryFiles = []string{\"{http.request.uri.path}\", dirIndex, indexFile}\n\t\t\t}\n\n\t\t\tdirRedir = true\n\t\t} else {\n\t\t\tif !strings.HasSuffix(tryFiles[len(tryFiles)-1], \".php\") {\n\t\t\t\t// use first_exist strategy if the last file is not a PHP file\n\t\t\t\ttryPolicy = \"\"\n\t\t\t}\n\n\t\t\tif slices.Contains(tryFiles, dirIndex) {\n\t\t\t\tdirRedir = true\n\t\t\t}\n\t\t}\n\n\t\t// route to redirect to canonical path if index PHP file\n\t\tif dirRedir {\n\t\t\tredirMatcherSet := caddy.ModuleMap{\n\t\t\t\t\"file\": h.JSON(fileserver.MatchFile{\n\t\t\t\t\tTryFiles: []string{dirIndex},\n\t\t\t\t\tRoot:     phpsrv.Root,\n\t\t\t\t}),\n\t\t\t\t\"not\": h.JSON(caddyhttp.MatchNot{\n\t\t\t\t\tMatcherSetsRaw: []caddy.ModuleMap{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"path\": h.JSON(caddyhttp.MatchPath{\"*/\"}),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}),\n\t\t\t}\n\t\t\tredirHandler := caddyhttp.StaticResponse{\n\t\t\t\tStatusCode: caddyhttp.WeakString(strconv.Itoa(http.StatusPermanentRedirect)),\n\t\t\t\tHeaders:    http.Header{\"Location\": []string{\"{http.request.orig_uri.path}/\"}},\n\t\t\t}\n\t\t\tredirRoute := caddyhttp.Route{\n\t\t\t\tMatcherSetsRaw: []caddy.ModuleMap{redirMatcherSet},\n\t\t\t\tHandlersRaw:    []json.RawMessage{caddyconfig.JSONModuleObject(redirHandler, \"handler\", \"static_response\", nil)},\n\t\t\t}\n\n\t\t\troutes = append(routes, redirRoute)\n\t\t}\n\n\t\t// route to rewrite to PHP index file\n\t\trewriteMatcherSet := caddy.ModuleMap{\n\t\t\t\"file\": h.JSON(fileserver.MatchFile{\n\t\t\t\tTryFiles:  tryFiles,\n\t\t\t\tTryPolicy: tryPolicy,\n\t\t\t\tSplitPath: extensions,\n\t\t\t\tRoot:      phpsrv.Root,\n\t\t\t}),\n\t\t}\n\t\trewriteHandler := rewrite.Rewrite{\n\t\t\tURI: \"{http.matchers.file.relative}\",\n\t\t}\n\t\trewriteRoute := caddyhttp.Route{\n\t\t\tMatcherSetsRaw: []caddy.ModuleMap{rewriteMatcherSet},\n\t\t\tHandlersRaw:    []json.RawMessage{caddyconfig.JSONModuleObject(rewriteHandler, \"handler\", \"rewrite\", nil)},\n\t\t}\n\n\t\troutes = append(routes, rewriteRoute)\n\t}\n\n\t// route to actually pass requests to PHP files;\n\t// match only requests that are for PHP files\n\tvar pathList []string\n\tfor _, ext := range extensions {\n\t\tpathList = append(pathList, \"*\"+ext)\n\t}\n\tphpMatcherSet := caddy.ModuleMap{\n\t\t\"path\": h.JSON(pathList),\n\t}\n\n\t// create the PHP route which is\n\t// conditional on matching PHP files\n\tphpRoute := caddyhttp.Route{\n\t\tMatcherSetsRaw: []caddy.ModuleMap{phpMatcherSet},\n\t\tHandlersRaw:    []json.RawMessage{caddyconfig.JSONModuleObject(phpsrv, \"handler\", \"php\", nil)},\n\t}\n\troutes = append(routes, phpRoute)\n\n\t// create the file server route\n\tif !disableFsrv {\n\t\tfileRoute := caddyhttp.Route{\n\t\t\tMatcherSetsRaw: []caddy.ModuleMap{},\n\t\t\tHandlersRaw:    []json.RawMessage{caddyconfig.JSONModuleObject(fsrv, \"handler\", \"file_server\", nil)},\n\t\t}\n\t\troutes = append(routes, fileRoute)\n\t}\n\n\tsubroute := caddyhttp.Subroute{\n\t\tRoutes: routes,\n\t}\n\n\t// the user's matcher is a prerequisite for ours, so\n\t// wrap ours in a subroute and return that\n\tif userMatcherSet != nil {\n\t\treturn []httpcaddyfile.ConfigValue{\n\t\t\t{\n\t\t\t\tClass: \"route\",\n\t\t\t\tValue: caddyhttp.Route{\n\t\t\t\t\tMatcherSetsRaw: []caddy.ModuleMap{userMatcherSet},\n\t\t\t\t\tHandlersRaw:    []json.RawMessage{caddyconfig.JSONModuleObject(subroute, \"handler\", \"subroute\", nil)},\n\t\t\t\t},\n\t\t\t},\n\t\t}, nil\n\t}\n\n\t// otherwise, return the literal subroute instead of\n\t// individual routes, to ensure they stay together and\n\t// are treated as a single unit, without necessarily\n\t// creating an actual subroute in the output\n\treturn []httpcaddyfile.ConfigValue{\n\t\t{\n\t\t\tClass: \"route\",\n\t\t\tValue: subroute,\n\t\t},\n\t}, nil\n}\n\n// workers can also match a path without being in the public directory\n// in this case we need to prepend the worker routes to the existing routes\nfunc prependWorkerRoutes(routes caddyhttp.RouteList, h httpcaddyfile.Helper, f FrankenPHPModule, fsrv caddy.Module, disableFsrv bool) caddyhttp.RouteList {\n\tvar allWorkerMatches caddyhttp.MatchPath\n\tfor _, w := range f.Workers {\n\t\tfor _, path := range w.MatchPath {\n\t\t\tallWorkerMatches = append(allWorkerMatches, path)\n\t\t}\n\t}\n\n\tif len(allWorkerMatches) == 0 {\n\t\treturn routes\n\t}\n\n\t// if there are match patterns, we need to check for files beforehand\n\tif !disableFsrv {\n\t\troutes = append(routes, caddyhttp.Route{\n\t\t\tMatcherSetsRaw: []caddy.ModuleMap{\n\t\t\t\t{\n\t\t\t\t\t\"file\": h.JSON(fileserver.MatchFile{\n\t\t\t\t\t\tTryFiles: []string{\"{http.request.uri.path}\"},\n\t\t\t\t\t\tRoot:     f.Root,\n\t\t\t\t\t}),\n\t\t\t\t\t\"not\": h.JSON(caddyhttp.MatchNot{\n\t\t\t\t\t\tMatcherSetsRaw: []caddy.ModuleMap{\n\t\t\t\t\t\t\t{\"path\": h.JSON(caddyhttp.MatchPath{\"*.php\"})},\n\t\t\t\t\t\t},\n\t\t\t\t\t}),\n\t\t\t\t},\n\t\t\t},\n\t\t\tHandlersRaw: []json.RawMessage{\n\t\t\t\tcaddyconfig.JSONModuleObject(fsrv, \"handler\", \"file_server\", nil),\n\t\t\t},\n\t\t})\n\t}\n\n\t// forward matching routes to the PHP handler\n\troutes = append(routes, caddyhttp.Route{\n\t\tMatcherSetsRaw: []caddy.ModuleMap{\n\t\t\t{\"path\": h.JSON(allWorkerMatches)},\n\t\t},\n\t\tHandlersRaw: []json.RawMessage{\n\t\t\tcaddyconfig.JSONModuleObject(f, \"handler\", \"php\", nil),\n\t\t},\n\t})\n\n\treturn routes\n}\n\n// Interface guards\nvar (\n\t_ caddy.Provisioner           = (*FrankenPHPModule)(nil)\n\t_ caddyhttp.MiddlewareHandler = (*FrankenPHPModule)(nil)\n\t_ caddyfile.Unmarshaler       = (*FrankenPHPModule)(nil)\n)\n"
  },
  {
    "path": "caddy/module_test.go",
    "content": "package caddy_test\n\nimport (\n\t\"net/http\"\n\t\"os\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"github.com/caddyserver/caddy/v2/caddytest\"\n)\n\nfunc TestRootBehavesTheSameOutsideAndInsidePhpServer(t *testing.T) {\n\ttester := caddytest.NewTester(t)\n\ttestPortNum, _ := strconv.Atoi(testPort)\n\ttestPortTwo := strconv.Itoa(testPortNum + 1)\n\texpectedFileResponse, _ := os.ReadFile(\"../testdata/files/static.txt\")\n\thostWithRootOutside := \"http://localhost:\" + testPort\n\thostWithRootInside := \"http://localhost:\" + testPortTwo\n\ttester.InitServer(`\n\t\t{\n\t\t\tskip_install_trust\n\t\t\tadmin localhost:2999\n\t\t}\n\n\t\t`+hostWithRootOutside+` {\n\t\t\troot ../testdata\n\t\t\tphp_server\n\t\t}\n\n\t\t`+hostWithRootInside+` {\n\t\t\tphp_server {\n\t\t\t\troot ../testdata\n\t\t\t}\n\t\t}\n\t\t`, \"caddyfile\")\n\n\t// serve a static file\n\ttester.AssertGetResponse(hostWithRootOutside+\"/files/static.txt\", http.StatusOK, string(expectedFileResponse))\n\ttester.AssertGetResponse(hostWithRootInside+\"/files/static.txt\", http.StatusOK, string(expectedFileResponse))\n\n\t// serve a php file\n\ttester.AssertGetResponse(hostWithRootOutside+\"/hello.php\", http.StatusOK, \"Hello from PHP\")\n\ttester.AssertGetResponse(hostWithRootInside+\"/hello.php\", http.StatusOK, \"Hello from PHP\")\n\n\t// fallback to index.php\n\ttester.AssertGetResponse(hostWithRootOutside+\"/some-path\", http.StatusOK, \"I am by birth a Genevese (i not set)\")\n\ttester.AssertGetResponse(hostWithRootInside+\"/some-path\", http.StatusOK, \"I am by birth a Genevese (i not set)\")\n\n\t// fallback to directory index ('dirIndex' in module.go)\n\ttester.AssertGetResponse(hostWithRootOutside+\"/dirindex/\", http.StatusOK, \"Hello from directory index.php\")\n\ttester.AssertGetResponse(hostWithRootInside+\"/dirindex/\", http.StatusOK, \"Hello from directory index.php\")\n}\n"
  },
  {
    "path": "caddy/php-cli.go",
    "content": "package caddy\n\nimport (\n\t\"errors\"\n\t\"os\"\n\t\"path/filepath\"\n\n\tcaddycmd \"github.com/caddyserver/caddy/v2/cmd\"\n\t\"github.com/dunglas/frankenphp\"\n\n\t\"github.com/spf13/cobra\"\n)\n\nfunc init() {\n\tcaddycmd.RegisterCommand(caddycmd.Command{\n\t\tName:  \"php-cli\",\n\t\tUsage: \"script.php [args ...]\",\n\t\tShort: \"Runs a PHP command\",\n\t\tLong: `\nExecutes a PHP script similarly to the CLI SAPI.`,\n\t\tCobraFunc: func(cmd *cobra.Command) {\n\t\t\tcmd.DisableFlagParsing = true\n\t\t\tcmd.RunE = caddycmd.WrapCommandFuncForCobra(cmdPHPCLI)\n\t\t},\n\t})\n}\n\nfunc cmdPHPCLI(fs caddycmd.Flags) (int, error) {\n\targs := os.Args[2:]\n\tif len(args) < 1 {\n\t\treturn 1, errors.New(\"the path to the PHP script is required\")\n\t}\n\n\tif frankenphp.EmbeddedAppPath != \"\" {\n\t\tif _, err := os.Stat(args[0]); err != nil {\n\t\t\targs[0] = filepath.Join(frankenphp.EmbeddedAppPath, args[0])\n\t\t}\n\t}\n\n\tvar status int\n\tif len(args) >= 2 && args[0] == \"-r\" {\n\t\tstatus = frankenphp.ExecutePHPCode(args[1])\n\t} else {\n\t\tstatus = frankenphp.ExecuteScriptCLI(args[0], args)\n\t}\n\n\tos.Exit(status)\n\n\treturn status, nil\n}\n"
  },
  {
    "path": "caddy/php-server.go",
    "content": "package caddy\n\nimport (\n\t\"encoding/json\"\n\t\"log\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/caddyserver/caddy/v2\"\n\t\"github.com/caddyserver/caddy/v2/caddyconfig\"\n\tcaddycmd \"github.com/caddyserver/caddy/v2/cmd\"\n\t\"github.com/caddyserver/caddy/v2/modules/caddyhttp\"\n\t\"github.com/caddyserver/caddy/v2/modules/caddyhttp/encode\"\n\t\"github.com/caddyserver/caddy/v2/modules/caddyhttp/fileserver\"\n\t\"github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite\"\n\t\"github.com/caddyserver/certmagic\"\n\t\"github.com/dunglas/frankenphp\"\n\n\t\"github.com/spf13/cobra\"\n)\n\nfunc init() {\n\tcaddycmd.RegisterCommand(caddycmd.Command{\n\t\tName:  \"php-server\",\n\t\tUsage: \"[--domain=<example.com>] [--root=<path>] [--listen=<addr>] [--worker=/path/to/worker.php<,nb-workers>] [--watch[=<glob-pattern>]]... [--access-log] [--debug] [--no-compress] [--mercure]\",\n\t\tShort: \"Spins up a production-ready PHP server\",\n\t\tLong: `\nA simple but production-ready PHP server. Useful for quick deployments,\ndemos, and development.\n\nThe listener's socket address can be customized with the --listen flag.\n\nIf a domain name is specified with --domain, the default listener address\nwill be changed to the HTTPS port and the server will use HTTPS. If using\na public domain, ensure A/AAAA records are properly configured before\nusing this option.\n\nFor more advanced use cases, see https://github.com/php/frankenphp/blob/main/docs/config.md`,\n\t\tCobraFunc: func(cmd *cobra.Command) {\n\t\t\tcmd.Flags().StringP(\"domain\", \"d\", \"\", \"Domain name at which to serve the files\")\n\t\t\tcmd.Flags().StringP(\"root\", \"r\", \"\", \"The path to the root of the site\")\n\t\t\tcmd.Flags().StringP(\"listen\", \"l\", \"\", \"The address to which to bind the listener\")\n\t\t\tcmd.Flags().StringArrayP(\"worker\", \"w\", []string{}, \"Worker script\")\n\t\t\tcmd.Flags().StringArray(\"watch\", []string{}, \"Glob pattern of directories and files to watch for changes\")\n\t\t\tcmd.Flags().BoolP(\"access-log\", \"a\", false, \"Enable the access log\")\n\t\t\tcmd.Flags().BoolP(\"debug\", \"v\", false, \"Enable verbose debug logs\")\n\t\t\tcmd.Flags().BoolP(\"mercure\", \"m\", false, \"Enable the built-in Mercure.rocks hub\")\n\t\t\tcmd.Flags().Bool(\"no-compress\", false, \"Disable Zstandard, Brotli and Gzip compression\")\n\n\t\t\tcmd.Flags().Lookup(\"watch\").NoOptDefVal = defaultWatchPattern\n\n\t\t\tcmd.RunE = caddycmd.WrapCommandFuncForCobra(cmdPHPServer)\n\t\t},\n\t})\n}\n\n// cmdPHPServer is freely inspired from the file-server command of the Caddy server (Apache License 2.0, Matthew Holt and The Caddy Authors)\nfunc cmdPHPServer(fs caddycmd.Flags) (int, error) {\n\tcaddy.TrapSignals()\n\n\tdomain := fs.String(\"domain\")\n\troot := fs.String(\"root\")\n\tlisten := fs.String(\"listen\")\n\taccessLog := fs.Bool(\"access-log\")\n\tdebug := fs.Bool(\"debug\")\n\tcompress := !fs.Bool(\"no-compress\")\n\tmercure := fs.Bool(\"mercure\")\n\n\tworkers, err := fs.GetStringArray(\"worker\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\twatch, err := fs.GetStringArray(\"watch\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tif frankenphp.EmbeddedAppPath != \"\" {\n\t\tif err := os.Chdir(frankenphp.EmbeddedAppPath); err != nil {\n\t\t\treturn caddy.ExitCodeFailedStartup, err\n\t\t}\n\t}\n\n\tvar workersOption []workerConfig\n\tif len(workers) != 0 {\n\t\tworkersOption = make([]workerConfig, 0, len(workers))\n\t\tfor _, worker := range workers {\n\t\t\tparts := strings.SplitN(worker, \",\", 2)\n\n\t\t\tvar num uint64\n\t\t\tif len(parts) > 1 {\n\t\t\t\tnum, _ = strconv.ParseUint(parts[1], 10, 32)\n\t\t\t}\n\n\t\t\tworkersOption = append(workersOption, workerConfig{FileName: parts[0], Num: int(num)})\n\t\t}\n\t\tworkersOption[0].Watch = watch\n\t}\n\n\tif frankenphp.EmbeddedAppPath != \"\" {\n\t\tif _, err := os.Stat(\"php.ini\"); err == nil {\n\t\t\tiniScanDir := os.Getenv(\"PHP_INI_SCAN_DIR\")\n\n\t\t\tif err := os.Setenv(\"PHP_INI_SCAN_DIR\", iniScanDir+\":\"+frankenphp.EmbeddedAppPath); err != nil {\n\t\t\t\treturn caddy.ExitCodeFailedStartup, err\n\t\t\t}\n\t\t}\n\n\t\tif _, err := os.Stat(\"Caddyfile\"); err == nil {\n\t\t\tconfig, _, _, err := caddycmd.LoadConfig(\"Caddyfile\", \"caddyfile\")\n\t\t\tif err != nil {\n\t\t\t\treturn caddy.ExitCodeFailedStartup, err\n\t\t\t}\n\n\t\t\tif err = caddy.Load(config, true); err != nil {\n\t\t\t\treturn caddy.ExitCodeFailedStartup, err\n\t\t\t}\n\n\t\t\tselect {}\n\t\t}\n\n\t\tif root == \"\" {\n\t\t\troot = defaultDocumentRoot\n\t\t}\n\t}\n\n\tconst indexFile = \"index.php\"\n\textensions := []string{\".php\"}\n\ttryFiles := []string{\"{http.request.uri.path}\", \"{http.request.uri.path}/\" + indexFile, indexFile}\n\n\trrs := true\n\tphpHandler := FrankenPHPModule{\n\t\tRoot:               root,\n\t\tSplitPath:          extensions,\n\t\tResolveRootSymlink: &rrs,\n\t}\n\n\t// route to redirect to canonical path if index PHP file\n\tredirMatcherSet := caddy.ModuleMap{\n\t\t\"file\": caddyconfig.JSON(fileserver.MatchFile{\n\t\t\tRoot:     root,\n\t\t\tTryFiles: []string{\"{http.request.uri.path}/\" + indexFile},\n\t\t}, nil),\n\t\t\"not\": caddyconfig.JSON(caddyhttp.MatchNot{\n\t\t\tMatcherSetsRaw: []caddy.ModuleMap{\n\t\t\t\t{\n\t\t\t\t\t\"path\": caddyconfig.JSON(caddyhttp.MatchPath{\"*/\"}, nil),\n\t\t\t\t},\n\t\t\t},\n\t\t}, nil),\n\t}\n\tredirHandler := caddyhttp.StaticResponse{\n\t\tStatusCode: caddyhttp.WeakString(strconv.Itoa(http.StatusPermanentRedirect)),\n\t\tHeaders:    http.Header{\"Location\": []string{\"{http.request.orig_uri.path}/\"}},\n\t}\n\tredirRoute := caddyhttp.Route{\n\t\tMatcherSetsRaw: []caddy.ModuleMap{redirMatcherSet},\n\t\tHandlersRaw:    []json.RawMessage{caddyconfig.JSONModuleObject(redirHandler, \"handler\", \"static_response\", nil)},\n\t}\n\n\t// route to rewrite to PHP index file\n\trewriteMatcherSet := caddy.ModuleMap{\n\t\t\"file\": caddyconfig.JSON(fileserver.MatchFile{\n\t\t\tRoot:      root,\n\t\t\tTryFiles:  tryFiles,\n\t\t\tSplitPath: extensions,\n\t\t}, nil),\n\t}\n\trewriteHandler := rewrite.Rewrite{\n\t\tURI: \"{http.matchers.file.relative}\",\n\t}\n\trewriteRoute := caddyhttp.Route{\n\t\tMatcherSetsRaw: []caddy.ModuleMap{rewriteMatcherSet},\n\t\tHandlersRaw:    []json.RawMessage{caddyconfig.JSONModuleObject(rewriteHandler, \"handler\", \"rewrite\", nil)},\n\t}\n\n\t// route to actually pass requests to PHP files;\n\t// match only requests that are for PHP files\n\tvar pathList []string\n\tfor _, ext := range extensions {\n\t\tpathList = append(pathList, \"*\"+ext)\n\t}\n\tphpMatcherSet := caddy.ModuleMap{\n\t\t\"path\": caddyconfig.JSON(pathList, nil),\n\t}\n\n\t// create the PHP route which is\n\t// conditional on matching PHP files\n\tphpRoute := caddyhttp.Route{\n\t\tMatcherSetsRaw: []caddy.ModuleMap{phpMatcherSet},\n\t\tHandlersRaw:    []json.RawMessage{caddyconfig.JSONModuleObject(phpHandler, \"handler\", \"php\", nil)},\n\t}\n\n\tfileRoute := caddyhttp.Route{\n\t\tMatcherSetsRaw: []caddy.ModuleMap{},\n\t\tHandlersRaw:    []json.RawMessage{caddyconfig.JSONModuleObject(fileserver.FileServer{Root: root}, \"handler\", \"file_server\", nil)},\n\t}\n\n\tsubroute := caddyhttp.Subroute{\n\t\tRoutes: caddyhttp.RouteList{redirRoute, rewriteRoute, phpRoute, fileRoute},\n\t}\n\n\tif compress {\n\t\tgzip, err := caddy.GetModule(\"http.encoders.gzip\")\n\t\tif err != nil {\n\t\t\treturn caddy.ExitCodeFailedStartup, err\n\t\t}\n\n\t\tbr, err := caddy.GetModule(\"http.encoders.br\")\n\t\tif err != nil && brotli {\n\t\t\treturn caddy.ExitCodeFailedStartup, err\n\t\t}\n\n\t\tzstd, err := caddy.GetModule(\"http.encoders.zstd\")\n\t\tif err != nil {\n\t\t\treturn caddy.ExitCodeFailedStartup, err\n\t\t}\n\n\t\tvar (\n\t\t\tencodings caddy.ModuleMap\n\t\t\tprefer    []string\n\t\t)\n\t\tif brotli {\n\t\t\tencodings = caddy.ModuleMap{\n\t\t\t\t\"zstd\": caddyconfig.JSON(zstd.New(), nil),\n\t\t\t\t\"br\":   caddyconfig.JSON(br.New(), nil),\n\t\t\t\t\"gzip\": caddyconfig.JSON(gzip.New(), nil),\n\t\t\t}\n\t\t\tprefer = []string{\"zstd\", \"br\", \"gzip\"}\n\t\t} else {\n\t\t\tencodings = caddy.ModuleMap{\n\t\t\t\t\"zstd\": caddyconfig.JSON(zstd.New(), nil),\n\t\t\t\t\"gzip\": caddyconfig.JSON(gzip.New(), nil),\n\t\t\t}\n\t\t\tprefer = []string{\"zstd\", \"gzip\"}\n\t\t}\n\n\t\tencodeRoute := caddyhttp.Route{\n\t\t\tMatcherSetsRaw: []caddy.ModuleMap{},\n\t\t\tHandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(encode.Encode{\n\t\t\t\tEncodingsRaw: encodings,\n\t\t\t\tPrefer:       prefer,\n\t\t\t}, \"handler\", \"encode\", nil)},\n\t\t}\n\n\t\tsubroute.Routes = append(caddyhttp.RouteList{encodeRoute}, subroute.Routes...)\n\t}\n\n\tif mercure {\n\t\tmercureRoute, err := createMercureRoute()\n\t\tif err != nil {\n\t\t\treturn caddy.ExitCodeFailedStartup, err\n\t\t}\n\n\t\tsubroute.Routes = append(caddyhttp.RouteList{mercureRoute}, subroute.Routes...)\n\t}\n\n\troute := caddyhttp.Route{\n\t\tHandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(subroute, \"handler\", \"subroute\", nil)},\n\t}\n\n\tif domain != \"\" {\n\t\troute.MatcherSetsRaw = []caddy.ModuleMap{\n\t\t\t{\n\t\t\t\t\"host\": caddyconfig.JSON(caddyhttp.MatchHost{domain}, nil),\n\t\t\t},\n\t\t}\n\t}\n\n\tserver := &caddyhttp.Server{\n\t\tReadHeaderTimeout: caddy.Duration(10 * time.Second),\n\t\tIdleTimeout:       caddy.Duration(30 * time.Second),\n\t\tMaxHeaderBytes:    1024 * 10,\n\t\tRoutes:            caddyhttp.RouteList{route},\n\t}\n\tif listen == \"\" {\n\t\tif domain == \"\" {\n\t\t\tlisten = \":80\"\n\t\t} else {\n\t\t\tlisten = \":\" + strconv.Itoa(certmagic.HTTPSPort)\n\t\t}\n\t}\n\tserver.Listen = []string{listen}\n\tif accessLog {\n\t\tserver.Logs = &caddyhttp.ServerLogConfig{}\n\t}\n\n\thttpApp := caddyhttp.App{\n\t\tServers: map[string]*caddyhttp.Server{\"php\": server},\n\t}\n\n\tvar f bool\n\tcfg := &caddy.Config{\n\t\tAdmin: &caddy.AdminConfig{\n\t\t\tDisabled: true,\n\t\t\tConfig: &caddy.ConfigSettings{\n\t\t\t\tPersist: &f,\n\t\t\t},\n\t\t},\n\t\tAppsRaw: caddy.ModuleMap{\n\t\t\t\"http\":       caddyconfig.JSON(httpApp, nil),\n\t\t\t\"frankenphp\": caddyconfig.JSON(FrankenPHPApp{Workers: workersOption}, nil),\n\t\t},\n\t}\n\n\tif debug {\n\t\tcfg.Logging = &caddy.Logging{\n\t\t\tLogs: map[string]*caddy.CustomLog{\n\t\t\t\t\"default\": {\n\t\t\t\t\tBaseLog: caddy.BaseLog{Level: slog.LevelDebug.String()},\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\t}\n\n\terr = caddy.Run(cfg)\n\tif err != nil {\n\t\treturn caddy.ExitCodeFailedStartup, err\n\t}\n\n\tlog.Printf(\"Caddy serving PHP app on %s\", listen)\n\n\tselect {}\n}\n"
  },
  {
    "path": "caddy/watcher_test.go",
    "content": "//go:build !nowatcher\n\npackage caddy_test\n\nimport (\n\t\"net/http\"\n\t\"testing\"\n\n\t\"github.com/caddyserver/caddy/v2/caddytest\"\n)\n\nfunc TestWorkerWithInactiveWatcher(t *testing.T) {\n\ttester := caddytest.NewTester(t)\n\ttester.InitServer(`\n\t\t{\n\t\t\tskip_install_trust\n\t\t\tadmin localhost:2999\n\t\t\thttp_port `+testPort+`\n\n\t\t\tfrankenphp {\n\t\t\t\tworker {\n\t\t\t\t\tfile ../testdata/worker-with-counter.php\n\t\t\t\t\tnum 1\n\t\t\t\t\twatch ./**/*.php\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tlocalhost:`+testPort+` {\n\t\t\troot ../testdata\n\t\t\trewrite worker-with-counter.php\n\t\t\tphp\n\t\t}\n\t\t`, \"caddyfile\")\n\n\ttester.AssertGetResponse(\"http://localhost:\"+testPort, http.StatusOK, \"requests:1\")\n\ttester.AssertGetResponse(\"http://localhost:\"+testPort, http.StatusOK, \"requests:2\")\n}\n"
  },
  {
    "path": "caddy/workerconfig.go",
    "content": "package caddy\n\nimport (\n\t\"net/http\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"strconv\"\n\n\t\"github.com/caddyserver/caddy/v2\"\n\t\"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile\"\n\t\"github.com/caddyserver/caddy/v2/modules/caddyhttp\"\n\t\"github.com/dunglas/frankenphp\"\n\t\"github.com/dunglas/frankenphp/internal/fastabs\"\n)\n\n// workerConfig represents the \"worker\" directive in the Caddyfile\n// it can appear in the \"frankenphp\", \"php_server\" and \"php\" directives\n//\n//\tfrankenphp {\n//\t\tworker {\n//\t\t\tname \"my-worker\"\n//\t\t\tfile \"my-worker.php\"\n//\t\t}\n//\t}\ntype workerConfig struct {\n\tmercureContext\n\n\t// Name for the worker. Default: the filename for FrankenPHPApp workers, always prefixed with \"m#\" for FrankenPHPModule workers.\n\tName string `json:\"name,omitempty\"`\n\t// FileName sets the path to the worker script.\n\tFileName string `json:\"file_name,omitempty\"`\n\t// Num sets the number of workers to start.\n\tNum int `json:\"num,omitempty\"`\n\t// MaxThreads sets the maximum number of threads for this worker.\n\tMaxThreads int `json:\"max_threads,omitempty\"`\n\t// Env sets an extra environment variable to the given value. Can be specified more than once for multiple environment variables.\n\tEnv map[string]string `json:\"env,omitempty\"`\n\t// Directories to watch for file changes\n\tWatch []string `json:\"watch,omitempty\"`\n\t// The path to match against the worker\n\tMatchPath []string `json:\"match_path,omitempty\"`\n\t// MaxConsecutiveFailures sets the maximum number of consecutive failures before panicking (defaults to 6, set to -1 to never panick)\n\tMaxConsecutiveFailures int `json:\"max_consecutive_failures,omitempty\"`\n\n\toptions        []frankenphp.WorkerOption\n\trequestOptions []frankenphp.RequestOption\n\tabsFileName    string\n\tmatchRelPath   string // pre-computed relative URL path for fast matching\n}\n\nfunc unmarshalWorker(d *caddyfile.Dispenser) (workerConfig, error) {\n\twc := workerConfig{}\n\tif d.NextArg() {\n\t\twc.FileName = d.Val()\n\t}\n\n\tif d.NextArg() {\n\t\tif d.Val() == \"watch\" {\n\t\t\twc.Watch = append(wc.Watch, defaultWatchPattern)\n\t\t} else {\n\t\t\tv, err := strconv.ParseUint(d.Val(), 10, 32)\n\t\t\tif err != nil {\n\t\t\t\treturn wc, err\n\t\t\t}\n\n\t\t\twc.Num = int(v)\n\t\t}\n\t}\n\n\tif d.NextArg() {\n\t\treturn wc, d.Errf(`FrankenPHP: too many \"worker\" arguments: %s`, d.Val())\n\t}\n\n\tfor d.NextBlock(1) {\n\t\tswitch v := d.Val(); v {\n\t\tcase \"name\":\n\t\t\tif !d.NextArg() {\n\t\t\t\treturn wc, d.ArgErr()\n\t\t\t}\n\t\t\twc.Name = d.Val()\n\t\tcase \"file\":\n\t\t\tif !d.NextArg() {\n\t\t\t\treturn wc, d.ArgErr()\n\t\t\t}\n\t\t\twc.FileName = d.Val()\n\t\tcase \"num\":\n\t\t\tif !d.NextArg() {\n\t\t\t\treturn wc, d.ArgErr()\n\t\t\t}\n\n\t\t\tv, err := strconv.ParseUint(d.Val(), 10, 32)\n\t\t\tif err != nil {\n\t\t\t\treturn wc, d.WrapErr(err)\n\t\t\t}\n\n\t\t\twc.Num = int(v)\n\t\tcase \"max_threads\":\n\t\t\tif !d.NextArg() {\n\t\t\t\treturn wc, d.ArgErr()\n\t\t\t}\n\n\t\t\tv, err := strconv.ParseUint(d.Val(), 10, 32)\n\t\t\tif err != nil {\n\t\t\t\treturn wc, d.WrapErr(err)\n\t\t\t}\n\n\t\t\twc.MaxThreads = int(v)\n\t\tcase \"env\":\n\t\t\targs := d.RemainingArgs()\n\t\t\tif len(args) != 2 {\n\t\t\t\treturn wc, d.ArgErr()\n\t\t\t}\n\t\t\tif wc.Env == nil {\n\t\t\t\twc.Env = make(map[string]string)\n\t\t\t}\n\t\t\twc.Env[args[0]] = args[1]\n\t\tcase \"watch\":\n\t\t\tpatterns := d.RemainingArgs()\n\t\t\tif len(patterns) == 0 {\n\t\t\t\t// the default if the watch directory is left empty:\n\t\t\t\twc.Watch = append(wc.Watch, defaultWatchPattern)\n\t\t\t} else {\n\t\t\t\twc.Watch = append(wc.Watch, patterns...)\n\t\t\t}\n\t\tcase \"match\":\n\t\t\t// provision the path so it's identical to Caddy match rules\n\t\t\t// see: https://github.com/caddyserver/caddy/blob/master/modules/caddyhttp/matchers.go\n\t\t\tcaddyMatchPath := (caddyhttp.MatchPath)(d.RemainingArgs())\n\t\t\tif err := caddyMatchPath.Provision(caddy.Context{}); err != nil {\n\t\t\t\treturn wc, d.WrapErr(err)\n\t\t\t}\n\n\t\t\twc.MatchPath = caddyMatchPath\n\t\tcase \"max_consecutive_failures\":\n\t\t\tif !d.NextArg() {\n\t\t\t\treturn wc, d.ArgErr()\n\t\t\t}\n\n\t\t\tv, err := strconv.Atoi(d.Val())\n\t\t\tif err != nil {\n\t\t\t\treturn wc, d.WrapErr(err)\n\t\t\t}\n\t\t\tif v < -1 {\n\t\t\t\treturn wc, d.Errf(\"max_consecutive_failures must be >= -1\")\n\t\t\t}\n\n\t\t\twc.MaxConsecutiveFailures = v\n\t\tdefault:\n\t\t\treturn wc, wrongSubDirectiveError(\"worker\", \"name, file, num, env, watch, match, max_consecutive_failures, max_threads\", v)\n\t\t}\n\t}\n\n\tif wc.FileName == \"\" {\n\t\treturn wc, d.Err(`the \"file\" argument must be specified`)\n\t}\n\n\tif frankenphp.EmbeddedAppPath != \"\" && filepath.IsLocal(wc.FileName) {\n\t\twc.FileName = filepath.Join(frankenphp.EmbeddedAppPath, wc.FileName)\n\t}\n\n\treturn wc, nil\n}\n\nfunc (wc *workerConfig) inheritEnv(env map[string]string) {\n\tif wc.Env == nil {\n\t\twc.Env = make(map[string]string, len(env))\n\t}\n\tfor k, v := range env {\n\t\t// do not overwrite existing environment variables\n\t\tif _, exists := wc.Env[k]; !exists {\n\t\t\twc.Env[k] = v\n\t\t}\n\t}\n}\n\nfunc (wc *workerConfig) matchesPath(r *http.Request, documentRoot string) bool {\n\t// try to match against a pattern if one is assigned\n\tif len(wc.MatchPath) != 0 {\n\t\treturn (caddyhttp.MatchPath)(wc.MatchPath).Match(r)\n\t}\n\n\t// fast path: compare the request URL path against the pre-computed relative path\n\tif wc.matchRelPath != \"\" {\n\t\treqPath := r.URL.Path\n\t\tif reqPath == wc.matchRelPath {\n\t\t\treturn true\n\t\t}\n\n\t\t// ensure leading slash for relative paths (see #2166)\n\t\tif reqPath == \"\" || reqPath[0] != '/' {\n\t\t\treqPath = \"/\" + reqPath\n\t\t}\n\n\t\treturn path.Clean(reqPath) == wc.matchRelPath\n\t}\n\n\t// fallback when documentRoot is dynamic (contains placeholders)\n\tfullPath, _ := fastabs.FastAbs(filepath.Join(documentRoot, r.URL.Path))\n\n\treturn fullPath == wc.absFileName\n}\n"
  },
  {
    "path": "cgi.go",
    "content": "package frankenphp\n\n// #cgo nocallback frankenphp_register_server_vars\n// #cgo nocallback frankenphp_register_variable_safe\n// #cgo nocallback frankenphp_register_known_variable\n// #cgo nocallback frankenphp_init_persistent_string\n// #cgo noescape frankenphp_register_server_vars\n// #cgo noescape frankenphp_register_variable_safe\n// #cgo noescape frankenphp_register_known_variable\n// #cgo noescape frankenphp_init_persistent_string\n// #include \"frankenphp.h\"\n// #include <php_variables.h>\nimport \"C\"\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"net\"\n\t\"net/http\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"unicode/utf8\"\n\t\"unsafe\"\n\n\t\"github.com/dunglas/frankenphp/internal/phpheaders\"\n\t\"golang.org/x/text/language\"\n\t\"golang.org/x/text/search\"\n)\n\n// cStringHTTPMethods caches C string versions of common HTTP methods\n// to avoid allocations in pinCString on every request.\nvar cStringHTTPMethods = map[string]*C.char{\n\t\"GET\":     C.CString(\"GET\"),\n\t\"HEAD\":    C.CString(\"HEAD\"),\n\t\"POST\":    C.CString(\"POST\"),\n\t\"PUT\":     C.CString(\"PUT\"),\n\t\"DELETE\":  C.CString(\"DELETE\"),\n\t\"CONNECT\": C.CString(\"CONNECT\"),\n\t\"OPTIONS\": C.CString(\"OPTIONS\"),\n\t\"TRACE\":   C.CString(\"TRACE\"),\n\t\"PATCH\":   C.CString(\"PATCH\"),\n}\n\n// computeKnownVariables returns a set of CGI environment variables for the request.\n//\n// TODO: handle this case https://github.com/caddyserver/caddy/issues/3718\n// Inspired by https://github.com/caddyserver/caddy/blob/master/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go\nfunc addKnownVariablesToServer(fc *frankenPHPContext, trackVarsArray *C.zval) {\n\trequest := fc.request\n\t// Separate remote IP and port; more lenient than net.SplitHostPort\n\tvar ip, port string\n\tif idx := strings.LastIndex(request.RemoteAddr, \":\"); idx > -1 {\n\t\tip = request.RemoteAddr[:idx]\n\t\tport = request.RemoteAddr[idx+1:]\n\t} else {\n\t\tip = request.RemoteAddr\n\t}\n\n\t// Remove [] from IPv6 addresses\n\tif len(ip) > 0 && ip[0] == '[' {\n\t\tip = ip[1 : len(ip)-1]\n\t}\n\n\tvar rs, https, sslProtocol *C.zend_string\n\tvar sslCipher string\n\n\tif request.TLS == nil {\n\t\trs = C.frankenphp_strings.httpLowercase\n\t\thttps = C.frankenphp_strings.empty\n\t\tsslProtocol = C.frankenphp_strings.empty\n\t\tsslCipher = \"\"\n\t} else {\n\t\trs = C.frankenphp_strings.httpsLowercase\n\t\thttps = C.frankenphp_strings.on\n\n\t\t// and pass the protocol details in a manner compatible with Apache's mod_ssl\n\t\t// (which is why these have an SSL_ prefix and not TLS_).\n\t\tsslProtocol = tlsProtocol(request.TLS.Version)\n\n\t\tif request.TLS.CipherSuite != 0 {\n\t\t\tsslCipher = tls.CipherSuiteName(request.TLS.CipherSuite)\n\t\t}\n\t}\n\n\treqHost, reqPort, _ := net.SplitHostPort(request.Host)\n\n\tif reqHost == \"\" {\n\t\t// whatever, just assume there was no port\n\t\treqHost = request.Host\n\t}\n\n\tif reqPort == \"\" {\n\t\t// compliance with the CGI specification requires that\n\t\t// the SERVER_PORT variable MUST be set to the TCP/IP port number on which this request is received from the client\n\t\t// even if the port is the default port for the scheme and could otherwise be omitted from a URI.\n\t\t// https://tools.ietf.org/html/rfc3875#section-4.1.15\n\t\tswitch rs {\n\t\tcase C.frankenphp_strings.httpsLowercase:\n\t\t\treqPort = \"443\"\n\t\tcase C.frankenphp_strings.httpLowercase:\n\t\t\treqPort = \"80\"\n\t\t}\n\t}\n\n\tserverPort := reqPort\n\tcontentLength := request.Header.Get(\"Content-Length\")\n\n\tvar requestURI string\n\tif fc.originalRequest != nil {\n\t\trequestURI = fc.originalRequest.URL.RequestURI()\n\t} else {\n\t\trequestURI = fc.requestURI\n\t}\n\n\trequestPath := ensureLeadingSlash(request.URL.Path)\n\n\tC.frankenphp_register_server_vars(trackVarsArray, C.frankenphp_server_vars{\n\t\t// approximate total length to avoid array re-hashing:\n\t\t// 28 CGI vars + headers + environment\n\t\ttotal_num_vars: C.size_t(28 + len(request.Header) + len(fc.env) + lengthOfEnv),\n\n\t\t// CGI vars with variable values\n\t\tremote_addr:         toUnsafeChar(ip),\n\t\tremote_addr_len:     C.size_t(len(ip)),\n\t\tremote_host:         toUnsafeChar(ip),\n\t\tremote_host_len:     C.size_t(len(ip)),\n\t\tremote_port:         toUnsafeChar(port),\n\t\tremote_port_len:     C.size_t(len(port)),\n\t\tdocument_root:       toUnsafeChar(fc.documentRoot),\n\t\tdocument_root_len:   C.size_t(len(fc.documentRoot)),\n\t\tpath_info:           toUnsafeChar(fc.pathInfo),\n\t\tpath_info_len:       C.size_t(len(fc.pathInfo)),\n\t\tphp_self:            toUnsafeChar(requestPath),\n\t\tphp_self_len:        C.size_t(len(requestPath)),\n\t\tdocument_uri:        toUnsafeChar(fc.docURI),\n\t\tdocument_uri_len:    C.size_t(len(fc.docURI)),\n\t\tscript_filename:     toUnsafeChar(fc.scriptFilename),\n\t\tscript_filename_len: C.size_t(len(fc.scriptFilename)),\n\t\tscript_name:         toUnsafeChar(fc.scriptName),\n\t\tscript_name_len:     C.size_t(len(fc.scriptName)),\n\t\tserver_name:         toUnsafeChar(reqHost),\n\t\tserver_name_len:     C.size_t(len(reqHost)),\n\t\tserver_port:         toUnsafeChar(serverPort),\n\t\tserver_port_len:     C.size_t(len(serverPort)),\n\t\tcontent_length:      toUnsafeChar(contentLength),\n\t\tcontent_length_len:  C.size_t(len(contentLength)),\n\t\tserver_protocol:     toUnsafeChar(request.Proto),\n\t\tserver_protocol_len: C.size_t(len(request.Proto)),\n\t\thttp_host:           toUnsafeChar(request.Host),\n\t\thttp_host_len:       C.size_t(len(request.Host)),\n\t\trequest_uri:         toUnsafeChar(requestURI),\n\t\trequest_uri_len:     C.size_t(len(requestURI)),\n\t\tssl_cipher:          toUnsafeChar(sslCipher),\n\t\tssl_cipher_len:      C.size_t(len(sslCipher)),\n\n\t\t// CGI vars with known values\n\t\trequest_scheme: rs,          // \"http\" or \"https\"\n\t\tssl_protocol:   sslProtocol, // values from tlsProtocol\n\t\thttps:          https,       // \"on\" or empty\n\t})\n}\n\nfunc addHeadersToServer(ctx context.Context, request *http.Request, trackVarsArray *C.zval) {\n\tfor field, val := range request.Header {\n\t\tif k := commonHeaders[field]; k != nil {\n\t\t\tv := strings.Join(val, \", \")\n\t\t\tC.frankenphp_register_known_variable(k, toUnsafeChar(v), C.size_t(len(v)), trackVarsArray)\n\t\t\tcontinue\n\t\t}\n\n\t\t// if the header name could not be cached, it needs to be registered safely\n\t\t// this is more inefficient but allows additional sanitizing by PHP\n\t\tk := phpheaders.GetUnCommonHeader(ctx, field)\n\t\tv := strings.Join(val, \", \")\n\t\tC.frankenphp_register_variable_safe(toUnsafeChar(k), toUnsafeChar(v), C.size_t(len(v)), trackVarsArray)\n\t}\n}\n\nfunc addPreparedEnvToServer(fc *frankenPHPContext, trackVarsArray *C.zval) {\n\tfor k, v := range fc.env {\n\t\tC.frankenphp_register_variable_safe(toUnsafeChar(k), toUnsafeChar(v), C.size_t(len(v)), trackVarsArray)\n\t}\n\tfc.env = nil\n}\n\n//export go_register_server_variables\nfunc go_register_server_variables(threadIndex C.uintptr_t, trackVarsArray *C.zval) {\n\tthread := phpThreads[threadIndex]\n\tfc := thread.frankenPHPContext()\n\n\tif fc.request != nil {\n\t\taddKnownVariablesToServer(fc, trackVarsArray)\n\t\taddHeadersToServer(thread.context(), fc.request, trackVarsArray)\n\t}\n\n\t// The Prepared Environment is registered last and can overwrite any previous values\n\taddPreparedEnvToServer(fc, trackVarsArray)\n}\n\n// splitCgiPath splits the request path into SCRIPT_NAME, SCRIPT_FILENAME, PATH_INFO, DOCUMENT_URI\nfunc splitCgiPath(fc *frankenPHPContext) {\n\tpath := fc.request.URL.Path\n\tsplitPath := fc.splitPath\n\n\tif splitPath == nil {\n\t\tsplitPath = []string{\".php\"}\n\t}\n\n\tif splitPos := splitPos(path, splitPath); splitPos > -1 {\n\t\tfc.docURI = path[:splitPos]\n\t\tfc.pathInfo = path[splitPos:]\n\n\t\t// Strip PATH_INFO from SCRIPT_NAME\n\t\tfc.scriptName = strings.TrimSuffix(path, fc.pathInfo)\n\n\t\t// Ensure the SCRIPT_NAME has a leading slash for compliance with RFC3875\n\t\t// Info: https://tools.ietf.org/html/rfc3875#section-4.1.13\n\t\tif fc.scriptName != \"\" && !strings.HasPrefix(fc.scriptName, \"/\") {\n\t\t\tfc.scriptName = \"/\" + fc.scriptName\n\t\t}\n\t}\n\n\t// TODO: is it possible to delay this and avoid saving everything in the context?\n\t// SCRIPT_FILENAME is the absolute path of SCRIPT_NAME\n\tfc.scriptFilename = sanitizedPathJoin(fc.documentRoot, fc.scriptName)\n\tfc.worker = workersByPath[fc.scriptFilename]\n}\n\nvar splitSearchNonASCII = search.New(language.Und, search.IgnoreCase)\n\n// splitPos returns the index where path should be split based on splitPath.\n// example: if splitPath is [\".php\"]\n// \"/path/to/script.php/some/path\": (\"/path/to/script.php\", \"/some/path\")\nfunc splitPos(path string, splitPath []string) int {\n\tif len(splitPath) == 0 {\n\t\treturn 0\n\t}\n\n\tpathLen := len(path)\n\n\t// We are sure that split strings are all ASCII-only and lower-case because of validation and normalization in WithRequestSplitPath\n\tfor _, split := range splitPath {\n\t\tsplitLen := len(split)\n\n\t\tfor i := 0; i < pathLen; i++ {\n\t\t\tif path[i] >= utf8.RuneSelf {\n\t\t\t\tif _, end := splitSearchNonASCII.IndexString(path, split); end > -1 {\n\t\t\t\t\treturn end\n\t\t\t\t}\n\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tif i+splitLen > pathLen {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tmatch := true\n\t\t\tfor j := 0; j < splitLen; j++ {\n\t\t\t\tc := path[i+j]\n\n\t\t\t\tif c >= utf8.RuneSelf {\n\t\t\t\t\tif _, end := splitSearchNonASCII.IndexString(path, split); end > -1 {\n\t\t\t\t\t\treturn end\n\t\t\t\t\t}\n\n\t\t\t\t\tbreak\n\t\t\t\t}\n\n\t\t\t\tif 'A' <= c && c <= 'Z' {\n\t\t\t\t\tc += 'a' - 'A'\n\t\t\t\t}\n\n\t\t\t\tif c != split[j] {\n\t\t\t\t\tmatch = false\n\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif match {\n\t\t\t\treturn i + splitLen\n\t\t\t}\n\t\t}\n\t}\n\n\treturn -1\n}\n\n// go_update_request_info updates the sapi_request_info struct\n// See: https://github.com/php/php-src/blob/345e04b619c3bc11ea17ee02cdecad6ae8ce5891/main/SAPI.h#L72\n//\n//export go_update_request_info\nfunc go_update_request_info(threadIndex C.uintptr_t, info *C.sapi_request_info) *C.char {\n\tthread := phpThreads[threadIndex]\n\tfc := thread.frankenPHPContext()\n\trequest := fc.request\n\n\tif request == nil {\n\t\treturn nil\n\t}\n\n\tif m, ok := cStringHTTPMethods[request.Method]; ok {\n\t\tinfo.request_method = m\n\t} else {\n\t\tinfo.request_method = thread.pinCString(request.Method)\n\t}\n\tinfo.query_string = thread.pinCString(request.URL.RawQuery)\n\tinfo.content_length = C.zend_long(request.ContentLength)\n\n\tif contentType := request.Header.Get(\"Content-Type\"); contentType != \"\" {\n\t\tinfo.content_type = thread.pinCString(contentType)\n\t}\n\n\tif fc.pathInfo != \"\" {\n\t\tinfo.path_translated = thread.pinCString(sanitizedPathJoin(fc.documentRoot, fc.pathInfo)) // See: http://www.oreilly.com/openbook/cgi/ch02_04.html\n\t}\n\n\tinfo.request_uri = thread.pinCString(fc.requestURI)\n\n\tinfo.proto_num = C.int(request.ProtoMajor*1000 + request.ProtoMinor)\n\n\tauthorizationHeader := request.Header.Get(\"Authorization\")\n\tif authorizationHeader == \"\" {\n\t\treturn nil\n\t}\n\n\treturn thread.pinCString(authorizationHeader)\n}\n\n// SanitizedPathJoin performs filepath.Join(root, reqPath) that\n// is safe against directory traversal attacks. It uses logic\n// similar to that in the Go standard library, specifically\n// in the implementation of http.Dir. The root is assumed to\n// be a trusted path, but reqPath is not; and the output will\n// never be outside of root. The resulting path can be used\n// with the local file system.\n//\n// Adapted from https://github.com/caddyserver/caddy/blob/master/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go\n// Copyright 2015 Matthew Holt and The Caddy Authors\nfunc sanitizedPathJoin(root, reqPath string) string {\n\tif root == \"\" {\n\t\troot = \".\"\n\t}\n\n\tpath := filepath.Join(root, filepath.Clean(\"/\"+reqPath))\n\n\t// filepath.Join also cleans the path, and cleaning strips\n\t// the trailing slash, so we need to re-add it afterward.\n\t// if the length is 1, then it's a path to the root,\n\t// and that should return \".\", so we don't append the separator.\n\tif strings.HasSuffix(reqPath, \"/\") && len(reqPath) > 1 {\n\t\tpath += separator\n\t}\n\n\treturn path\n}\n\nconst separator = string(filepath.Separator)\n\nfunc ensureLeadingSlash(path string) string {\n\tif path == \"\" || path[0] == '/' {\n\t\treturn path\n\t}\n\n\treturn \"/\" + path\n}\n\n// toUnsafeChar returns a *C.char pointing at the backing bytes the Go string.\n// If C does not store the string, it may be passed directly in a Cgo call (most efficient).\n// If C stores the string, it must be pinned explicitly instead (inefficient).\n// C may never modify the string.\nfunc toUnsafeChar(s string) *C.char {\n\treturn (*C.char)(unsafe.Pointer(unsafe.StringData(s)))\n}\n\n// initialize a global zend_string that must never be freed and is ignored by GC\nfunc newPersistentZendString(str string) *C.zend_string {\n\treturn C.frankenphp_init_persistent_string(toUnsafeChar(str), C.size_t(len(str)))\n}\n\n// Protocol versions, in Apache mod_ssl format: https://httpd.apache.org/docs/current/mod/mod_ssl.html\n// Note that these are slightly different from SupportedProtocols in caddytls/config.go\nfunc tlsProtocol(proto uint16) *C.zend_string {\n\tswitch proto {\n\tcase tls.VersionTLS10:\n\t\treturn C.frankenphp_strings.tls1\n\tcase tls.VersionTLS11:\n\t\treturn C.frankenphp_strings.tls11\n\tcase tls.VersionTLS12:\n\t\treturn C.frankenphp_strings.tls12\n\tcase tls.VersionTLS13:\n\t\treturn C.frankenphp_strings.tls13\n\tdefault:\n\t\treturn C.frankenphp_strings.empty\n\t}\n}\n"
  },
  {
    "path": "cgi_test.go",
    "content": "package frankenphp\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestEnsureLeadingSlash(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tinput    string\n\t\texpected string\n\t}{\n\t\t{\"/index.php\", \"/index.php\"},\n\t\t{\"index.php\", \"/index.php\"},\n\t\t{\"/\", \"/\"},\n\t\t{\"\", \"\"},\n\t\t{\"/path/to/script.php\", \"/path/to/script.php\"},\n\t\t{\"path/to/script.php\", \"/path/to/script.php\"},\n\t\t{\"/index.php/path/info\", \"/index.php/path/info\"},\n\t\t{\"index.php/path/info\", \"/index.php/path/info\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.input+\"-\"+tt.expected, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tassert.Equal(t, tt.expected, ensureLeadingSlash(tt.input), \"ensureLeadingSlash(%q)\", tt.input)\n\t\t})\n\t}\n}\n\nfunc TestSplitPos(t *testing.T) {\n\ttests := []struct {\n\t\tname      string\n\t\tpath      string\n\t\tsplitPath []string\n\t\twantPos   int\n\t}{\n\t\t{\n\t\t\tname:      \"simple php extension\",\n\t\t\tpath:      \"/path/to/script.php\",\n\t\t\tsplitPath: []string{\".php\"},\n\t\t\twantPos:   19,\n\t\t},\n\t\t{\n\t\t\tname:      \"php extension with path info\",\n\t\t\tpath:      \"/path/to/script.php/some/path\",\n\t\t\tsplitPath: []string{\".php\"},\n\t\t\twantPos:   19,\n\t\t},\n\t\t{\n\t\t\tname:      \"case insensitive match\",\n\t\t\tpath:      \"/path/to/script.PHP\",\n\t\t\tsplitPath: []string{\".php\"},\n\t\t\twantPos:   19,\n\t\t},\n\t\t{\n\t\t\tname:      \"mixed case match\",\n\t\t\tpath:      \"/path/to/script.PhP/info\",\n\t\t\tsplitPath: []string{\".php\"},\n\t\t\twantPos:   19,\n\t\t},\n\t\t{\n\t\t\tname:      \"no match\",\n\t\t\tpath:      \"/path/to/script.txt\",\n\t\t\tsplitPath: []string{\".php\"},\n\t\t\twantPos:   -1,\n\t\t},\n\t\t{\n\t\t\tname:      \"empty split path\",\n\t\t\tpath:      \"/path/to/script.php\",\n\t\t\tsplitPath: []string{},\n\t\t\twantPos:   0,\n\t\t},\n\t\t{\n\t\t\tname:      \"multiple split paths first match\",\n\t\t\tpath:      \"/path/to/script.php\",\n\t\t\tsplitPath: []string{\".php\", \".phtml\"},\n\t\t\twantPos:   19,\n\t\t},\n\t\t{\n\t\t\tname:      \"multiple split paths second match\",\n\t\t\tpath:      \"/path/to/script.phtml\",\n\t\t\tsplitPath: []string{\".php\", \".phtml\"},\n\t\t\twantPos:   21,\n\t\t},\n\t\t// Unicode case-folding tests (security fix for GHSA-g966-83w7-6w38)\n\t\t// U+023A (Ⱥ) lowercases to U+2C65 (ⱥ), which has different UTF-8 byte length\n\t\t// Ⱥ: 2 bytes (C8 BA), ⱥ: 3 bytes (E2 B1 A5)\n\t\t{\n\t\t\tname:      \"unicode path with case-folding length expansion\",\n\t\t\tpath:      \"/ȺȺȺȺshell.php\",\n\t\t\tsplitPath: []string{\".php\"},\n\t\t\twantPos:   18, // correct position in original string\n\t\t},\n\t\t{\n\t\t\tname:      \"unicode path with extension after expansion chars\",\n\t\t\tpath:      \"/ȺȺȺȺshell.php/path/info\",\n\t\t\tsplitPath: []string{\".php\"},\n\t\t\twantPos:   18,\n\t\t},\n\t\t{\n\t\t\tname:      \"unicode in filename with multiple php occurrences\",\n\t\t\tpath:      \"/ȺȺȺȺshell.php.txt.php\",\n\t\t\tsplitPath: []string{\".php\"},\n\t\t\twantPos:   18, // should match first .php, not be confused by byte offset shift\n\t\t},\n\t\t{\n\t\t\tname:      \"unicode case insensitive extension\",\n\t\t\tpath:      \"/ȺȺȺȺshell.PHP\",\n\t\t\tsplitPath: []string{\".php\"},\n\t\t\twantPos:   18,\n\t\t},\n\t\t{\n\t\t\tname:      \"unicode in middle of path\",\n\t\t\tpath:      \"/path/Ⱥtest/script.php\",\n\t\t\tsplitPath: []string{\".php\"},\n\t\t\twantPos:   23, // Ⱥ is 2 bytes, so path is 23 bytes total, .php ends at byte 23\n\t\t},\n\t\t{\n\t\t\tname:      \"unicode only in directory not filename\",\n\t\t\tpath:      \"/Ⱥ/script.php\",\n\t\t\tsplitPath: []string{\".php\"},\n\t\t\twantPos:   14,\n\t\t},\n\t\t// Additional Unicode characters that expand when lowercased\n\t\t// U+0130 (İ - Turkish capital I with dot) lowercases to U+0069 + U+0307\n\t\t{\n\t\t\tname:      \"turkish capital I with dot\",\n\t\t\tpath:      \"/İtest.php\",\n\t\t\tsplitPath: []string{\".php\"},\n\t\t\twantPos:   11,\n\t\t},\n\t\t// Ensure standard ASCII still works correctly\n\t\t{\n\t\t\tname:      \"ascii only path with case variation\",\n\t\t\tpath:      \"/PATH/TO/SCRIPT.PHP/INFO\",\n\t\t\tsplitPath: []string{\".php\"},\n\t\t\twantPos:   19,\n\t\t},\n\t\t{\n\t\t\tname:      \"path at root\",\n\t\t\tpath:      \"/index.php\",\n\t\t\tsplitPath: []string{\".php\"},\n\t\t\twantPos:   10,\n\t\t},\n\t\t{\n\t\t\tname:      \"extension in middle of filename\",\n\t\t\tpath:      \"/test.php.bak\",\n\t\t\tsplitPath: []string{\".php\"},\n\t\t\twantPos:   9,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgotPos := splitPos(tt.path, tt.splitPath)\n\t\t\tassert.Equal(t, tt.wantPos, gotPos, \"splitPos(%q, %v)\", tt.path, tt.splitPath)\n\n\t\t\t// Verify that the split produces valid substrings\n\t\t\tif gotPos > 0 && gotPos <= len(tt.path) {\n\t\t\t\tscriptName := tt.path[:gotPos]\n\t\t\t\tpathInfo := tt.path[gotPos:]\n\n\t\t\t\t// The script name should end with one of the split extensions (case-insensitive)\n\t\t\t\thasValidEnding := false\n\t\t\t\tfor _, split := range tt.splitPath {\n\t\t\t\t\tif strings.HasSuffix(strings.ToLower(scriptName), split) {\n\t\t\t\t\t\thasValidEnding = true\n\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tassert.True(t, hasValidEnding, \"script name %q should end with one of %v\", scriptName, tt.splitPath)\n\n\t\t\t\t// Original path should be reconstructable\n\t\t\t\tassert.Equal(t, tt.path, scriptName+pathInfo, \"path should be reconstructable from split parts\")\n\t\t\t}\n\t\t})\n\t}\n}\n\n// TestSplitPosUnicodeSecurityRegression specifically tests the vulnerability\n// described in GHSA-g966-83w7-6w38 where Unicode case-folding caused\n// incorrect SCRIPT_NAME/PATH_INFO splitting\nfunc TestSplitPosUnicodeSecurityRegression(t *testing.T) {\n\t// U+023A: Ⱥ (UTF-8: C8 BA). Lowercase is ⱥ (UTF-8: E2 B1 A5), longer in bytes.\n\tpath := \"/ȺȺȺȺshell.php.txt.php\"\n\tsplit := []string{\".php\"}\n\n\tpos := splitPos(path, split)\n\n\t// The vulnerable code would return 22 (computed on lowercased string)\n\t// The correct code should return 18 (position in original string)\n\texpectedPos := strings.Index(path, \".php\") + len(\".php\")\n\tassert.Equal(t, expectedPos, pos, \"split position should match first .php in original string\")\n\tassert.Equal(t, 18, pos, \"split position should be 18, not 22\")\n\n\tif pos > 0 && pos <= len(path) {\n\t\tscriptName := path[:pos]\n\t\tpathInfo := path[pos:]\n\n\t\tassert.Equal(t, \"/ȺȺȺȺshell.php\", scriptName, \"script name should be the path up to first .php\")\n\t\tassert.Equal(t, \".txt.php\", pathInfo, \"path info should be the remainder after first .php\")\n\t}\n}\n"
  },
  {
    "path": "cgo.go",
    "content": "package frankenphp\n\n// #cgo darwin pkg-config: libxml-2.0\n// #cgo unix CFLAGS: -Wall -Werror\n// #cgo linux CFLAGS: -D_GNU_SOURCE\n// #cgo unix LDFLAGS: -lphp -lm -lutil\n// #cgo linux LDFLAGS: -ldl -lresolv\n// #cgo darwin LDFLAGS: -Wl,-rpath,/usr/local/lib -liconv -ldl\n// #cgo windows CFLAGS: -D_WINDOWS -DWINDOWS=1 -DZEND_WIN32=1 -DPHP_WIN32=1 -DWIN32 -D_MBCS -D_USE_MATH_DEFINES -DNDebug -DNDEBUG -DZEND_DEBUG=0 -DZTS=1 -DFD_SETSIZE=256\n// #cgo windows LDFLAGS: -lpthreadVC3\nimport \"C\"\n"
  },
  {
    "path": "cli.go",
    "content": "package frankenphp\n\n// #include \"frankenphp.h\"\nimport \"C\"\nimport \"unsafe\"\n\n// ExecuteScriptCLI executes the PHP script passed as parameter.\n// It returns the exit status code of the script.\nfunc ExecuteScriptCLI(script string, args []string) int {\n\t// Ensure extensions are registered before CLI execution\n\tregisterExtensions()\n\n\tcScript := C.CString(script)\n\tdefer C.free(unsafe.Pointer(cScript))\n\n\targc, argv := convertArgs(args)\n\tdefer freeArgs(argv)\n\n\treturn int(C.frankenphp_execute_script_cli(cScript, argc, (**C.char)(unsafe.Pointer(&argv[0])), false))\n}\n\nfunc ExecutePHPCode(phpCode string) int {\n\t// Ensure extensions are registered before CLI execution\n\tregisterExtensions()\n\n\tcCode := C.CString(phpCode)\n\tdefer C.free(unsafe.Pointer(cCode))\n\treturn int(C.frankenphp_execute_script_cli(cCode, 0, nil, true))\n}\n"
  },
  {
    "path": "cli_test.go",
    "content": "package frankenphp_test\n\nimport (\n\t\"errors\"\n\t\"log\"\n\t\"os\"\n\t\"os/exec\"\n\t\"testing\"\n\n\t\"github.com/dunglas/frankenphp\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestExecuteScriptCLI(t *testing.T) {\n\tif _, err := os.Stat(\"internal/testcli/testcli\"); err != nil {\n\t\tt.Skip(\"internal/testcli/testcli has not been compiled, run `cd internal/testcli/ && go build`\")\n\t}\n\n\tcmd := exec.Command(\"internal/testcli/testcli\", \"testdata/command.php\", \"foo\", \"bar\")\n\tstdoutStderr, err := cmd.CombinedOutput()\n\tassert.Error(t, err)\n\n\tvar exitError *exec.ExitError\n\tif errors.As(err, &exitError) {\n\t\tassert.Equal(t, 3, exitError.ExitCode())\n\t}\n\n\tstdoutStderrStr := string(stdoutStderr)\n\n\tassert.Contains(t, stdoutStderrStr, `\"foo\"`)\n\tassert.Contains(t, stdoutStderrStr, `\"bar\"`)\n\tassert.Contains(t, stdoutStderrStr, \"From the CLI\")\n}\n\nfunc TestExecuteCLICode(t *testing.T) {\n\tif _, err := os.Stat(\"internal/testcli/testcli\"); err != nil {\n\t\tt.Skip(\"internal/testcli/testcli has not been compiled, run `cd internal/testcli/ && go build`\")\n\t}\n\n\tcmd := exec.Command(\"internal/testcli/testcli\", \"-r\", \"echo 'Hello World';\")\n\tstdoutStderr, err := cmd.CombinedOutput()\n\tassert.NoError(t, err)\n\n\tstdoutStderrStr := string(stdoutStderr)\n\tassert.Equal(t, stdoutStderrStr, `Hello World`)\n}\n\nfunc ExampleExecuteScriptCLI() {\n\tif len(os.Args) <= 1 {\n\t\tlog.Println(\"Usage: my-program script.php\")\n\t\tos.Exit(1)\n\t}\n\n\tos.Exit(frankenphp.ExecuteScriptCLI(os.Args[1], os.Args))\n}\n"
  },
  {
    "path": "context.go",
    "content": "package frankenphp\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n)\n\n// frankenPHPContext provides contextual information about the Request to handle.\ntype frankenPHPContext struct {\n\tmercureContext\n\n\tdocumentRoot    string\n\tsplitPath       []string\n\tenv             PreparedEnv\n\tlogger          *slog.Logger\n\trequest         *http.Request\n\toriginalRequest *http.Request\n\tworker          *worker\n\n\tdocURI         string\n\tpathInfo       string\n\tscriptName     string\n\tscriptFilename string\n\trequestURI     string\n\n\t// Whether the request is already closed by us\n\tisDone bool\n\n\tresponseWriter     http.ResponseWriter\n\tresponseController *http.ResponseController\n\thandlerParameters  any\n\thandlerReturn      any\n\n\tdone      chan any\n\tstartedAt time.Time\n}\n\ntype contextHolder struct {\n\tctx               context.Context\n\tfrankenPHPContext *frankenPHPContext\n}\n\n// fromContext extracts the frankenPHPContext from a context.\nfunc fromContext(ctx context.Context) (fctx *frankenPHPContext, ok bool) {\n\tfctx, ok = ctx.Value(contextKey).(*frankenPHPContext)\n\treturn\n}\n\nfunc newFrankenPHPContext() *frankenPHPContext {\n\treturn &frankenPHPContext{\n\t\tdone:      make(chan any),\n\t\tstartedAt: time.Now(),\n\t}\n}\n\n// NewRequestWithContext creates a new FrankenPHP request context.\nfunc NewRequestWithContext(r *http.Request, opts ...RequestOption) (*http.Request, error) {\n\tfc := newFrankenPHPContext()\n\tfc.request = r\n\n\tfor _, o := range opts {\n\t\tif err := o(fc); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tif fc.logger == nil {\n\t\tfc.logger = globalLogger\n\t}\n\n\tif fc.documentRoot == \"\" {\n\t\tif EmbeddedAppPath != \"\" {\n\t\t\tfc.documentRoot = EmbeddedAppPath\n\t\t} else {\n\t\t\tvar err error\n\t\t\tif fc.documentRoot, err = os.Getwd(); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\t}\n\n\t// If a worker is already assigned explicitly, use its filename and skip parsing path variables\n\tif fc.worker != nil {\n\t\tfc.scriptFilename = fc.worker.fileName\n\t} else {\n\t\t// If no worker was assigned, split the path into the \"traditional\" CGI path variables.\n\t\t// This needs to already happen here in case a worker script still matches the path.\n\t\tsplitCgiPath(fc)\n\t}\n\n\tfc.requestURI = r.URL.RequestURI()\n\n\tc := context.WithValue(r.Context(), contextKey, fc)\n\n\treturn r.WithContext(c), nil\n}\n\n// newDummyContext creates a fake context from a request path\nfunc newDummyContext(requestPath string, opts ...RequestOption) (*frankenPHPContext, error) {\n\tr, err := http.NewRequestWithContext(globalCtx, http.MethodGet, requestPath, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfr, err := NewRequestWithContext(r, opts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfc, _ := fromContext(fr.Context())\n\n\treturn fc, nil\n}\n\n// closeContext sends the response to the client\nfunc (fc *frankenPHPContext) closeContext() {\n\tif fc.isDone {\n\t\treturn\n\t}\n\n\tclose(fc.done)\n\tfc.isDone = true\n}\n\n// validate checks if the request should be outright rejected\nfunc (fc *frankenPHPContext) validate() error {\n\tif strings.Contains(fc.request.URL.Path, \"\\x00\") {\n\t\tfc.reject(ErrInvalidRequestPath)\n\n\t\treturn ErrInvalidRequestPath\n\t}\n\n\tcontentLengthStr := fc.request.Header.Get(\"Content-Length\")\n\tif contentLengthStr != \"\" {\n\t\tif contentLength, err := strconv.Atoi(contentLengthStr); err != nil || contentLength < 0 {\n\t\t\te := fmt.Errorf(\"%w: %q\", ErrInvalidContentLengthHeader, contentLengthStr)\n\n\t\t\tfc.reject(e)\n\n\t\t\treturn e\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (fc *frankenPHPContext) clientHasClosed() bool {\n\tif fc.request == nil {\n\t\treturn false\n\t}\n\n\tselect {\n\tcase <-fc.request.Context().Done():\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\n// reject sends a response with the given status code and error\nfunc (fc *frankenPHPContext) reject(err error) {\n\tif fc.isDone {\n\t\treturn\n\t}\n\n\tre := &ErrRejected{}\n\tif !errors.As(err, re) {\n\t\t// Should never happen\n\t\tpanic(\"only instance of ErrRejected can be passed to reject\")\n\t}\n\n\trw := fc.responseWriter\n\tif rw != nil {\n\t\trw.WriteHeader(re.status)\n\t\t_, _ = rw.Write([]byte(err.Error()))\n\n\t\tif f, ok := rw.(http.Flusher); ok {\n\t\t\tf.Flush()\n\t\t}\n\t}\n\n\tfc.closeContext()\n}\n"
  },
  {
    "path": "debugstate.go",
    "content": "package frankenphp\n\nimport (\n\t\"github.com/dunglas/frankenphp/internal/state\"\n)\n\n// EXPERIMENTAL: ThreadDebugState prints the state of a single PHP thread - debugging purposes only\ntype ThreadDebugState struct {\n\tIndex                    int\n\tName                     string\n\tState                    string\n\tIsWaiting                bool\n\tIsBusy                   bool\n\tWaitingSinceMilliseconds int64\n}\n\n// EXPERIMENTAL: FrankenPHPDebugState prints the state of all PHP threads - debugging purposes only\ntype FrankenPHPDebugState struct {\n\tThreadDebugStates   []ThreadDebugState\n\tReservedThreadCount int\n}\n\n// EXPERIMENTAL: DebugState prints the state of all PHP threads - debugging purposes only\nfunc DebugState() FrankenPHPDebugState {\n\tfullState := FrankenPHPDebugState{\n\t\tThreadDebugStates:   make([]ThreadDebugState, 0, len(phpThreads)),\n\t\tReservedThreadCount: 0,\n\t}\n\tfor _, thread := range phpThreads {\n\t\tif thread.state.Is(state.Reserved) {\n\t\t\tfullState.ReservedThreadCount++\n\t\t\tcontinue\n\t\t}\n\t\tfullState.ThreadDebugStates = append(fullState.ThreadDebugStates, threadDebugState(thread))\n\t}\n\n\treturn fullState\n}\n\n// threadDebugState creates a small jsonable status message for debugging purposes\nfunc threadDebugState(thread *phpThread) ThreadDebugState {\n\treturn ThreadDebugState{\n\t\tIndex:                    thread.threadIndex,\n\t\tName:                     thread.name(),\n\t\tState:                    thread.state.Name(),\n\t\tIsWaiting:                thread.state.IsInWaitingState(),\n\t\tIsBusy:                   !thread.state.IsInWaitingState(),\n\t\tWaitingSinceMilliseconds: thread.state.WaitTime(),\n\t}\n}\n"
  },
  {
    "path": "dev-alpine.Dockerfile",
    "content": "# syntax=docker/dockerfile:1\n#checkov:skip=CKV_DOCKER_2\n#checkov:skip=CKV_DOCKER_3\nFROM golang:1.26-alpine\n\nENV GOTOOLCHAIN=local\nENV CFLAGS=\"-ggdb3\"\nENV PHPIZE_DEPS=\"\\\n\tautoconf \\\n\tdpkg-dev \\\n\tfile \\\n\tg++ \\\n\tgcc \\\n\tlibc-dev \\\n\tmake \\\n\tpkgconfig \\\n\tre2c\"\n\nSHELL [\"/bin/ash\", \"-eo\", \"pipefail\", \"-c\"]\n\nRUN apk add --no-cache \\\n\t$PHPIZE_DEPS \\\n\targon2-dev \\\n\tbrotli-dev \\\n\tcurl-dev \\\n\toniguruma-dev \\\n\treadline-dev \\\n\tlibsodium-dev \\\n\tsqlite-dev \\\n\topenssl-dev \\\n\tlibxml2-dev \\\n\tzlib-dev \\\n\tbison \\\n\tnss-tools \\\n\t# file watcher \\\n\tlibstdc++ \\\n\tlinux-headers \\\n\t# Dev tools \\\n\tgit \\\n\tclang \\\n\tcmake \\\n\tllvm \\\n\tgdb \\\n\tvalgrind \\\n\tneovim \\\n\tzsh \\\n\tlibtool && \\\n\techo 'set auto-load safe-path /' > /root/.gdbinit\n\nWORKDIR /usr/local/src/php\nRUN git clone --branch=PHP-8.5 https://github.com/php/php-src.git . && \\\n\t# --enable-embed is necessary to generate libphp.so, but we don't use this SAPI directly\n\t./buildconf --force && \\\n\tEXTENSION_DIR=/usr/lib/frankenphp/modules ./configure \\\n\t\t--enable-embed \\\n\t\t--enable-zts \\\n\t\t--disable-zend-signals \\\n\t\t--enable-zend-max-execution-timers \\\n\t\t--with-config-file-path=/etc/frankenphp/php.ini \\\n\t\t--with-config-file-scan-dir=/etc/frankenphp/php.d \\\n\t\t--enable-debug && \\\n\tmake -j\"$(nproc)\" && \\\n\tmake install && \\\n\tldconfig /etc/ld.so.conf.d && \\\n\t\tmkdir -p /etc/frankenphp/php.d && \\\n\t\t\tcp php.ini-development /etc/frankenphp/php.ini && \\\n\t\t\techo \"zend_extension=opcache.so\" >> /etc/frankenphp/php.ini && \\\n\t\t\techo \"opcache.enable=1\" >> /etc/frankenphp/php.ini && \\\n\tphp --version\n\n# Install e-dant/watcher (necessary for file watching)\nWORKDIR /usr/local/src/watcher\nRUN git clone https://github.com/e-dant/watcher . && \\\n\t\tcmake -S . -B build -DCMAKE_BUILD_TYPE=Release && \\\n\tcmake --build build/ && \\\n\tcmake --install build\n\nWORKDIR /go/src/app\nCOPY . .\n\nWORKDIR /go/src/app/caddy/frankenphp\nRUN ../../go.sh build -buildvcs=false\n\nWORKDIR /go/src/app\nCMD [ \"zsh\" ]\n"
  },
  {
    "path": "dev.Dockerfile",
    "content": "# syntax=docker/dockerfile:1\n#checkov:skip=CKV_DOCKER_2\n#checkov:skip=CKV_DOCKER_3\nFROM golang:1.26\n\nENV GOTOOLCHAIN=local\nENV CFLAGS=\"-ggdb3\"\nENV PHPIZE_DEPS=\"\\\n\tautoconf \\\n\tdpkg-dev \\\n\tfile \\\n\tg++ \\\n\tgcc \\\n\tlibc-dev \\\n\tmake \\\n\tpkg-config \\\n\tre2c\"\n\nSHELL [\"/bin/bash\", \"-o\", \"pipefail\", \"-c\"]\n\n# hadolint ignore=DL3009\nRUN apt-get update && \\\n\tapt-get -y --no-install-recommends install \\\n\t$PHPIZE_DEPS \\\n\tlibargon2-dev \\\n\tlibbrotli-dev \\\n\tlibcurl4-openssl-dev \\\n\tlibonig-dev \\\n\tlibreadline-dev \\\n\tlibsodium-dev \\\n\tlibsqlite3-dev \\\n\tlibssl-dev \\\n\tlibxml2-dev \\\n\tzlib1g-dev \\\n\tbison \\\n\tlibnss3-tools \\\n\t# Dev tools \\\n\tgit \\\n\tclang \\\n\tcmake \\\n\tllvm \\\n\tgdb \\\n\tvalgrind \\\n\tneovim \\\n\tzsh \\\n\tlibtool-bin && \\\n\techo 'set auto-load safe-path /' > /root/.gdbinit && \\\n\techo '* soft core unlimited' >> /etc/security/limits.conf \\\n\t&& \\\n\tapt-get clean\n\nWORKDIR /usr/local/src/php\nRUN git clone --branch=PHP-8.5 https://github.com/php/php-src.git . && \\\n\t# --enable-embed is only necessary to generate libphp.so, we don't use this SAPI directly\n\t./buildconf --force && \\\n\tEXTENSION_DIR=/usr/lib/frankenphp/modules ./configure \\\n\t\t--enable-embed \\\n\t\t--enable-zts \\\n\t\t--disable-zend-signals \\\n\t\t--enable-zend-max-execution-timers \\\n\t\t--with-config-file-path=/etc/frankenphp/php.ini \\\n\t\t--with-config-file-scan-dir=/etc/frankenphp/php.d \\\n\t\t--enable-debug && \\\n\tmake -j\"$(nproc)\" && \\\n\tmake install && \\\n\tldconfig && \\\n\t\tmkdir -p /etc/frankenphp/php.d && \\\n\t\t\tcp php.ini-development /etc/frankenphp/php.ini && \\\n\t\t\techo \"zend_extension=opcache.so\" >> /etc/frankenphp/php.ini && \\\n\t\t\techo \"opcache.enable=1\" >> /etc/frankenphp/php.ini && \\\n\tphp --version\n\n# Install e-dant/watcher (necessary for file watching)\nWORKDIR /usr/local/src/watcher\nRUN git clone https://github.com/e-dant/watcher . && \\\n\tcmake -S . -B build -DCMAKE_BUILD_TYPE=Release && \\\n\tcmake --build build/ && \\\n\tcmake --install build && \\\n\tcp build/libwatcher-c.so /usr/local/lib/libwatcher-c.so && \\\n\tldconfig\n\nWORKDIR /go/src/app\nCOPY --link . ./\n\nWORKDIR /go/src/app/caddy/frankenphp\nRUN ../../go.sh build -buildvcs=false\n\nWORKDIR /go/src/app\nCMD [ \"zsh\" ]\n"
  },
  {
    "path": "docker-bake.hcl",
    "content": "variable \"IMAGE_NAME\" {\n    default = \"dunglas/frankenphp\"\n}\n\nvariable \"VERSION\" {\n    default = \"dev\"\n}\n\nvariable \"PHP_VERSION\" {\n    default = \"8.2,8.3,8.4,8.5\"\n}\n\nvariable \"GO_VERSION\" {\n    default = \"1.26\"\n}\n\nvariable \"BASE_FINGERPRINT\" {\n    default = \"\"\n}\n\nvariable \"SPC_OPT_BUILD_ARGS\" {\n    default = \"\"\n}\n\nvariable \"SHA\" {}\n\nvariable \"LATEST\" {\n    default = true\n}\n\nvariable \"CACHE\" {\n    default = \"\"\n}\n\nvariable \"CI\" {\n    # CI flag coming from the environment or --set; empty by default\n    default = \"\"\n}\n\nvariable DEFAULT_PHP_VERSION {\n    default = \"8.5\"\n}\n\nfunction \"tag\" {\n    params = [version, os, php-version, tgt]\n    result = [\n        version == \"\" ? \"\" : \"${IMAGE_NAME}:${trimprefix(\"${version}${tgt == \"builder\" ? \"-builder\" : \"\"}-php${php-version}-${os}\", \"latest-\")}\",\n        php-version == DEFAULT_PHP_VERSION && os == \"trixie\" && version != \"\" ? \"${IMAGE_NAME}:${trimprefix(\"${version}${tgt == \"builder\" ? \"-builder\" : \"\"}\", \"latest-\")}\" : \"\",\n        php-version == DEFAULT_PHP_VERSION && version != \"\" ? \"${IMAGE_NAME}:${trimprefix(\"${version}${tgt == \"builder\" ? \"-builder\" : \"\"}-${os}\", \"latest-\")}\" : \"\",\n        os == \"trixie\" && version != \"\" ? \"${IMAGE_NAME}:${trimprefix(\"${version}${tgt == \"builder\" ? \"-builder\" : \"\"}-php${php-version}\", \"latest-\")}\" : \"\",\n    ]\n}\n\n# cleanTag ensures that the tag is a valid Docker tag\n# cleanTag ensures that the tag is a valid Docker tag\n# see https://github.com/distribution/distribution/blob/v2.8.2/reference/regexp.go#L37\nfunction \"clean_tag\" {\n    params = [tag]\n    result = substr(regex_replace(regex_replace(tag, \"[^\\\\w.-]\", \"-\"), \"^([^\\\\w])\", \"r$0\"), 0, 127)\n}\n\n# semver adds semver-compliant tag if a semver version number is passed, or returns the revision itself\n# see https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string\nfunction \"semver\" {\n  params = [rev]\n  result = __semver(_semver(regexall(\"^v?(?P<major>0|[1-9]\\\\d*)\\\\.(?P<minor>0|[1-9]\\\\d*)\\\\.(?P<patch>0|[1-9]\\\\d*)(?:-(?P<prerelease>(?:0|[1-9]\\\\d*|\\\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\\\.(?:0|[1-9]\\\\d*|\\\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\\\+(?P<buildmetadata>[0-9a-zA-Z-]+(?:\\\\.[0-9a-zA-Z-]+)*))?$\", rev)))\n}\n\nfunction \"_semver\" {\n    params = [matches]\n    result = length(matches) == 0 ? {} : matches[0]\n}\n\nfunction \"__semver\" {\n    params = [v]\n    result = v == {} ? [clean_tag(VERSION)] : v.prerelease == null ? [v.major, \"${v.major}.${v.minor}\", \"${v.major}.${v.minor}.${v.patch}\"] : [\"${v.major}.${v.minor}.${v.patch}-${v.prerelease}\"]\n}\n\nfunction \"php_version\" {\n    params = [v]\n    result = _php_version(v, regexall(\"(?P<major>\\\\d+)\\\\.(?P<minor>\\\\d+)\", v)[0])\n}\n\nfunction \"_php_version\" {\n    params = [v, m]\n    result = \"${m.major}.${m.minor}\" == DEFAULT_PHP_VERSION ? [v, \"${m.major}.${m.minor}\", \"${m.major}\"] : [v, \"${m.major}.${m.minor}\"]\n}\n\ntarget \"default\" {\n    name = \"${tgt}-php-${replace(php-version, \".\", \"-\")}-${os}\"\n    matrix = {\n        os = [\"trixie\", \"bookworm\", \"alpine\"]\n        php-version = split(\",\", PHP_VERSION)\n        tgt = [\"builder\", \"runner\"]\n    }\n    contexts = {\n        php-base = \"docker-image://php:${php-version}-zts-${os}\"\n        golang-base = \"docker-image://golang:${GO_VERSION}-${os}\"\n    }\n    dockerfile = os == \"alpine\" ? \"alpine.Dockerfile\" : \"Dockerfile\"\n    context = \"./\"\n    target = tgt\n    # arm/v6 is only available for Alpine: https://github.com/docker-library/golang/issues/502\n    platforms = os == \"alpine\" ? [\n        \"linux/amd64\",\n        \"linux/386\",\n        \"linux/arm/v6\",\n        \"linux/arm/v7\",\n        \"linux/arm64\",\n    ] : [\n        \"linux/amd64\",\n        \"linux/386\",\n        \"linux/arm/v7\",\n        \"linux/arm64\"\n    ]\n    tags = distinct(flatten(\n        [for pv in php_version(php-version) : flatten([\n            LATEST ? tag(\"latest\", os, pv, tgt) : [],\n            tag(SHA == \"\" || VERSION != \"dev\" ? \"\" : \"sha-${substr(SHA, 0, 7)}\", os, pv, tgt),\n            VERSION == \"dev\" ? [] : [for v in semver(VERSION) : tag(v, os, pv, tgt)]\n        ])\n    ]))\n    labels = {\n        \"org.opencontainers.image.created\" = \"${timestamp()}\"\n        \"org.opencontainers.image.version\" = VERSION\n        \"org.opencontainers.image.revision\" = SHA\n        \"dev.frankenphp.base.fingerprint\" = BASE_FINGERPRINT\n    }\n    args = {\n        FRANKENPHP_VERSION = VERSION\n    }\n    secret = [\"id=github-token,env=GITHUB_TOKEN\"]\n}\n\ntarget \"static-builder-musl\" {\n    contexts = {\n        golang-base = \"docker-image://golang:${GO_VERSION}-alpine\"\n    }\n    dockerfile = \"static-builder-musl.Dockerfile\"\n    context = \"./\"\n    platforms = [\n        \"linux/amd64\",\n        \"linux/arm64\",\n    ]\n    tags = distinct(flatten([\n        LATEST ? \"${IMAGE_NAME}:static-builder-musl\" : \"\",\n        SHA == \"\" || VERSION != \"dev\" ? \"\" : \"${IMAGE_NAME}:static-builder-musl-sha-${substr(SHA, 0, 7)}\",\n        VERSION == \"dev\" ? [] : [for v in semver(VERSION) : \"${IMAGE_NAME}:static-builder-musl-${v}\"]\n    ]))\n    labels = {\n        \"org.opencontainers.image.created\" = \"${timestamp()}\"\n        \"org.opencontainers.image.version\" = VERSION\n        \"org.opencontainers.image.revision\" = SHA\n        \"dev.frankenphp.base.fingerprint\" = BASE_FINGERPRINT\n    }\n    args = {\n        FRANKENPHP_VERSION = VERSION\n        CI = CI\n        SPC_OPT_BUILD_ARGS = SPC_OPT_BUILD_ARGS\n    }\n    secret = [\"id=github-token,env=GITHUB_TOKEN\"]\n}\n\ntarget \"static-builder-gnu\" {\n    dockerfile = \"static-builder-gnu.Dockerfile\"\n    context = \"./\"\n    platforms = [\n        \"linux/amd64\",\n        \"linux/arm64\"\n    ]\n    tags = distinct(flatten([\n        LATEST ? \"${IMAGE_NAME}:static-builder-gnu\" : \"\",\n        SHA == \"\" || VERSION != \"dev\" ? \"\" : \"${IMAGE_NAME}:static-builder-gnu-sha-${substr(SHA, 0, 7)}\",\n        VERSION == \"dev\" ? [] : [for v in semver(VERSION) : \"${IMAGE_NAME}:static-builder-gnu-${v}\"]\n    ]))\n    labels = {\n        \"org.opencontainers.image.created\" = \"${timestamp()}\"\n        \"org.opencontainers.image.version\" = VERSION\n        \"org.opencontainers.image.revision\" = SHA\n        \"dev.frankenphp.base.fingerprint\" = BASE_FINGERPRINT\n    }\n    args = {\n        FRANKENPHP_VERSION = VERSION\n        GO_VERSION = GO_VERSION\n        CI = CI\n        SPC_OPT_BUILD_ARGS = SPC_OPT_BUILD_ARGS\n    }\n    secret = [\"id=github-token,env=GITHUB_TOKEN\"]\n}\n"
  },
  {
    "path": "docs/classic.md",
    "content": "# Using Classic Mode\n\nWithout any additional configuration, FrankenPHP operates in classic mode. In this mode, FrankenPHP functions like a traditional PHP server, directly serving PHP files. This makes it a seamless drop-in replacement for PHP-FPM or Apache with mod_php.\n\nSimilar to Caddy, FrankenPHP accepts an unlimited number of connections and uses a [fixed number of threads](config.md#caddyfile-config) to serve them. The number of accepted and queued connections is limited only by the available system resources.\nThe PHP thread pool operates with a fixed number of threads initialized at startup, comparable to the static mode of PHP-FPM. It's also possible to let threads [scale automatically at runtime](performance.md#max_threads), similar to the dynamic mode of PHP-FPM.\n\nQueued connections will wait indefinitely until a PHP thread is available to serve them. To avoid this, you can use the max_wait_time [configuration](config.md#caddyfile-config) in FrankenPHP's global configuration to limit the duration a request can wait for a free PHP thread before being rejected.\nAdditionally, you can set a reasonable [write timeout in Caddy](https://caddyserver.com/docs/caddyfile/options#timeouts).\n\nEach Caddy instance will only spin up one FrankenPHP thread pool, which will be shared across all `php_server` blocks.\n"
  },
  {
    "path": "docs/cn/CONTRIBUTING.md",
    "content": "# 贡献\n\n## 编译 PHP\n\n### 使用 Docker (Linux)\n\n构建开发环境 Docker 镜像：\n\n```console\ndocker build -t frankenphp-dev -f dev.Dockerfile .\ndocker run --cap-add=SYS_PTRACE --security-opt seccomp=unconfined -p 8080:8080 -p 443:443 -p 443:443/udp -v $PWD:/go/src/app -it frankenphp-dev\n```\n\n该镜像包含常用的开发工具（Go、GDB、Valgrind、Neovim等）并使用以下 php 设置位置\n\n- php.ini: `/etc/frankenphp/php.ini` 默认提供了一个带有开发预设的 php.ini 文件。\n- 附加配置文件: `/etc/frankenphp/php.d/*.ini`\n- php 扩展: `/usr/lib/frankenphp/modules/`\n\n如果你的 Docker 版本低于 23.0，则会因为 dockerignore [pattern issue](https://github.com/moby/moby/pull/42676) 而导致构建失败。将目录添加到 `.dockerignore`。\n\n```patch\n !testdata/*.php\n !testdata/*.txt\n+!caddy\n+!internal\n```\n\n### 不使用 Docker (Linux 和 macOS)\n\n[按照说明从源代码编译](https://frankenphp.dev/docs/compile/) 并传递 `--debug` 配置标志。\n\n## 运行测试套件\n\n```console\ngo test -race -v ./...\n```\n\n## Caddy 模块\n\n使用 FrankenPHP Caddy 模块构建 Caddy：\n\n```console\ncd caddy/frankenphp/\ngo build -tags nobadger,nomysql,nopgx\ncd ../../\n```\n\n使用 FrankenPHP Caddy 模块运行 Caddy：\n\n```console\ncd testdata/\n../caddy/frankenphp/frankenphp run\n```\n\n服务器正在监听 `127.0.0.1:80`：\n\n> [!NOTE]\n> 如果您正在使用 Docker，您必须绑定容器的 80 端口或者在容器内部执行命令。\n\n```console\ncurl -vk http://127.0.0.1/phpinfo.php\n```\n\n## 最小测试服务器\n\n构建最小测试服务器：\n\n```console\ncd internal/testserver/\ngo build\ncd ../../\n```\n\n运行测试服务器：\n\n```console\ncd testdata/\n../internal/testserver/testserver\n```\n\n服务器正在监听 `127.0.0.1:8080`：\n\n```console\ncurl -v http://127.0.0.1:8080/phpinfo.php\n```\n\n## 本地构建 Docker 镜像\n\n打印 bake 计划:\n\n```console\ndocker buildx bake -f docker-bake.hcl --print\n```\n\n本地构建 amd64 的 FrankenPHP 镜像：\n\n```console\ndocker buildx bake -f docker-bake.hcl --pull --load --set \"*.platform=linux/amd64\"\n```\n\n本地构建 arm64 的 FrankenPHP 镜像：\n\n```console\ndocker buildx bake -f docker-bake.hcl --pull --load --set \"*.platform=linux/arm64\"\n```\n\n从头开始为 arm64 和 amd64 构建 FrankenPHP 镜像并推送到 Docker Hub：\n\n```console\ndocker buildx bake -f docker-bake.hcl --pull --no-cache --push\n```\n\n## 使用静态构建调试分段错误\n\n1. 从 GitHub 下载 FrankenPHP 二进制文件的调试版本或创建包含调试符号的自定义静态构建：\n\n   ```console\n   docker buildx bake \\\n       --load \\\n       --set static-builder.args.DEBUG_SYMBOLS=1 \\\n       --set \"static-builder.platform=linux/amd64\" \\\n       static-builder\n   docker cp $(docker create --name static-builder-musl dunglas/frankenphp:static-builder-musl):/go/src/app/dist/frankenphp-linux-$(uname -m) frankenphp\n   ```\n\n2. 将当前版本的 `frankenphp` 替换为 debug FrankenPHP 可执行文件\n3. 照常启动 FrankenPHP（或者，你可以直接使用 GDB 启动 FrankenPHP： `gdb --args frankenphp run`）\n4. 使用 GDB 附加到进程：\n\n   ```console\n   gdb -p `pidof frankenphp`\n   ```\n\n5. 如有必要，请在 GDB shell 中输入 `continue`\n6. 使 FrankenPHP 崩溃\n7. 在 GDB shell 中输入 `bt`\n8. 复制输出\n\n## 在 GitHub Actions 中调试分段错误\n\n1. 打开 `.github/workflows/tests.yml`\n2. 启用 PHP 调试符号\n\n   ```patch\n       - uses: shivammathur/setup-php@v2\n         # ...\n         env:\n           phpts: ts\n   +       debug: true\n   ```\n\n3. 启用 `tmate` 以连接到容器\n\n   ```patch\n       - name: Set CGO flags\n         run: echo \"CGO_CFLAGS=$(php-config --includes)\" >> \"$GITHUB_ENV\"\n   +   - run: |\n   +       sudo apt install gdb\n   +       mkdir -p /home/runner/.config/gdb/\n   +       printf \"set auto-load safe-path /\\nhandle SIG34 nostop noprint pass\" > /home/runner/.config/gdb/gdbinit\n   +   - uses: mxschmitt/action-tmate@v3\n   ```\n\n4. 连接到容器\n5. 打开 `frankenphp.go`\n6. 启用 `cgosymbolizer`\n\n   ```patch\n   -\t//_ \"github.com/ianlancetaylor/cgosymbolizer\"\n   +\t_ \"github.com/ianlancetaylor/cgosymbolizer\"\n   ```\n\n7. 下载模块： `go get`\n8. 在容器中，可以使用 GDB 和以下：\n\n   ```console\n   go test -c -ldflags=-w\n   gdb --args frankenphp.test -test.run ^MyTest$\n   ```\n\n9. 当错误修复后，恢复所有这些更改\n\n## 其他开发资源\n\n- [PHP 嵌入 uWSGI](https://github.com/unbit/uwsgi/blob/master/plugins/php/php_plugin.c)\n- [PHP 嵌入 NGINX Unit](https://github.com/nginx/unit/blob/master/src/nxt_php_sapi.c)\n- [PHP 嵌入 Go (go-php)](https://github.com/deuill/go-php)\n- [PHP 嵌入 Go (GoEmPHP)](https://github.com/mikespook/goemphp)\n- [PHP 嵌入 C++](https://gist.github.com/paresy/3cbd4c6a469511ac7479aa0e7c42fea7)\n- [扩展和嵌入 PHP 作者：Sara Golemon](https://books.google.fr/books?id=zMbGvK17_tYC&pg=PA254&lpg=PA254#v=onepage&q&f=false)\n- [TSRMLS_CC到底是什么？](http://blog.golemon.com/2006/06/what-heck-is-tsrmlscc-anyway.html)\n- [SDL 绑定](https://pkg.go.dev/github.com/veandco/go-sdl2@v0.4.21/sdl#Main)\n\n## Docker 相关资源\n\n- [Bake 文件定义](https://docs.docker.com/build/customize/bake/file-definition/)\n- [`docker buildx build`](https://docs.docker.com/engine/reference/commandline/buildx_build/)\n\n## 有用的命令\n\n```console\napk add strace util-linux gdb\nstrace -e 'trace=!futex,epoll_ctl,epoll_pwait,tgkill,rt_sigreturn' -p 1\n```\n\n## 翻译文档\n\n要将文档和网站翻译成新语言，请按照下列步骤操作：\n\n1. 在此存储库的 `docs/` 目录中创建一个以语言的 2 个字符的 ISO 代码命名的新目录\n2. 将 `docs/` 目录根目录中的所有 `.md` 文件复制到新目录中（始终使用英文版本作为翻译源，因为它始终是最新的）\n3. 将 `README.md` 和 `CONTRIBUTING.md` 文件从根目录复制到新目录\n4. 翻译文件的内容，但不要更改文件名，也不要翻译以 `> [!` 开头的字符串（这是 GitHub 的特殊标记）\n5. 创建翻译的拉取请求\n6. 在 [站点存储库](https://github.com/dunglas/frankenphp-website/tree/main) 中，复制并翻译 `content/`、`data/` 和 `i18n/` 目录中的翻译文件\n7. 转换创建的 YAML 文件中的值\n8. 在站点存储库上打开拉取请求\n"
  },
  {
    "path": "docs/cn/README.md",
    "content": "# FrankenPHP: 适用于 PHP 的现代应用服务器\n\n<h1 align=\"center\"><a href=\"https://frankenphp.dev\"><img src=\"../../frankenphp.png\" alt=\"FrankenPHP\" width=\"600\"></a></h1>\n\nFrankenPHP 是建立在 [Caddy](https://caddyserver.com/) Web 服务器之上的现代 PHP 应用程序服务器。\n\nFrankenPHP 凭借其令人惊叹的功能为你的 PHP 应用程序提供了超能力：[早期提示](early-hints.md)、[worker 模式](worker.md)、[实时功能](mercure.md)、自动 HTTPS、HTTP/2 和 HTTP/3 支持......\n\nFrankenPHP 可与任何 PHP 应用程序一起使用，并且由于提供了与 worker 模式的集成，使你的 Symfony 和 Laravel 项目比以往任何时候都更快。\n\nFrankenPHP 也可以用作独立的 Go 库，将 PHP 嵌入到任何使用 `net/http` 的应用程序中。\n\n[**了解更多** _frankenphp.dev_](https://frankenphp.dev/cn/) 以及查看此演示文稿：\n\n<a href=\"https://dunglas.dev/2022/10/frankenphp-the-modern-php-app-server-written-in-go/\"><img src=\"https://dunglas.dev/wp-content/uploads/2022/10/frankenphp.png\" alt=\"Slides\" width=\"600\"></a>\n\n## 开始\n\n在 Windows 上，请使用 [WSL](https://learn.microsoft.com/windows/wsl/) 运行 FrankenPHP。\n\n### 安装脚本\n\n你可以将以下命令复制到终端中，自动安装适用于你平台的版本：\n\n```console\ncurl https://frankenphp.dev/install.sh | sh\n```\n\n### 独立二进制\n\n我们为 Linux 和 macOS 提供用于开发的 FrankenPHP 静态二进制文件，\n包含 [PHP 8.4](https://www.php.net/releases/8.4/zh.php) 以及大多数常用 PHP 扩展。\n\n[下载 FrankenPHP](https://github.com/dunglas/frankenphp/releases)\n\n**安装扩展：** 常见扩展已内置，无法再安装更多扩展。\n\n### rpm 软件包\n\n我们的维护者为所有使用 `dnf` 的系统提供 rpm 包。安装方式：\n\n```console\nsudo dnf install https://rpm.henderkes.com/static-php-1-0.noarch.rpm\nsudo dnf module enable php-zts:static-8.4 # 可用 8.2-8.5\nsudo dnf install frankenphp\n```\n\n**安装扩展：** `sudo dnf install php-zts-<extension>`\n\n对于默认不可用的扩展，请使用 [PIE](https://github.com/php/pie)：\n\n```console\nsudo dnf install pie-zts\nsudo pie-zts install asgrim/example-pie-extension\n```\n\n### deb 软件包\n\n我们的维护者为所有使用 `apt` 的系统提供 deb 包。安装方式：\n\n```console\nsudo curl -fsSL https://key.henderkes.com/static-php.gpg -o /usr/share/keyrings/static-php.gpg && \\\necho \"deb [signed-by=/usr/share/keyrings/static-php.gpg] https://deb.henderkes.com/ stable main\" | sudo tee /etc/apt/sources.list.d/static-php.list && \\\nsudo apt update\nsudo apt install frankenphp\n```\n\n**安装扩展：** `sudo apt install php-zts-<extension>`\n\n对于默认不可用的扩展，请使用 [PIE](https://github.com/php/pie)：\n\n```console\nsudo apt install pie-zts\nsudo pie-zts install asgrim/example-pie-extension\n```\n\n### Docker\n\n此外，还可以使用 [Docker 镜像](https://frankenphp.dev/docs/docker/)：\n\n```console\ndocker run -v .:/app/public \\\n    -p 80:80 -p 443:443 -p 443:443/udp \\\n    dunglas/frankenphp\n```\n\n访问 `https://localhost`, 并享受吧!\n\n> [!TIP]\n>\n> 不要尝试使用 `https://127.0.0.1`。使用 `https://localhost` 并接受自签名证书。\n> 使用 [`SERVER_NAME` 环境变量](config.md#environment-variables) 更改要使用的域。\n\n### Homebrew\n\nFrankenPHP 也作为 [Homebrew](https://brew.sh) 软件包提供，适用于 macOS 和 Linux 系统。\n\n安装方法：\n\n```console\nbrew install dunglas/frankenphp/frankenphp\n```\n\n**安装扩展：** 使用 [PIE](https://github.com/php/pie)。\n\n### 用法\n\n要提供当前目录的内容，请运行：\n\n```console\nfrankenphp php-server\n```\n\n你还可以使用以下命令运行命令行脚本：\n\n```console\nfrankenphp php-cli /path/to/your/script.php\n```\n\n对于 deb 和 rpm 软件包，还可以启动 systemd 服务：\n\n```console\nsudo systemctl start frankenphp\n```\n\n## 文档\n\n- [Classic 模式](classic.md)\n- [worker 模式](worker.md)\n- [早期提示支持(103 HTTP status code)](early-hints.md)\n- [实时功能](mercure.md)\n- [高效地服务大型静态文件](x-sendfile.md)\n- [配置](config.md)\n- [用 Go 编写 PHP 扩展](extensions.md)\n- [Docker 镜像](docker.md)\n- [在生产环境中部署](production.md)\n- [性能优化](performance.md)\n- [创建独立、可自行执行的 PHP 应用程序](embed.md)\n- [创建静态二进制文件](static.md)\n- [从源代码编译](compile.md)\n- [Laravel 集成](laravel.md)\n- [已知问题](known-issues.md)\n- [演示应用程序 (Symfony) 和性能测试](https://github.com/dunglas/frankenphp-demo)\n- [Go 库文档](https://pkg.go.dev/github.com/dunglas/frankenphp)\n- [贡献和调试](https://frankenphp.dev/docs/contributing/)\n\n## 示例和框架\n\n- [Symfony](https://github.com/dunglas/symfony-docker)\n- [API Platform](https://api-platform.com/docs/distribution/)\n- [Laravel](laravel.md)\n- [Sulu](https://sulu.io/blog/running-sulu-with-frankenphp)\n- [WordPress](https://github.com/StephenMiracle/frankenwp)\n- [Drupal](https://github.com/dunglas/frankenphp-drupal)\n- [Joomla](https://github.com/alexandreelise/frankenphp-joomla)\n- [TYPO3](https://github.com/ochorocho/franken-typo3)\n- [Magento2](https://github.com/ekino/frankenphp-magento2)\n"
  },
  {
    "path": "docs/cn/classic.md",
    "content": "# 使用经典模式\n\n在没有任何额外配置的情况下，FrankenPHP 以经典模式运行。在此模式下，FrankenPHP 的功能类似于传统的 PHP 服务器，直接提供 PHP 文件服务。这使其成为 PHP-FPM 或 Apache with mod_php 的无缝替代品。\n\n与 Caddy 类似，FrankenPHP 接受无限数量的连接，并使用[固定数量的线程](config.md#caddyfile-配置)来为它们提供服务。接受和排队的连接数量仅受可用系统资源的限制。\nPHP 线程池使用在启动时初始化的固定数量的线程运行，类似于 PHP-FPM 的静态模式。也可以让线程在[运行时自动扩展](performance.md#max_threads)，类似于 PHP-FPM 的动态模式。\n\n排队的连接将无限期等待，直到有 PHP 线程可以为它们提供服务。为了避免这种情况，你可以在 FrankenPHP 的全局配置中使用 max_wait_time [配置](config.md#caddyfile-配置)来限制请求可以等待空闲的 PHP 线程的时间，超时后将被拒绝。\n此外，你还可以在 Caddy 中设置合理的[写超时](https://caddyserver.com/docs/caddyfile/options#timeouts)。\n\n每个 Caddy 实例只会启动一个 FrankenPHP 线程池，该线程池将在所有 `php_server` 块之间共享。\n"
  },
  {
    "path": "docs/cn/compile.md",
    "content": "# 从源代码编译\n\n本文档解释了如何创建一个 FrankenPHP 构建，它将 PHP 加载为一个动态库。\n这是推荐的方法。\n\n或者，你也可以 [编译静态版本](static.md)。\n\n## 安装 PHP\n\nFrankenPHP 支持 PHP 8.2 及更高版本。\n\n### 使用 Homebrew (Linux 和 Mac)\n\n安装与 FrankenPHP 兼容的 libphp 版本的最简单方法是使用 [Homebrew PHP](https://github.com/shivammathur/homebrew-php) 提供的 ZTS 包。\n\n首先，如果尚未安装，请安装 [Homebrew](https://brew.sh)。\n\n然后，安装 PHP 的 ZTS 变体、Brotli（可选，用于压缩支持）和 watcher（可选，用于文件更改检测）：\n\n```console\nbrew install shivammathur/php/php-zts brotli watcher\nbrew link --overwrite --force shivammathur/php/php-zts\n```\n\n### 通过编译 PHP\n\n或者，你可以按照以下步骤，使用 FrankenPHP 所需的选项从源代码编译 PHP。\n\n首先，[获取 PHP 源代码](https://www.php.net/downloads.php) 并提取它们：\n\n```console\ntar xf php-*\ncd php-*/\n```\n\n然后，运行适用于你平台的 `configure` 脚本。\n以下 `./configure` 标志是必需的，但你可以添加其他标志，例如编译扩展或附加功能。\n\n#### Linux\n\n```console\n./configure \\\n    --enable-embed \\\n    --enable-zts \\\n    --disable-zend-signals \\\n    --enable-zend-max-execution-timers\n```\n\n#### Mac\n\n使用 [Homebrew](https://brew.sh/) 包管理器安装所需的和可选的依赖项：\n\n```console\nbrew install libiconv bison brotli re2c pkg-config watcher\necho 'export PATH=\"/opt/homebrew/opt/bison/bin:$PATH\"' >> ~/.zshrc\n```\n\n然后运行 `./configure` 脚本：\n\n```console\n./configure \\\n    --enable-embed \\\n    --enable-zts \\\n    --disable-zend-signals \\\n    --with-iconv=/opt/homebrew/opt/libiconv/\n```\n\n#### 编译 PHP\n\n最后，编译并安装 PHP：\n\n```console\nmake -j\"$(getconf _NPROCESSORS_ONLN)\"\nsudo make install\n```\n\n## 安装可选依赖项\n\n某些 FrankenPHP 功能依赖于必须安装的可选系统依赖项。\n或者，可以通过向 Go 编译器传递构建标签来禁用这些功能。\n\n| 功能                  | 依赖项                                                                | 用于禁用的构建标签 |\n| --------------------- | --------------------------------------------------------------------- | ------------------ |\n| Brotli 压缩           | [Brotli](https://github.com/google/brotli)                            | nobrotli           |\n| 文件更改时重启 worker | [Watcher C](https://github.com/e-dant/watcher/tree/release/watcher-c) | nowatcher          |\n\n## 编译 Go 应用\n\n你现在可以构建最终的二进制文件。\n\n### 使用 xcaddy\n\n推荐的方法是使用 [xcaddy](https://github.com/caddyserver/xcaddy) 来编译 FrankenPHP。\n`xcaddy` 还允许轻松添加 [自定义 Caddy 模块](https://caddyserver.com/docs/modules/) 和 FrankenPHP 扩展：\n\n```console\nCGO_ENABLED=1 \\\nXCADDY_GO_BUILD_FLAGS=\"-ldflags='-w -s' -tags=nobadger,nomysql,nopgx\" \\\nCGO_CFLAGS=$(php-config --includes) \\\nCGO_LDFLAGS=\"$(php-config --ldflags) $(php-config --libs)\" \\\nxcaddy build \\\n    --output frankenphp \\\n    --with github.com/dunglas/frankenphp/caddy \\\n    --with github.com/dunglas/mercure/caddy \\\n    --with github.com/dunglas/vulcain/caddy\n    # 在这里添加额外的 Caddy 模块和 FrankenPHP 扩展\n```\n\n> [!TIP]\n>\n> 如果你的系统基于 musl libc（Alpine Linux 上默认使用）并搭配 Symfony 使用，\n> 你可能需要增加默认堆栈大小。\n> 否则，你可能会收到如下错误 `PHP Fatal error: Maximum call stack size of 83360 bytes reached during compilation. Try splitting expression`\n>\n> 请将 `XCADDY_GO_BUILD_FLAGS` 环境变量更改为如下类似的值\n> `XCADDY_GO_BUILD_FLAGS=$'-ldflags \"-w -s -extldflags \\'-Wl,-z,stack-size=0x80000\\'\"'`\n> （根据你的应用需求更改堆栈大小）。\n\n### 不使用 xcaddy\n\n或者，可以通过直接使用 `go` 命令来编译 FrankenPHP 而不使用 `xcaddy`：\n\n```console\ncurl -L https://github.com/php/frankenphp/archive/refs/heads/main.tar.gz | tar xz\ncd frankenphp-main/caddy/frankenphp\nCGO_CFLAGS=$(php-config --includes) CGO_LDFLAGS=\"$(php-config --ldflags) $(php-config --libs)\" go build -tags=nobadger,nomysql,nopgx\n```\n"
  },
  {
    "path": "docs/cn/config.md",
    "content": "# 配置\n\nFrankenPHP、Caddy 以及 [Mercure](mercure.md) 和 [Vulcain](https://vulcain.rocks) 模块可以使用 [Caddy 支持的格式](https://caddyserver.com/docs/getting-started#your-first-config) 进行配置。\n\n最常见的格式是 `Caddyfile`，它是一种简单、易读的文本格式。默认情况下，FrankenPHP 会在当前目录中查找 `Caddyfile`。你可以使用 `-c` 或 `--config` 选项指定自定义路径。\n\n以下是用于服务 PHP 应用程序的最小 `Caddyfile` 示例：\n\n```caddyfile\n# 响应的主机名\nlocalhost\n\n# 可选：提供文件的目录，否则默认为当前目录\n#root public/\nphp_server\n```\n\n一个更高级的 `Caddyfile`，支持更多功能并提供方便的环境变量，可以在 [FrankenPHP 仓库中](https://github.com/php/frankenphp/blob/main/caddy/frankenphp/Caddyfile)找到，并随 Docker 镜像提供。\n\nPHP 本身可以[使用 `php.ini` 文件](https://www.php.net/manual/en/configuration.file.php)进行配置。\n\n根据你的安装方法，FrankenPHP 和 PHP 解释器将在以下位置查找配置文件。\n\n## Docker\n\nFrankenPHP:\n\n- `/etc/frankenphp/Caddyfile`: 主配置文件\n- `/etc/frankenphp/Caddyfile.d/*.caddyfile`: 自动加载的附加配置文件\n\nPHP:\n\n- `php.ini`: `/usr/local/etc/php/php.ini`（默认情况下不提供 `php.ini`）\n- 附加配置文件: `/usr/local/etc/php/conf.d/*.ini`\n- PHP 扩展: `/usr/local/lib/php/extensions/no-debug-zts-<YYYYMMDD>/`\n- 你应该复制 PHP 项目提供的官方模板：\n\n```dockerfile\nFROM dunglas/frankenphp\n\n# 生产环境:\nRUN cp $PHP_INI_DIR/php.ini-production $PHP_INI_DIR/php.ini\n\n# 或开发环境:\nRUN cp $PHP_INI_DIR/php.ini-development $PHP_INI_DIR/php.ini\n```\n\n## RPM 和 Debian 包\n\nFrankenPHP:\n\n- `/etc/frankenphp/Caddyfile`: 主配置文件\n- `/etc/frankenphp/Caddyfile.d/*.caddyfile`: 自动加载的附加配置文件\n\nPHP:\n\n- `php.ini`: `/etc/php-zts/php.ini`（默认情况下提供带有生产预设的 `php.ini` 文件）\n- 附加配置文件: `/etc/php-zts/conf.d/*.ini`\n\n## 静态二进制文件\n\nFrankenPHP:\n\n- 在当前工作目录: `Caddyfile`\n\nPHP:\n\n- `php.ini`: 执行 `frankenphp run` 或 `frankenphp php-server` 的目录，然后是 `/etc/frankenphp/php.ini`\n- 附加配置文件: `/etc/frankenphp/php.d/*.ini`\n- PHP 扩展: 无法加载，将它们打包在二进制文件本身中\n- 复制 [PHP 源代码](https://github.com/php/php-src/) 中提供的 `php.ini-production` 或 `php.ini-development` 中的一个。\n\n## Caddyfile 配置\n\n可以在站点块中使用 `php_server` 或 `php` [HTTP 指令](https://caddyserver.com/docs/caddyfile/concepts#directives) 来为你的 PHP 应用程序提供服务。\n\n最小示例：\n\n```caddyfile\nlocalhost {\n\t# 启用压缩（可选）\n\tencode zstd br gzip\n\t# 在当前目录中执行 PHP 文件并提供资源服务\n\tphp_server\n}\n```\n\n你还可以使用 `frankenphp` [全局选项](https://caddyserver.com/docs/caddyfile/concepts#global-options) 显式配置 FrankenPHP：\n\n```caddyfile\n{\n\tfrankenphp {\n\t\tnum_threads <num_threads> # 设置要启动的 PHP 线程数量。默认：可用 CPU 数量的 2 倍。\n\t\tmax_threads <num_threads> # 限制可以在运行时启动的额外 PHP 线程的数量。默认值：num_threads。可以设置为 'auto'。\n\t\tmax_wait_time <duration> # 设置请求在超时之前可以等待的最大时间，直到找到一个空闲的 PHP 线程。 默认：禁用。\n\t\tmax_idle_time <duration> # 设置一个自动扩展的线程在被停用之前可以空闲的最长时间。默认：5s。\n\t\tphp_ini <key> <value> # 设置一个 php.ini 指令。可以多次使用以设置多个指令。\n\t\tworker {\n\t\t\tfile <path> # 设置工作脚本的路径。\n\t\t\tnum <num> # 设置要启动的 PHP 线程数量，默认为可用 CPU 数量的 2 倍。\n\t\t\tenv <key> <value> # 设置一个额外的环境变量为给定的值。可以多次指定以设置多个环境变量。\n\t\t\twatch <path> # 设置要监视文件更改的路径。可以为多个路径多次指定。\n\t\t\tname <name> # 设置worker的名称，用于日志和指标。默认值：worker文件的绝对路径。\n\t\t\tmax_consecutive_failures <num> # 设置在工人被视为不健康之前的最大连续失败次数，-1意味着工人将始终重新启动。默认值：6。\n\t\t}\n\t}\n}\n\n# ...\n```\n\n或者，您可以使用 `worker` 选项的一行简短形式：\n\n```caddyfile\n{\n\tfrankenphp {\n\t\tworker <file> <num>\n\t}\n}\n\n# ...\n```\n\n如果您在同一服务器上服务多个应用程序，您还可以定义多个工作线程：\n\n```caddyfile\napp.example.com {\n    root /path/to/app/public\n\tphp_server {\n\t\troot /path/to/app/public # 允许更好的缓存\n\t\tworker index.php <num>\n\t}\n}\n\nother.example.com {\n    root /path/to/other/public\n\tphp_server {\n\t\troot /path/to/other/public\n\t\tworker index.php <num>\n\t}\n}\n\n# ...\n```\n\n使用 `php_server` 指令通常是您需要的，\n但是如果你需要完全控制，你可以使用更低级的 `php` 指令。\n`php` 指令将所有输入传递给 PHP，而不是先检查是否\n是一个PHP文件。在[性能页面](performance.md#try_files)中了解更多关于它的信息。\n\n使用 `php_server` 指令等同于以下配置：\n\n```caddyfile\nroute {\n\t# 为目录请求添加尾斜杠\n\t@canonicalPath {\n\t\tfile {path}/index.php\n\t\tnot path */\n\t}\n\tredir @canonicalPath {path}/ 308\n\t# 如果请求的文件不存在，则尝试 index 文件\n\t@indexFiles file {\n\t\ttry_files {path} {path}/index.php index.php\n\t\tsplit_path .php\n\t}\n\trewrite @indexFiles {http.matchers.file.relative}\n\t# FrankenPHP!\n\t@phpFiles path *.php\n\tphp @phpFiles\n\tfile_server\n}\n```\n\n`php_server` 和 `php` 指令有以下选项：\n\n```caddyfile\nphp_server [<matcher>] {\n\troot <directory> # 将根文件夹设置为站点。默认值：`root` 指令。\n\tsplit_path <delim...> # 设置用于将 URI 分割成两部分的子字符串。第一个匹配的子字符串将用来将 \"路径信息\" 与路径分开。第一部分后缀为匹配的子字符串，并将被视为实际资源（CGI 脚本）名称。第二部分将被设置为脚本使用的 PATH_INFO。默认值：`.php`。\n\tresolve_root_symlink false # 禁用通过评估符号链接（如果存在）将 `root` 目录解析为其实际值（默认启用）。\n\tenv <key> <value> # 设置一个额外的环境变量为给定的值。可以多次指定以设置多个环境变量。\n\tfile_server off # 禁用内置的 file_server 指令。\n\tworker { # 为此服务器创建特定的worker。可以多次指定以创建多个workers。\n\t\tfile <path> # 设置工作脚本的路径，可以相对于 php_server 根目录\n\t\tnum <num> # 设置要启动的 PHP 线程数，默认为可用 CPU 数量的 2 倍\n\t\tname <name> # 为worker设置名称，用于日志和指标。默认值：worker文件的绝对路径。定义在 php_server 块中时，始终以 m# 开头。\n\t\twatch <path> # 设置要监视文件更改的路径。可以为多个路径多次指定。\n\t\tenv <key> <value> # 设置一个额外的环境变量为给定值。可以多次指定以设置多个环境变量。此工作进程的环境变量也从 php_server 父进程继承，但可以在此处覆盖。\n\t\tmatch <path> # 将worker匹配到路径模式。覆盖 try_files，并且只能在 php_server 指令中使用。\n\t}\n\tworker <other_file> <num> # 也可以像在全局 frankenphp 块中那样使用简短形式。\n}\n```\n\n### 监控文件变化\n\n由于 workers 只会启动您的应用程序一次并将其保留在内存中，\n因此对您的 PHP 文件的任何更改不会立即反映出来。\n\nWorkers 可以通过 `watch` 指令在文件更改时重新启动。\n这对开发环境很有用。\n\n```caddyfile\n{\n\tfrankenphp {\n\t\tworker {\n\t\t\tfile  /path/to/app/public/worker.php\n\t\t\twatch\n\t\t}\n\t}\n}\n```\n\n此功能通常与[热重载](hot-reload.md)结合使用。\n\n如果没有指定 `watch` 目录，它将回退到 `./**/*.{env,php,twig,yaml,yml}`，\n这将监视启动 FrankenPHP 进程的目录及其子目录中的所有 `.env`、`.php`、`.twig`、`.yaml` 和 `.yml` 文件。\n你也可以通过 [shell 文件名模式](https://pkg.go.dev/path/filepath#Match) 指定一个或多个目录：\n\n```caddyfile\n{\n\tfrankenphp {\n\t\tworker {\n\t\t\tfile  /path/to/app/public/worker.php\n\t\t\twatch /path/to/app # 监视 /path/to/app 所有子目录中的所有文件\n\t\t\twatch /path/to/app/*.php # 监视位于/path/to/app中的以.php结尾的文件\n\t\t\twatch /path/to/app/**/*.php # 监视 /path/to/app 及子目录中的 PHP 文件\n\t\t\twatch /path/to/app/**/*.{php,twig} # 在/path/to/app及其子目录中监视PHP和Twig文件\n\t\t}\n\t}\n}\n```\n\n- `**` 模式表示递归监视\n- 目录也可以是相对的（相对于FrankenPHP进程启动的位置）\n- 如果您定义了多个workers，当文件发生更改时，将重新启动所有workers。\n- 小心查看在运行时创建的文件（如日志），因为它们可能导致不必要的工作进程重启。\n\n文件监视器基于[e-dant/watcher](https://github.com/e-dant/watcher)。\n\n## 将 worker 匹配到一条路径\n\n在传统的PHP应用程序中，脚本总是放在公共目录中。\n这对于工作脚本也是如此，这些脚本被视为任何其他PHP脚本。\n如果您想将工作脚本放在公共目录外，可以通过 `match` 指令来实现。\n\n`match` 指令是 `try_files` 的一种优化替代方案，仅在 `php_server` 和 `php` 内部可用。\n以下示例将始终在公共目录中提供文件（如果存在），否则会将请求转发给与路径模式匹配的 worker。\n\n```caddyfile\n{\n\tfrankenphp {\n\t\tphp_server {\n\t\t\tworker {\n\t\t\t\tfile /path/to/worker.php # 文件可以在公共路径之外\n\t\t\t\tmatch /api/* # 所有以 /api/ 开头的请求将由此 worker 处理\n\t\t\t}\n\t\t}\n\t}\n}\n```\n\n## 环境变量\n\n可以使用以下环境变量在不修改 `Caddyfile` 的情况下注入 Caddy 指令：\n\n- `SERVER_NAME`: 更改[监听的地址](https://caddyserver.com/docs/caddyfile/concepts#addresses)，提供的宿主名也将用于生成的TLS证书。\n- `SERVER_ROOT`: 更改网站的根目录，默认为 `public/`\n- `CADDY_GLOBAL_OPTIONS`: 注入[全局选项](https://caddyserver.com/docs/caddyfile/options)\n- `FRANKENPHP_CONFIG`: 在 `frankenphp` 指令下注入配置\n\n至于 FPM 和 CLI SAPIs，环境变量默认在 `$_SERVER` 超全局中暴露。\n\n`variables_order` PHP 指令中 `S` 的值始终等于 `ES`，无论 `E` 在该指令中的其他位置如何。\n\n## PHP 配置\n\n为了加载[附加的 PHP 配置文件](https://www.php.net/manual/en/configuration.file.php#configuration.file.scan)，可以使用 `PHP_INI_SCAN_DIR` 环境变量。设置后，PHP 将加载给定目录中所有带有 `.ini` 扩展名的文件。\n\n您还可以通过在 `Caddyfile` 中使用 `php_ini` 指令来更改 PHP 配置：\n\n```caddyfile\n{\n    frankenphp {\n        php_ini memory_limit 256M\n\n        # 或者\n\n        php_ini {\n            memory_limit 256M\n            max_execution_time 15\n        }\n    }\n}\n```\n\n### 禁用 HTTPS\n\n默认情况下，FrankenPHP 会自动为所有主机名（包括 `localhost`）启用 HTTPS。\n如果你想禁用 HTTPS（例如在开发环境中），你可以将 `SERVER_NAME` 环境变量设置为 `http://` 或 `:80`：\n\n或者，你可以使用 [Caddy 文档](https://caddyserver.com/docs/automatic-https#activation) 中描述的所有其他方法。\n\n如果你想将 HTTPS 与 `127.0.0.1` IP 地址而不是 `localhost` 主机名一起使用，请阅读[已知问题](known-issues.md#using-https127001-with-docker)部分。\n\n### 全双工 (HTTP/1)\n\n在使用 HTTP/1.x 时，可能希望启用全双工模式，以便在整个请求体被读取之前允许写入响应。(例如：[Mercure](mercure.md)、WebSocket、Server-Sent Events 等)\n\n这是一个可选配置，需要添加到 `Caddyfile` 中的全局选项中：\n\n```caddyfile\n{\n  servers {\n    enable_full_duplex\n  }\n}\n```\n\n> [!CAUTION]\n>\n> 启用此选项可能导致不支持全双工的旧 HTTP/1.x 客户端死锁。\n> 这也可以通过 `CADDY_GLOBAL_OPTIONS` 环境变量配置来实现：\n\n```sh\nCADDY_GLOBAL_OPTIONS=\"servers {\n  enable_full_duplex\n}\"\n```\n\n您可以在[Caddy文档](https://caddyserver.com/docs/caddyfile/options#enable-full-duplex)中找到有关此设置的更多信息。\n\n## 启用调试模式\n\n使用Docker镜像时，将`CADDY_GLOBAL_OPTIONS`环境变量设置为`debug`以启用调试模式:\n\n```console\ndocker run -v $PWD:/app/public \\\n    -e CADDY_GLOBAL_OPTIONS=debug \\\n    -p 80:80 -p 443:443 -p 443:443/udp \\\n    dunglas/frankenphp\n```\n\n## Shell 补全\n\nFrankenPHP 提供对 Bash、Zsh、Fish 和 PowerShell 的内置 shell 补全支持。这为所有命令（包括 `php-server`、`php-cli` 和 `extension-init` 等自定义命令）及其标志启用了自动补全。\n\n### Bash\n\n要在当前 shell 会话中加载补全：\n\n```console\nsource <(frankenphp completion bash)\n```\n\n要为每个新会话加载补全，运行：\n\n**Linux:**\n\n```console\nfrankenphp completion bash > /usr/share/bash-completion/completions/frankenphp\n```\n\n**macOS:**\n\n```console\nfrankenphp completion bash > $(brew --prefix)/share/bash-completion/completions/frankenphp\n```\n\n### Zsh\n\n如果您的环境中尚未启用 shell 补全，您将需要启用它。您可以执行以下命令一次：\n\n```console\necho \"autoload -U compinit; compinit\" >> ~/.zshrc\n```\n\n要为每个会话加载补全，请执行一次：\n\n```console\nfrankenphp completion zsh > \"${fpath[1]}/_frankenphp\"\n```\n\n您需要启动一个新的 shell 会话才能使此设置生效。\n\n### Fish\n\n要在当前 shell 会话中加载补全：\n\n```console\nfrankenphp completion fish | source\n```\n\n要为每个新会话加载补全，请执行一次：\n\n```console\nfrankenphp completion fish > ~/.config/fish/completions/frankenphp.fish\n```\n\n### PowerShell\n\n要在当前 shell 会话中加载补全：\n\n```powershell\nfrankenphp completion powershell | Out-String | Invoke-Expression\n```\n\n要为每个新会话加载补全，请执行一次：\n\n```powershell\nfrankenphp completion powershell | Out-File -FilePath (Join-Path (Split-Path $PROFILE) \"frankenphp.ps1\")\nAdd-Content -Path $PROFILE -Value '. (Join-Path (Split-Path $PROFILE) \"frankenphp.ps1\")'\n```\n\n您需要启动一个新的 shell 会话才能使此设置生效。\n\n您需要启动一个新的 shell 会话才能使此设置生效。\n"
  },
  {
    "path": "docs/cn/docker.md",
    "content": "# 构建自定义 Docker 镜像\n\n[FrankenPHP Docker 镜像](https://hub.docker.com/r/dunglas/frankenphp) 基于 [官方 PHP 镜像](https://hub.docker.com/_/php/)。\n提供适用于流行架构的 Debian 和 Alpine Linux 变体。\n推荐使用 Debian 变体。\n\n提供 PHP 8.2、8.3、8.4 和 8.5 的变体。\n\n标签遵循此模式：`dunglas/frankenphp:<frankenphp-version>-php<php-version>-<os>`\n\n- `<frankenphp-version>` 和 `<php-version>` 分别是 FrankenPHP 和 PHP 的版本号，范围从主版本（例如 `1`）、次版本（例如 `1.2`）到补丁版本（例如 `1.2.3`）。\n- `<os>` 要么是 `trixie`（用于 Debian Trixie），`bookworm`（用于 Debian Bookworm），要么是 `alpine`（用于 Alpine 的最新稳定版本）。\n\n[浏览标签](https://hub.docker.com/r/dunglas/frankenphp/tags)。\n\n## 如何使用镜像\n\n在项目中创建 `Dockerfile`：\n\n```dockerfile\nFROM dunglas/frankenphp\n\nCOPY . /app/public\n```\n\n然后运行以下命令以构建并运行 Docker 镜像：\n\n```console\ndocker build -t my-php-app .\ndocker run -it --rm --name my-running-app my-php-app\n```\n\n## 如何调整配置\n\n为了方便，镜像中提供了一个包含有用环境变量的[默认 `Caddyfile`](https://github.com/php/frankenphp/blob/main/caddy/frankenphp/Caddyfile)。\n\n## 如何安装更多 PHP 扩展\n\n[`docker-php-extension-installer`](https://github.com/mlocati/docker-php-extension-installer) 脚本在基础镜像中提供。\n添加额外的 PHP 扩展很简单：\n\n```dockerfile\nFROM dunglas/frankenphp\n\n# 在此处添加其他扩展：\nRUN install-php-extensions \\\n\tpdo_mysql \\\n\tgd \\\n\tintl \\\n\tzip \\\n\topcache\n```\n\n## 如何安装更多 Caddy 模块\n\nFrankenPHP 建立在 Caddy 之上，所有 [Caddy 模块](https://caddyserver.com/docs/modules/) 都可以与 FrankenPHP 一起使用。\n\n安装自定义 Caddy 模块的最简单方法是使用 [xcaddy](https://github.com/caddyserver/xcaddy)：\n\n```dockerfile\nFROM dunglas/frankenphp:builder AS builder\n\n# 在构建器镜像中复制 xcaddy\nCOPY --from=caddy:builder /usr/bin/xcaddy /usr/bin/xcaddy\n\n# 必须启用 CGO 才能构建 FrankenPHP\nRUN CGO_ENABLED=1 \\\n    XCADDY_SETCAP=1 \\\n    XCADDY_GO_BUILD_FLAGS=\"-ldflags='-w -s' -tags=nobadger,nomysql,nopgx\" \\\n    CGO_CFLAGS=$(php-config --includes) \\\n    CGO_LDFLAGS=\"$(php-config --ldflags) $(php-config --libs)\" \\\n    xcaddy build \\\n        --output /usr/local/bin/frankenphp \\\n        --with github.com/dunglas/frankenphp=./ \\\n        --with github.com/dunglas/frankenphp/caddy=./caddy/ \\\n        --with github.com/dunglas/caddy-cbrotli \\\n        # Mercure 和 Vulcain 包含在官方版本中，如果不需要你可以删除它们\n        --with github.com/dunglas/mercure/caddy \\\n        --with github.com/dunglas/vulcain/caddy\n        # 在此处添加额外的 Caddy 模块\n\nFROM dunglas/frankenphp AS runner\n\n# 将官方二进制文件替换为包含自定义模块的二进制文件\nCOPY --from=builder /usr/local/bin/frankenphp /usr/local/bin/frankenphp\n```\n\nFrankenPHP 提供的[构建器镜像](https://hub.docker.com/r/dunglas/frankenphp/tags?name=builder)适用于所有版本的 FrankenPHP 和 PHP，同时支持 Debian 和 Alpine。\n\n> [!TIP]\n>\n> 如果你正在使用 Alpine Linux 和 Symfony，你可能需要[增加默认堆栈大小](compile.md#using-xcaddy)。\n\n## 默认启用 worker 模式\n\n设置 `FRANKENPHP_CONFIG` 环境变量以使用 worker 脚本启动 FrankenPHP：\n\n```dockerfile\nFROM dunglas/frankenphp\n\n# ...\n\nENV FRANKENPHP_CONFIG=\"worker ./public/index.php\"\n```\n\n## 在开发中使用卷\n\n要使用 FrankenPHP 轻松开发，请从包含应用程序源代码的主机挂载目录作为 Docker 容器中的卷：\n\n```console\ndocker run -v $PWD:/app/public -p 80:80 -p 443:443 -p 443:443/udp --tty my-php-app\n```\n\n> [!TIP]\n>\n> `--tty` 选项允许使用易读的日志，而不是 JSON 日志。\n\n使用 Docker Compose：\n\n```yaml\n# compose.yaml\n\nservices:\n  php:\n    image: dunglas/frankenphp\n    # 如果要使用自定义 Dockerfile，请取消注释以下行\n    #build: .\n    # 如果要在生产环境中运行，请取消注释以下行\n    # restart: always\n    ports:\n      - \"80:80\" # HTTP\n      - \"443:443\" # HTTPS\n      - \"443:443/udp\" # HTTP/3\n    volumes:\n      - ./:/app/public\n      - caddy_data:/data\n      - caddy_config:/config\n    # 在生产环境中注释以下行，它允许在开发环境中使用易读日志\n    tty: true\n\n# Caddy 证书和配置所需的数据卷\nvolumes:\n  caddy_data:\n  caddy_config:\n```\n\n## 以非 root 用户身份运行\n\nFrankenPHP 可以在 Docker 中以非 root 用户身份运行。\n\n下面是一个示例 `Dockerfile`：\n\n```dockerfile\nFROM dunglas/frankenphp\n\nARG USER=appuser\n\nRUN \\\n\t# 在基于 Alpine 的发行版使用 \"adduser -D ${USER}\"\n\tuseradd ${USER}; \\\n\t# 添加绑定到 80 和 443 端口的额外能力\n\tsetcap CAP_NET_BIND_SERVICE=+eip /usr/local/bin/frankenphp; \\\n\t# 赋予 /config/caddy 和 /data/caddy 目录的写入权限\n\tchown -R ${USER}:${USER} /config/caddy /data/caddy\n\nUSER ${USER}\n```\n\n### 在不使用能力的情况下运行\n\n即使在无根运行时，FrankenPHP 也需要 `CAP_NET_BIND_SERVICE` 能力来将\nWeb 服务器绑定到特权端口（80 和 443）。\n\n如果你在非特权端口（1024 及以上）上公开 FrankenPHP，则可以以非 root 用户身份运行\nWeb 服务器，并且不需要任何能力：\n\n```dockerfile\nFROM dunglas/frankenphp\n\nARG USER=appuser\n\nRUN \\\n\t# 在基于 Alpine 的发行版使用 \"adduser -D ${USER}\"\n\tuseradd ${USER}; \\\n\t# 移除默认能力\n\tsetcap -r /usr/local/bin/frankenphp; \\\n\t# 赋予 /config/caddy 和 /data/caddy 目录的写入权限\n\tchown -R ${USER}:${USER} /config/caddy /data/caddy\n\nUSER ${USER}\n```\n\n接下来，设置 `SERVER_NAME` 环境变量以使用非特权端口。\n示例：`:8000`\n\n## 更新\n\nDocker 镜像会在以下情况下构建：\n\n- 发布新的版本后\n- 每日 UTC 时间上午 4 点，如果新的官方 PHP 镜像可用\n\n## 强化镜像\n\n为了进一步减少 FrankenPHP Docker 镜像的攻击面和大小，还可以基于 [Google distroless](https://github.com/GoogleContainerTools/distroless) 或 [Docker hardened](https://www.docker.com/products/hardened-images) 镜像构建它们。\n\n> [!WARNING]\n> 这些最小化的基础镜像不包含 shell 或包管理器，这使得调试更加困难。因此，仅在安全性优先级很高的情况下，才推荐将其用于生产环境。\n\n当添加额外的 PHP 扩展时，你需要一个中间构建阶段：\n\n```dockerfile\nFROM dunglas/frankenphp AS builder\n\n# 在此处添加额外的 PHP 扩展\nRUN install-php-extensions pdo_mysql pdo_pgsql #...\n\n# 将 frankenphp 和所有已安装扩展的共享库复制到临时位置\n# 你也可以通过分析 frankenphp 二进制文件和每个扩展 .so 文件的 ldd 输出手动执行此步骤\nRUN apt-get update && apt-get install -y libtree && \\\n    EXT_DIR=\"$(php -r 'echo ini_get(\"extension_dir\");')\" && \\\n    FRANKENPHP_BIN=\"$(which frankenphp)\"; \\\n    LIBS_TMP_DIR=\"/tmp/libs\"; \\\n    mkdir -p \"$LIBS_TMP_DIR\"; \\\n    for target in \"$FRANKENPHP_BIN\" $(find \"$EXT_DIR\" -maxdepth 2 -type f -name \"*.so\"); do \\\n        libtree -pv \"$target\" | sed 's/.*── \\(.*\\) \\[.*/\\1/' | grep -v \"^$target\" | while IFS= read -r lib; do \\\n            [ -z \"$lib\" ] && continue; \\\n            base=$(basename \"$lib\"); \\\n            destfile=\"$LIBS_TMP_DIR/$base\"; \\\n            if [ ! -f \"$destfile\" ]; then \\\n                cp \"$lib\" \"$destfile\"; \\\n            fi; \\\n        done; \\\n    done\n\n\n# Distroless debian 基础镜像，确保它与基础镜像使用相同的 debian 版本\nFROM gcr.io/distroless/base-debian13\n# Docker hardened 镜像替代方案\n# FROM dhi.io/debian:13\n\n# 你的应用程序和 Caddyfile 要复制到容器中的位置\nARG PATH_TO_APP=\".\"\nARG PATH_TO_CADDYFILE=\"./Caddyfile\"\n\n# 将你的应用程序复制到 /app\n# 为了进一步强化，请确保只有可写路径由非 root 用户拥有\nCOPY --chown=nonroot:nonroot \"$PATH_TO_APP\" /app\nCOPY \"$PATH_TO_CADDYFILE\" /etc/caddy/Caddyfile\n\n# 复制 frankenphp 和必要的库\nCOPY --from=builder /usr/local/bin/frankenphp /usr/local/bin/frankenphp\nCOPY --from=builder /usr/local/lib/php/extensions /usr/local/lib/php/extensions\nCOPY --from=builder /tmp/libs /usr/lib\n\n# 复制 php.ini 配置文件\nCOPY --from=builder /usr/local/etc/php/conf.d /usr/local/etc/php/conf.d\nCOPY --from=builder /usr/local/etc/php/php.ini-production /usr/local/etc/php/php.ini\n\n# Caddy 数据目录——即使在只读根文件系统上，也必须对非 root 用户可写\nENV XDG_CONFIG_HOME=/config \\\n    XDG_DATA_HOME=/data\nCOPY --from=builder --chown=nonroot:nonroot /data/caddy /data/caddy\nCOPY --from=builder --chown=nonroot:nonroot /config/caddy /config/caddy\n\nUSER nonroot\n\nWORKDIR /app\n\n# 运行 frankenphp 并使用提供的 Caddyfile 的入口点\nENTRYPOINT [\"/usr/local/bin/frankenphp\", \"run\", \"-c\", \"/etc/caddy/Caddyfile\"]\n```\n\n## 开发版本\n\n开发版本可在 [`dunglas/frankenphp-dev`](https://hub.docker.com/repository/docker/dunglas/frankenphp-dev) Docker 仓库中获取。\n每次将新的提交推送到 GitHub 仓库的主分支时，都会触发一次新的构建。\n\n`latest*` 标签指向 `main` 分支的 HEAD。形式为 `sha-<git-commit-hash>` 的标签也可用。\n"
  },
  {
    "path": "docs/cn/early-hints.md",
    "content": "# 早期提示\n\nFrankenPHP 原生支持 [103 Early Hints 状态码](https://developer.chrome.com/blog/early-hints/)。\n使用早期提示可以将网页的加载时间缩短 30%。\n\n```php\n<?php\n\nheader('Link: </style.css>; rel=preload; as=style');\nheaders_send(103);\n\n// 慢速算法和 SQL 查询\n\necho <<<'HTML'\n<!DOCTYPE html>\n<title>Hello FrankenPHP</title>\n<link rel=\"stylesheet\" href=\"style.css\">\nHTML;\n```\n\n早期提示由普通模式和 [worker](worker.md) 模式支持。\n"
  },
  {
    "path": "docs/cn/embed.md",
    "content": "# PHP 应用程序作为独立二进制文件\n\nFrankenPHP 能够将 PHP 应用程序的源代码和资源文件嵌入到静态的、独立的二进制文件中。\n\n由于这个特性，PHP 应用程序可以作为独立的二进制文件分发，包括应用程序本身、PHP 解释器和生产级 Web 服务器 Caddy。\n\n了解有关此功能的更多信息 [Kévin 在 SymfonyCon 上的演讲](https://dunglas.dev/2023/12/php-and-symfony-apps-as-standalone-binaries/)。\n\n有关嵌入 Laravel 应用程序，请[阅读此特定文档条目](laravel.md#laravel-apps-as-standalone-binaries)。\n\n## 准备你的应用\n\n在创建独立二进制文件之前，请确保应用已准备好进行打包。\n\n例如，你可能希望：\n\n- 给应用安装生产环境的依赖\n- 导出 autoloader\n- 如果可能，为应用启用生产模式\n- 丢弃不需要的文件，例如 `.git` 或测试文件，以减小最终二进制文件的大小\n\n例如，对于 Symfony 应用程序，你可以使用以下命令：\n\n```console\n# 导出项目以避免 .git/ 等目录\nmkdir $TMPDIR/my-prepared-app\ngit archive HEAD | tar -x -C $TMPDIR/my-prepared-app\ncd $TMPDIR/my-prepared-app\n\n# 设置适当的环境变量\necho APP_ENV=prod > .env.local\necho APP_DEBUG=0 >> .env.local\n\n# 删除测试和其他不需要的文件以节省空间\n# 或者，将这些文件添加到您的 .gitattributes 文件中，并设置 export-ignore 属性\nrm -Rf tests/\n\n# 安装依赖项\ncomposer install --ignore-platform-reqs --no-dev -a\n\n# 优化 .env\ncomposer dump-env prod\n```\n\n### 自定义配置\n\n要自定义[配置](config.md)，您可以放置一个 `Caddyfile` 以及一个 `php.ini` 文件\n在应用程序的主目录中嵌入（在之前的示例中是`$TMPDIR/my-prepared-app`）。\n\n## 创建 Linux 二进制文件\n\n创建 Linux 二进制文件的最简单方法是使用我们提供的基于 Docker 的构建器。\n\n1. 在准备好的应用的存储库中创建一个名为 `static-build.Dockerfile` 的文件。\n\n   ```dockerfile\n   FROM --platform=linux/amd64 dunglas/frankenphp:static-builder-gnu\n   # 如果你打算在 glibc 系统上运行该二进制文件，请使用 static-builder-gnu\n\n   # 复制应用代码\n   WORKDIR /go/src/app/dist/app\n   COPY . .\n\n   # 构建静态二进制文件\n   WORKDIR /go/src/app/\n   RUN EMBED=dist/app/ ./build-static.sh\n   ```\n\n   > [!CAUTION]\n   >\n   > 某些 `.dockerignore` 文件（例如默认的 [Symfony Docker `.dockerignore`](https://github.com/dunglas/symfony-docker/blob/main/.dockerignore)）\n   > 会忽略 `vendor/` 文件夹和 `.env` 文件。在构建之前，请务必调整或删除 `.dockerignore` 文件。\n\n2. 构建:\n\n   ```console\n   docker build -t static-app -f static-build.Dockerfile .\n   ```\n\n3. 提取二进制文件\n\n   ```console\n   docker cp $(docker create --name static-app-tmp static-app):/go/src/app/dist/frankenphp-linux-x86_64 my-app ; docker rm static-app-tmp\n   ```\n\n生成的二进制文件是当前目录中名为 `my-app` 的文件。\n\n## 为其他操作系统创建二进制文件\n\n如果你不想使用 Docker，或者想要构建 macOS 二进制文件，你可以使用我们提供的 shell 脚本：\n\n```console\ngit clone https://github.com/php/frankenphp\ncd frankenphp\nEMBED=/path/to/your/app ./build-static.sh\n```\n\n在 `dist/` 目录中生成的二进制文件名称为 `frankenphp-<os>-<arch>`。\n\n## 使用二进制文件\n\n就是这样！`my-app` 文件（或其他操作系统上的 `dist/frankenphp-<os>-<arch>`）包含你的独立应用程序！\n\n若要启动 Web 应用，请执行：\n\n```console\n./my-app php-server\n```\n\n如果你的应用包含 [worker 脚本](worker.md)，请使用如下命令启动 worker：\n\n```console\n./my-app php-server --worker public/index.php\n```\n\n要启用 HTTPS（自动创建 Let's Encrypt 证书）、HTTP/2 和 HTTP/3，请指定要使用的域名：\n\n```console\n./my-app php-server --domain localhost\n```\n\n你还可以运行二进制文件中嵌入的 PHP CLI 脚本：\n\n```console\n./my-app php-cli bin/console\n```\n\n## PHP Extensions\n\n默认情况下，脚本将构建您项目的 `composer.json` 文件中所需的扩展（如果有的话）。\n如果 `composer.json` 文件不存在，将构建默认扩展，如 [静态构建条目](static.md) 中所述。\n\n要自定义扩展，请使用 `PHP_EXTENSIONS` 环境变量。\n\n## 自定义构建\n\n[阅读静态构建文档](static.md) 查看如何自定义二进制文件（扩展、PHP 版本等）。\n\n## 分发二进制文件\n\n在Linux上，创建的二进制文件使用[UPX](https://upx.github.io)进行压缩。\n\n在Mac上，您可以在发送文件之前压缩它以减小文件大小。\n我们推荐使用 `xz`。\n"
  },
  {
    "path": "docs/cn/extension-workers.md",
    "content": "# 扩展 Worker\n\n扩展 Worker 使您的 [FrankenPHP 扩展](https://frankenphp.dev/docs/extensions/) 能够管理专用的 PHP 线程池，用于执行后台任务、处理异步事件或实现自定义协议。适用于队列系统、事件监听器、调度器等。\n\n## 注册 Worker\n\n### 静态注册\n\n如果您的 worker 不需要用户配置（固定的脚本路径、固定的线程数），您可以直接在 `init()` 函数中注册 worker。\n\n```go\npackage myextension\n\nimport (\n\t\"github.com/dunglas/frankenphp\"\n\t\"github.com/dunglas/frankenphp/caddy\"\n)\n\n// 与 worker 池通信的全局句柄\nvar worker frankenphp.Workers\n\nfunc init() {\n\t// 模块加载时注册 worker。\n\tworker = caddy.RegisterWorkers(\n\t\t\"my-internal-worker\", // 唯一名称\n\t\t\"worker.php\",         // 脚本路径（相对于执行目录或绝对路径）\n\t\t2,                    // 固定线程数\n\t\t// 可选的生命周期钩子\n\t\tfrankenphp.WithWorkerOnServerStartup(func() {\n\t\t\t// 全局设置逻辑...\n\t\t}),\n\t)\n}\n```\n\n### 在 Caddy 模块中（用户可配置）\n\n如果您计划共享您的扩展（例如通用的队列或事件监听器），您应该将其封装在一个 Caddy 模块中。这允许用户通过 `Caddyfile` 配置脚本路径和线程数。这需要实现 `caddy.Provisioner` 接口并解析 Caddyfile ([查看示例](https://github.com/dunglas/frankenphp-queue/blob/989120d394d66dd6c8e2101cac73dd622fade334/caddy.go))。\n\n### 在纯 Go 应用程序中（嵌入式）\n\n如果您 [在没有 Caddy 的标准 Go 应用程序中嵌入 FrankenPHP](https://pkg.go.dev/github.com/dunglas/frankenphp#example-ServeHTTP)，您可以在初始化选项时使用 `frankenphp.WithExtensionWorkers` 注册扩展 worker。\n\n## 与 Worker 交互\n\n一旦 worker 池激活，您就可以向其分派任务。这可以在 [导出到 PHP 的原生函数](https://frankenphp.dev/docs/extensions/#writing-the-extension) 中完成，也可以从任何 Go 逻辑中完成，例如 cron 调度器、事件监听器 (MQTT、Kafka) 或任何其他 goroutine。\n\n### 无头模式：`SendMessage`\n\n使用 `SendMessage` 将原始数据直接传递给您的 worker 脚本。这非常适合队列或简单命令。\n\n#### 示例：一个异步队列扩展\n\n```go\n// #include <Zend/zend_types.h>\nimport \"C\"\nimport (\n\t\"context\"\n\t\"unsafe\"\n\t\"github.com/dunglas/frankenphp\"\n)\n\n//export_php:function my_queue_push(mixed $data): bool\nfunc my_queue_push(data *C.zval) bool {\n\t// 1. 确保 worker 已准备就绪\n\tif worker == nil {\n\t\treturn false\n\t}\n\n\t// 2. 分派给后台 worker\n\t_, err := worker.SendMessage(\n\t\tcontext.Background(), // 标准 Go 上下文\n\t\tunsafe.Pointer(data), // 要传递给 worker 的数据\n\t\tnil, // 可选的 http.ResponseWriter\n\t)\n\n\treturn err == nil\n}\n```\n\n### HTTP 模拟：`SendRequest`\n\n如果您的扩展需要调用一个期望标准 Web 环境（填充 `$_SERVER`、`$_GET` 等）的 PHP 脚本，请使用 `SendRequest`。\n\n```go\n// #include <Zend/zend_types.h>\nimport \"C\"\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"unsafe\"\n\t\"github.com/dunglas/frankenphp\"\n)\n\n//export_php:function my_worker_http_request(string $path): string\nfunc my_worker_http_request(path *C.zend_string) unsafe.Pointer {\n\t// 1. 准备请求和记录器\n\turl := frankenphp.GoString(unsafe.Pointer(path))\n\treq, _ := http.NewRequest(\"GET\", url, http.NoBody)\n\trr := httptest.NewRecorder()\n\n\t// 2. 分派给 worker\n\tif err := worker.SendRequest(rr, req); err != nil {\n\t\treturn nil\n\t}\n\n\t// 3. 返回捕获的响应\n\treturn frankenphp.PHPString(rr.Body.String(), false)\n}\n```\n\n## Worker 脚本\n\nPHP worker 脚本在一个循环中运行，可以处理原始消息和 HTTP 请求。\n\n```php\n<?php\n// 在同一个循环中处理原始消息和 HTTP 请求\n$handler = function ($payload = null) {\n    // 情况 1：消息模式\n    if ($payload !== null) {\n        return \"Received payload: \" . $payload;\n    }\n\n    // 情况 2：HTTP 模式（标准 PHP 超全局变量会被填充）\n    echo \"Hello from page: \" . $_SERVER['REQUEST_URI'];\n};\n\nwhile (frankenphp_handle_request($handler)) {\n    gc_collect_cycles();\n}\n```\n\n## 生命周期钩子\n\nFrankenPHP 提供了钩子，用于在生命周期的特定点执行 Go 代码。\n\n| 钩子类型 | 选项名称                     | 签名                     | 上下文与用例                                         |\n| :------- | :--------------------------- | :----------------------- | :--------------------------------------------------- |\n| **服务器** | `WithWorkerOnServerStartup`  | `func()`                 | 全局设置。**只运行一次**。示例：连接到 NATS/Redis。  |\n| **服务器** | `WithWorkerOnServerShutdown` | `func()`                 | 全局清理。**只运行一次**。示例：关闭共享连接。       |\n| **线程** | `WithWorkerOnReady`          | `func(threadID int)`     | 每线程设置。在线程启动时调用。接收线程 ID。          |\n| **线程** | `WithWorkerOnShutdown`       | `func(threadID int)`     | 每线程清理。接收线程 ID。                            |\n\n### 示例\n\n```go\npackage myextension\n\nimport (\n    \"fmt\"\n    \"github.com/dunglas/frankenphp\"\n    frankenphpCaddy \"github.com/dunglas/frankenphp/caddy\"\n)\n\nfunc init() {\n    workerHandle = frankenphpCaddy.RegisterWorkers(\n        \"my-worker\", \"worker.php\", 2,\n\n        // 服务器启动（全局）\n        frankenphp.WithWorkerOnServerStartup(func() {\n            fmt.Println(\"扩展：服务器正在启动...\")\n        }),\n\n        // 线程就绪（每线程）\n        // 注意：此函数接受一个表示线程 ID 的整数\n        frankenphp.WithWorkerOnReady(func(id int) {\n            fmt.Printf(\"扩展：Worker 线程 #%d 已就绪。\\n\", id)\n        }),\n    )\n}\n```\n"
  },
  {
    "path": "docs/cn/extensions.md",
    "content": "# 使用 Go 编写 PHP 扩展\n\n使用 FrankenPHP，你可以**使用 Go 编写 PHP 扩展**，这允许你创建**高性能的原生函数**，可以直接从 PHP 调用。你的应用程序可以利用任何现有或新的 Go 库，以及直接从你的 PHP 代码中使用**协程（goroutines）的并发模型**。\n\n编写 PHP 扩展通常使用 C 语言完成，但通过一些额外的工作，也可以使用其他语言编写。PHP 扩展允许你利用底层语言的强大功能来扩展 PHP 的功能，例如，通过添加原生函数或优化特定操作。\n\n借助 Caddy 模块，你可以使用 Go 编写 PHP 扩展，并将其快速集成到 FrankenPHP 中。\n\n## 两种方法\n\nFrankenPHP 提供两种方式来创建 Go 语言的 PHP 扩展：\n\n1. **使用扩展生成器** - 推荐的方法，为大多数用例生成所有必要的样板代码，让你专注于编写 Go 代码\n2. **手动实现** - 对于高级用例，完全控制扩展结构\n\n我们将从生成器方法开始，因为这是最简单的入门方式，然后为那些需要完全控制的人展示手动实现。\n\n## 使用扩展生成器\n\nFrankenPHP 捆绑了一个工具，允许你**仅使用 Go 创建 PHP 扩展**。**无需编写 C 代码**或直接使用 CGO：FrankenPHP 还包含一个**公共类型 API**，帮助你在 Go 中编写扩展，而无需担心**PHP/C 和 Go 之间的类型转换**。\n\n> [!TIP]\n> 如果你想了解如何从头开始在 Go 中编写扩展，可以阅读下面的手动实现部分，该部分演示了如何在不使用生成器的情况下在 Go 中编写 PHP 扩展。\n\n请记住，此工具**不是功能齐全的扩展生成器**。它旨在帮助你在 Go 中编写简单的扩展，但它不提供 PHP 扩展的最高级功能。如果你需要编写更**复杂和优化**的扩展，你可能需要编写一些 C 代码或直接使用 CGO。\n\n### 先决条件\n\n正如下面的手动实现部分所涵盖的，你需要[获取 PHP 源代码](https://www.php.net/downloads.php)并创建一个新的 Go 模块。\n\n#### 创建新模块并获取 PHP 源代码\n\n在 Go 中编写 PHP 扩展的第一步是创建一个新的 Go 模块。你可以使用以下命令：\n\n```console\ngo mod init github.com/my-account/my-module\n```\n\n第二步是为后续步骤[获取 PHP 源代码](https://www.php.net/downloads.php)。获取后，将它们解压到你选择的目录中，不要放在你的 Go 模块内：\n\n```console\ntar xf php-*\n```\n\n### 编写扩展\n\n现在一切都设置好了，可以在 Go 中编写你的原生函数。创建一个名为 `stringext.go` 的新文件。我们的第一个函数将接受一个字符串作为参数，重复次数，一个布尔值来指示是否反转字符串，并返回结果字符串。这应该看起来像这样：\n\n```go\nimport (\n    \"C\"\n    \"github.com/dunglas/frankenphp\"\n    \"strings\"\n)\n\n//export_php:function repeat_this(string $str, int $count, bool $reverse): string\nfunc repeat_this(s *C.zend_string, count int64, reverse bool) unsafe.Pointer {\n    str := frankenphp.GoString(unsafe.Pointer(s))\n\n    result := strings.Repeat(str, int(count))\n    if reverse {\n        runes := []rune(result)\n        for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {\n            runes[i], runes[j] = runes[j], runes[i]\n        }\n        result = string(runes)\n    }\n\n    return frankenphp.PHPString(result, false)\n}\n```\n\n这里有两个重要的事情要注意：\n\n- 指令注释 `//export_php:function` 定义了 PHP 中的函数签名。这是生成器知道如何使用正确的参数和返回类型生成 PHP 函数的方式；\n- 函数必须返回 `unsafe.Pointer`。FrankenPHP 提供了一个 API 来帮助你在 C 和 Go 之间进行类型转换。\n\n虽然第一点不言自明，但第二点可能更难理解。让我们在下一节中深入了解类型转换。\n\n### 类型转换\n\n虽然一些变量类型在 C/PHP 和 Go 之间具有相同的内存表示，但某些类型需要更多逻辑才能直接使用。这可能是编写扩展时最困难的部分，因为它需要了解 Zend 引擎的内部结构以及变量在 PHP 中的内部存储方式。此表总结了你需要知道的内容：\n\n| PHP 类型           | Go 类型             | 直接转换 | C 到 Go 助手          | Go 到 C 助手           | 类方法支持 |\n| ------------------ | ------------------- | -------- | --------------------- | ---------------------- | ---------- |\n| `int`              | `int64`             | ✅       | -                     | -                      | ✅         |\n| `?int`             | `*int64`            | ✅       | -                     | -                      | ✅         |\n| `float`            | `float64`           | ✅       | -                     | -                      | ✅         |\n| `?float`           | `*float64`          | ✅       | -                     | -                      | ✅         |\n| `bool`             | `bool`              | ✅       | -                     | -                      | ✅         |\n| `?bool`            | `*bool`             | ✅       | -                     | -                      | ✅         |\n| `string`/`?string` | `*C.zend_string`    | ❌       | frankenphp.GoString() | frankenphp.PHPString() | ✅         |\n| `array`            | `*frankenphp.Array` | ❌       | frankenphp.GoArray()  | frankenphp.PHPArray()  | ✅         |\n| `mixed`            | `any`               | ❌       | `GoValue()`           | `PHPValue()`           | ❌         |\n| `object`           | `struct`            | ❌       | _尚未实现_            | _尚未实现_             | ❌         |\n\n> [!NOTE]\n> 此表尚不详尽，将随着 FrankenPHP 类型 API 变得更加完整而完善。\n>\n> 特别是对于类方法，目前支持原始类型和数组。对象尚不能用作方法参数或返回类型。\n\n如果你参考上一节的代码片段，你可以看到助手用于转换第一个参数和返回值。我们的 `repeat_this()` 函数的第二和第三个参数不需要转换，因为底层类型的内存表示对于 C 和 Go 都是相同的。\n\n#### 处理数组\n\nFrankenPHP 通过 `frankenphp.Array` 类型为 PHP 数组提供原生支持。此类型表示 PHP 索引数组（列表）和关联数组（哈希映射），具有有序的键值对。\n\n**在 Go 中创建和操作数组：**\n\n```go\n//export_php:function process_data(array $input): array\nfunc process_data(arr *C.zval) unsafe.Pointer {\n    // 将 PHP 数组转换为 Go\n    goArray := frankenphp.GoArray(unsafe.Pointer(arr))\n\n\tresult := &frankenphp.Array{}\n\n    result.SetInt(0, \"first\")\n    result.SetInt(1, \"second\")\n    result.Append(\"third\") // 自动分配下一个整数键\n\n    result.SetString(\"name\", \"John\")\n    result.SetString(\"age\", int64(30))\n\n    for i := uint32(0); i < goArray.Len(); i++ {\n        key, value := goArray.At(i)\n        if key.Type == frankenphp.PHPStringKey {\n            result.SetString(\"processed_\"+key.Str, value)\n        } else {\n            result.SetInt(key.Int+100, value)\n        }\n    }\n\n    // 转换回 PHP 数组\n    return frankenphp.PHPArray(result)\n}\n```\n\n**`frankenphp.Array` 的关键特性：**\n\n- **有序键值对** - 像 PHP 数组一样维护插入顺序\n- **混合键类型** - 在同一数组中支持整数和字符串键\n- **类型安全** - `PHPKey` 类型确保正确的键处理\n- **自动列表检测** - 转换为 PHP 时，自动检测数组应该是打包列表还是哈希映射\n- **不支持对象** - 目前，只有标量类型和数组可以用作值。提供对象将导致 PHP 数组中的 `null` 值。\n\n**可用方法：**\n\n- `SetInt(key int64, value any)` - 使用整数键设置值\n- `SetString(key string, value any)` - 使用字符串键设置值\n- `Append(value any)` - 使用下一个可用整数键添加值\n- `Len() uint32` - 获取元素数量\n- `At(index uint32) (PHPKey, any)` - 获取索引处的键值对\n- `frankenphp.PHPArray(arr *frankenphp.Array) unsafe.Pointer` - 转换为 PHP 数组\n\n### 声明原生 PHP 类\n\n生成器支持将 Go 结构体声明为**不透明类**，可用于创建 PHP 对象。你可以使用 `//export_php:class` 指令注释来定义 PHP 类。例如：\n\n```go\n//export_php:class User\ntype UserStruct struct {\n    Name string\n    Age  int\n}\n```\n\n#### 什么是不透明类？\n\n**不透明类**是内部结构（属性）对 PHP 代码隐藏的类。这意味着：\n\n- **无直接属性访问**：你不能直接从 PHP 读取或写入属性（`$user->name` 不起作用）\n- **仅方法接口** - 所有交互必须通过你定义的方法进行\n- **更好的封装** - 内部数据结构完全由 Go 代码控制\n- **类型安全** - 没有 PHP 代码使用错误类型破坏内部状态的风险\n- **更清晰的 API** - 强制设计适当的公共接口\n\n这种方法提供了更好的封装，并防止 PHP 代码意外破坏 Go 对象的内部状态。与对象的所有交互都必须通过你明确定义的方法进行。\n\n#### 为类添加方法\n\n由于属性不能直接访问，你**必须定义方法**来与不透明类交互。使用 `//export_php:method` 指令来定义行为：\n\n```go\n//export_php:class User\ntype UserStruct struct {\n    Name string\n    Age  int\n}\n\n//export_php:method User::getName(): string\nfunc (us *UserStruct) GetUserName() unsafe.Pointer {\n    return frankenphp.PHPString(us.Name, false)\n}\n\n//export_php:method User::setAge(int $age): void\nfunc (us *UserStruct) SetUserAge(age int64) {\n    us.Age = int(age)\n}\n\n//export_php:method User::getAge(): int\nfunc (us *UserStruct) GetUserAge() int64 {\n    return int64(us.Age)\n}\n\n//export_php:method User::setNamePrefix(string $prefix = \"User\"): void\nfunc (us *UserStruct) SetNamePrefix(prefix *C.zend_string) {\n    us.Name = frankenphp.GoString(unsafe.Pointer(prefix)) + \": \" + us.Name\n}\n```\n\n#### 可空参数\n\n生成器支持在 PHP 签名中使用 `?` 前缀的可空参数。当参数可空时，它在你的 Go 函数中变成指针，允许你检查值在 PHP 中是否为 `null`：\n\n```go\n//export_php:method User::updateInfo(?string $name, ?int $age, ?bool $active): void\nfunc (us *UserStruct) UpdateInfo(name *C.zend_string, age *int64, active *bool) {\n    // 检查是否提供了 name（不为 null）\n    if name != nil {\n        us.Name = frankenphp.GoString(unsafe.Pointer(name))\n    }\n\n    // 检查是否提供了 age（不为 null）\n    if age != nil {\n        us.Age = int(*age)\n    }\n\n    // 检查是否提供了 active（不为 null）\n    if active != nil {\n        us.Active = *active\n    }\n}\n```\n\n**关于可空参数的要点：**\n\n- **可空原始类型**（`?int`、`?float`、`?bool`）在 Go 中变成指针（`*int64`、`*float64`、`*bool`）\n- **可空字符串**（`?string`）仍然是 `*C.zend_string`，但可以是 `nil`\n- **在解引用指针值之前检查 `nil`**\n- **PHP `null` 变成 Go `nil`** - 当 PHP 传递 `null` 时，你的 Go 函数接收 `nil` 指针\n\n> [!WARNING]\n> 目前，类方法有以下限制。**不支持对象**作为参数类型或返回类型。**完全支持数组**作为参数和返回类型。支持的类型：`string`、`int`、`float`、`bool`、`array` 和 `void`（用于返回类型）。**完全支持可空参数类型**，适用于所有标量类型（`?string`、`?int`、`?float`、`?bool`）。\n\n生成扩展后，你将被允许在 PHP 中使用类及其方法。请注意，你**不能直接访问属性**：\n\n```php\n<?php\n\n$user = new User();\n\n// ✅ 这可以工作 - 使用方法\n$user->setAge(25);\necho $user->getName();           // 输出：（空，默认值）\necho $user->getAge();            // 输出：25\n$user->setNamePrefix(\"Employee\");\n\n// ✅ 这也可以工作 - 可空参数\n$user->updateInfo(\"John\", 30, true);        // 提供所有参数\n$user->updateInfo(\"Jane\", null, false);     // Age 为 null\n$user->updateInfo(null, 25, null);          // Name 和 active 为 null\n\n// ❌ 这不会工作 - 直接属性访问\n// echo $user->name;             // 错误：无法访问私有属性\n// $user->age = 30;              // 错误：无法访问私有属性\n```\n\n这种设计确保你的 Go 代码完全控制如何访问和修改对象的状态，提供更好的封装和类型安全。\n\n### 声明常量\n\n生成器支持使用两个指令将 Go 常量导出到 PHP：`//export_php:const` 用于全局常量，`//export_php:classconst` 用于类常量。这允许你在 Go 和 PHP 代码之间共享配置值、状态代码和其他常量。\n\n#### 全局常量\n\n使用 `//export_php:const` 指令创建全局 PHP 常量：\n\n```go\n//export_php:const\nconst MAX_CONNECTIONS = 100\n\n//export_php:const\nconst API_VERSION = \"1.2.3\"\n\n//export_php:const\nconst STATUS_OK = iota\n\n//export_php:const\nconst STATUS_ERROR = iota\n```\n\n#### 类常量\n\n使用 `//export_php:classconst ClassName` 指令创建属于特定 PHP 类的常量：\n\n```go\n//export_php:classconst User\nconst STATUS_ACTIVE = 1\n\n//export_php:classconst User\nconst STATUS_INACTIVE = 0\n\n//export_php:classconst User\nconst ROLE_ADMIN = \"admin\"\n\n//export_php:classconst Order\nconst STATE_PENDING = iota\n\n//export_php:classconst Order\nconst STATE_PROCESSING = iota\n\n//export_php:classconst Order\nconst STATE_COMPLETED = iota\n```\n\n类常量在 PHP 中使用类名作用域访问：\n\n```php\n<?php\n\n// 全局常量\necho MAX_CONNECTIONS;    // 100\necho API_VERSION;        // \"1.2.3\"\n\n// 类常量\necho User::STATUS_ACTIVE;    // 1\necho User::ROLE_ADMIN;       // \"admin\"\necho Order::STATE_PENDING;   // 0\n```\n\n该指令支持各种值类型，包括字符串、整数、布尔值、浮点数和 iota 常量。使用 `iota` 时，生成器自动分配顺序值（0、1、2 等）。全局常量在你的 PHP 代码中作为全局常量可用，而类常量使用公共可见性限定在各自的类中。使用整数时，支持不同的可能记法（二进制、十六进制、八进制）并在 PHP 存根文件中按原样转储。\n\n你可以像在 Go 代码中习惯的那样使用常量。例如，让我们采用我们之前声明的 `repeat_this()` 函数，并将最后一个参数更改为整数：\n\n```go\nimport (\n    \"C\"\n    \"github.com/dunglas/frankenphp\"\n    \"strings\"\n)\n\n//export_php:const\nconst STR_REVERSE = iota\n\n//export_php:const\nconst STR_NORMAL = iota\n\n//export_php:classconst StringProcessor\nconst MODE_LOWERCASE = 1\n\n//export_php:classconst StringProcessor\nconst MODE_UPPERCASE = 2\n\n//export_php:function repeat_this(string $str, int $count, int $mode): string\nfunc repeat_this(s *C.zend_string, count int64, mode int) unsafe.Pointer {\n    str := frankenphp.GoString(unsafe.Pointer(s))\n\n    result := strings.Repeat(str, int(count))\n    if mode == STR_REVERSE {\n        // 反转字符串\n    }\n\n    if mode == STR_NORMAL {\n        // 无操作，只是为了展示常量\n    }\n\n    return frankenphp.PHPString(result, false)\n}\n\n//export_php:class StringProcessor\ntype StringProcessorStruct struct {\n    // 内部字段\n}\n\n//export_php:method StringProcessor::process(string $input, int $mode): string\nfunc (sp *StringProcessorStruct) Process(input *C.zend_string, mode int64) unsafe.Pointer {\n    str := frankenphp.GoString(unsafe.Pointer(input))\n\n    switch mode {\n    case MODE_LOWERCASE:\n        str = strings.ToLower(str)\n    case MODE_UPPERCASE:\n        str = strings.ToUpper(str)\n    }\n\n    return frankenphp.PHPString(str, false)\n}\n```\n\n### 使用命名空间\n\n生成器支持使用 `//export_php:namespace` 指令将 PHP 扩展的函数、类和常量组织在命名空间下。这有助于避免命名冲突，并为扩展的 API 提供更好的组织。\n\n#### 声明命名空间\n\n在你的 Go 文件顶部使用 `//export_php:namespace` 指令，将所有导出的符号放在特定命名空间下：\n\n```go\n//export_php:namespace My\\Extension\npackage main\n\nimport \"C\"\n\n//export_php:function hello(): string\nfunc hello() string {\n    return \"Hello from My\\\\Extension namespace!\"\n}\n\n//export_php:class User\ntype UserStruct struct {\n    // 内部字段\n}\n\n//export_php:method User::getName(): string\nfunc (u *UserStruct) GetName() unsafe.Pointer {\n    return frankenphp.PHPString(\"John Doe\", false)\n}\n\n//export_php:const\nconst STATUS_ACTIVE = 1\n```\n\n#### 在 PHP 中使用命名空间扩展\n\n当声明命名空间时，所有函数、类和常量都放在 PHP 中的该命名空间下：\n\n```php\n<?php\n\necho My\\Extension\\hello(); // \"Hello from My\\Extension namespace!\"\n\n$user = new My\\Extension\\User();\necho $user->getName(); // \"John Doe\"\n\necho My\\Extension\\STATUS_ACTIVE; // 1\n```\n\n#### 重要说明\n\n- 每个文件只允许**一个**命名空间指令。如果找到多个命名空间指令，生成器将返回错误。\n- 命名空间适用于文件中的**所有**导出符号：函数、类、方法和常量。\n- 命名空间名称遵循 PHP 命名空间约定，使用反斜杠（`\\`）作为分隔符。\n- 如果没有声明命名空间，符号将照常导出到全局命名空间。\n\n### 生成扩展\n\n这就是魔法发生的地方，现在可以生成你的扩展。你可以使用以下命令运行生成器：\n\n```console\nGEN_STUB_SCRIPT=php-src/build/gen_stub.php frankenphp extension-init my_extension.go\n```\n\n> [!NOTE]\n> 不要忘记将 `GEN_STUB_SCRIPT` 环境变量设置为你之前下载的 PHP 源代码中 `gen_stub.php` 文件的路径。这是在手动实现部分中提到的同一个 `gen_stub.php` 脚本。\n\n如果一切顺利，应该创建了一个名为 `build` 的新目录。此目录包含扩展的生成文件，包括带有生成的 PHP 函数存根的 `my_extension.go` 文件。\n\n### 将生成的扩展集成到 FrankenPHP 中\n\n我们的扩展现在已准备好编译并集成到 FrankenPHP 中。为此，请参阅 FrankenPHP [编译文档](compile.md)以了解如何编译 FrankenPHP。使用 `--with` 标志添加模块，指向你的模块路径：\n\n```console\nCGO_ENABLED=1 \\\nXCADDY_GO_BUILD_FLAGS=\"-ldflags='-w -s' -tags=nobadger,nomysql,nopgx\" \\\nCGO_CFLAGS=$(php-config --includes) \\\nCGO_LDFLAGS=\"$(php-config --ldflags) $(php-config --libs)\" \\\nxcaddy build \\\n    --output frankenphp \\\n    --with github.com/my-account/my-module/build\n```\n\n请注意，你指向在生成步骤中创建的 `/build` 子目录。但是，这不是强制性的：你也可以将生成的文件复制到你的模块目录并直接指向它。\n\n### 测试你的生成扩展\n\n你可以创建一个 PHP 文件来测试你创建的函数和类。例如，创建一个包含以下内容的 `index.php` 文件：\n\n```php\n<?php\n\n// 使用全局常量\nvar_dump(repeat_this('Hello World', 5, STR_REVERSE));\n\n// 使用类常量\n$processor = new StringProcessor();\necho $processor->process('Hello World', StringProcessor::MODE_LOWERCASE);  // \"hello world\"\necho $processor->process('Hello World', StringProcessor::MODE_UPPERCASE);  // \"HELLO WORLD\"\n```\n\n一旦你按照上一节所示将扩展集成到 FrankenPHP 中，你就可以使用 `./frankenphp php-server` 运行此测试文件，你应该看到你的扩展正在工作。\n\n## 手动实现\n\n如果你想了解扩展的工作原理或需要完全控制你的扩展，你可以手动编写它们。这种方法给你完全的控制，但需要更多的样板代码。\n\n### 基本函数\n\n我们将看到如何在 Go 中编写一个简单的 PHP 扩展，定义一个新的原生函数。此函数将从 PHP 调用，并将触发一个在 Caddy 日志中记录消息的协程。此函数不接受任何参数并且不返回任何内容。\n\n#### 定义 Go 函数\n\n在你的模块中，你需要定义一个新的原生函数，该函数将从 PHP 调用。为此，创建一个你想要的名称的文件，例如 `extension.go`，并添加以下代码：\n\n```go\npackage ext_go\n\n//#include \"extension.h\"\nimport \"C\"\nimport (\n    \"unsafe\"\n    \"github.com/caddyserver/caddy/v2\"\n    \"github.com/dunglas/frankenphp\"\n)\n\nfunc init() {\n    frankenphp.RegisterExtension(unsafe.Pointer(&C.ext_module_entry))\n}\n\n//export go_print_something\nfunc go_print_something() {\n    go func() {\n        caddy.Log().Info(\"Hello from a goroutine!\")\n    }()\n}\n```\n\n`frankenphp.RegisterExtension()` 函数通过处理内部 PHP 注册逻辑简化了扩展注册过程。`go_print_something` 函数使用 `//export` 指令表示它将在我们将编写的 C 代码中可访问，这要归功于 CGO。\n\n在此示例中，我们的新函数将触发一个在 Caddy 日志中记录消息的协程。\n\n#### 定义 PHP 函数\n\n为了允许 PHP 调用我们的函数，我们需要定义相应的 PHP 函数。为此，我们将创建一个存根文件，例如 `extension.stub.php`，其中包含以下代码：\n\n```php\n<?php\n\n/** @generate-class-entries */\n\nfunction go_print(): void {}\n```\n\n此文件定义了 `go_print()` 函数的签名，该函数将从 PHP 调用。`@generate-class-entries` 指令允许 PHP 自动为我们的扩展生成函数条目。\n\n这不是手动完成的，而是使用 PHP 源代码中提供的脚本（确保根据你的 PHP 源代码所在位置调整 `gen_stub.php` 脚本的路径）：\n\n```bash\nphp ../php-src/build/gen_stub.php extension.stub.php\n```\n\n此脚本将生成一个名为 `extension_arginfo.h` 的文件，其中包含 PHP 知道如何定义和调用我们函数所需的信息。\n\n#### 编写 Go 和 C 之间的桥梁\n\n现在，我们需要编写 Go 和 C 之间的桥梁。在你的模块目录中创建一个名为 `extension.h` 的文件，内容如下：\n\n```c\n#ifndef _EXTENSION_H\n#define _EXTENSION_H\n\n#include <php.h>\n\nextern zend_module_entry ext_module_entry;\n\n#endif\n```\n\n接下来，创建一个名为 `extension.c` 的文件，该文件将执行以下步骤：\n\n- 包含 PHP 头文件；\n- 声明我们的新原生 PHP 函数 `go_print()`；\n- 声明扩展元数据。\n\n让我们首先包含所需的头文件：\n\n```c\n#include <php.h>\n#include \"extension.h\"\n#include \"extension_arginfo.h\"\n\n// 包含 Go 导出的符号\n#include \"_cgo_export.h\"\n```\n\n然后我们将 PHP 函数定义为原生语言函数：\n\n```c\nPHP_FUNCTION(go_print)\n{\n    ZEND_PARSE_PARAMETERS_NONE();\n\n    go_print_something();\n}\n\nzend_module_entry ext_module_entry = {\n    STANDARD_MODULE_HEADER,\n    \"ext_go\",\n    ext_functions, /* Functions */\n    NULL,          /* MINIT */\n    NULL,          /* MSHUTDOWN */\n    NULL,          /* RINIT */\n    NULL,          /* RSHUTDOWN */\n    NULL,          /* MINFO */\n    \"0.1.1\",\n    STANDARD_MODULE_PROPERTIES\n};\n```\n\n在这种情况下，我们的函数不接受参数并且不返回任何内容。它只是调用我们之前定义的 Go 函数，使用 `//export` 指令导出。\n\n最后，我们在 `zend_module_entry` 结构中定义扩展的元数据，例如其名称、版本和属性。这些信息对于 PHP 识别和加载我们的扩展是必需的。请注意，`ext_functions` 是指向我们定义的 PHP 函数的指针数组，它由 `gen_stub.php` 脚本在 `extension_arginfo.h` 文件中自动生成。\n\n扩展注册由我们在 Go 代码中调用的 FrankenPHP 的 `RegisterExtension()` 函数自动处理。\n\n### 高级用法\n\n现在我们知道了如何在 Go 中创建基本的 PHP 扩展，让我们复杂化我们的示例。我们现在将创建一个 PHP 函数，该函数接受一个字符串作为参数并返回其大写版本。\n\n#### 定义 PHP 函数存根\n\n为了定义新的 PHP 函数，我们将修改我们的 `extension.stub.php` 文件以包含新的函数签名：\n\n```php\n<?php\n\n/** @generate-class-entries */\n\n/**\n * 将字符串转换为大写。\n *\n * @param string $string 要转换的字符串。\n * @return string 字符串的大写版本。\n */\nfunction go_upper(string $string): string {}\n```\n\n> [!TIP]\n> 不要忽视函数的文档！你可能会与其他开发人员共享扩展存根，以记录如何使用你的扩展以及哪些功能可用。\n\n通过使用 `gen_stub.php` 脚本重新生成存根文件，`extension_arginfo.h` 文件应该如下所示：\n\n```c\nZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_go_upper, 0, 1, IS_STRING, 0)\n    ZEND_ARG_TYPE_INFO(0, string, IS_STRING, 0)\nZEND_END_ARG_INFO()\n\nZEND_FUNCTION(go_upper);\n\nstatic const zend_function_entry ext_functions[] = {\n    ZEND_FE(go_upper, arginfo_go_upper)\n    ZEND_FE_END\n};\n```\n\n我们可以看到 `go_upper` 函数定义了一个 `string` 类型的参数和一个 `string` 的返回类型。\n\n#### Go 和 PHP/C 之间的类型转换\n\n你的 Go 函数不能直接接受 PHP 字符串作为参数。你需要将其转换为 Go 字符串。幸运的是，FrankenPHP 提供了助手函数来处理 PHP 字符串和 Go 字符串之间的转换，类似于我们在生成器方法中看到的。\n\n头文件保持简单：\n\n```c\n#ifndef _EXTENSION_H\n#define _EXTENSION_H\n\n#include <php.h>\n\nextern zend_module_entry ext_module_entry;\n\n#endif\n```\n\n我们现在可以在我们的 `extension.c` 文件中编写 Go 和 C 之间的桥梁。我们将 PHP 字符串直接传递给我们的 Go 函数：\n\n```c\nPHP_FUNCTION(go_upper)\n{\n    zend_string *str;\n\n    ZEND_PARSE_PARAMETERS_START(1, 1)\n        Z_PARAM_STR(str)\n    ZEND_PARSE_PARAMETERS_END();\n\n    zend_string *result = go_upper(str);\n    RETVAL_STR(result);\n}\n```\n\n你可以在 [PHP 内部手册](https://www.phpinternalsbook.com/php7/extensions_design/php_functions.html#parsing-parameters-zend-parse-parameters) 的专门页面中了解更多关于 `ZEND_PARSE_PARAMETERS_START` 和参数解析的信息。在这里，我们告诉 PHP 我们的函数接受一个 `string` 类型的强制参数作为 `zend_string`。然后我们将此字符串直接传递给我们的 Go 函数，并使用 `RETVAL_STR` 返回结果。\n\n只剩下一件事要做：在 Go 中实现 `go_upper` 函数。\n\n#### 实现 Go 函数\n\n我们的 Go 函数将接受 `*C.zend_string` 作为参数，使用 FrankenPHP 的助手函数将其转换为 Go 字符串，处理它，并将结果作为新的 `*C.zend_string` 返回。助手函数为我们处理所有内存管理和转换复杂性。\n\n```go\nimport \"strings\"\n\n//export go_upper\nfunc go_upper(s *C.zend_string) *C.zend_string {\n    str := frankenphp.GoString(unsafe.Pointer(s))\n\n    upper := strings.ToUpper(str)\n\n    return (*C.zend_string)(frankenphp.PHPString(upper, false))\n}\n```\n\n这种方法比手动内存管理更清洁、更安全。FrankenPHP 的助手函数自动处理 PHP 的 `zend_string` 格式和 Go 字符串之间的转换。`PHPString()` 中的 `false` 参数表示我们想要创建一个新的非持久字符串（在请求结束时释放）。\n\n> [!TIP]\n> 在此示例中，我们不执行任何错误处理，但你应该始终检查指针不是 `nil` 并且数据在 Go 函数中使用之前是有效的。\n\n### 将扩展集成到 FrankenPHP 中\n\n我们的扩展现在已准备好编译并集成到 FrankenPHP 中。为此，请参阅 FrankenPHP [编译文档](compile.md)以了解如何编译 FrankenPHP。使用 `--with` 标志添加模块，指向你的模块路径：\n\n```console\nCGO_ENABLED=1 \\\nXCADDY_GO_BUILD_FLAGS=\"-ldflags='-w -s' -tags=nobadger,nomysql,nopgx\" \\\nCGO_CFLAGS=$(php-config --includes) \\\nCGO_LDFLAGS=\"$(php-config --ldflags) $(php-config --libs)\" \\\nxcaddy build \\\n    --output frankenphp \\\n    --with github.com/my-account/my-module\n```\n\n就是这样！你的扩展现在集成到 FrankenPHP 中，可以在你的 PHP 代码中使用。\n\n### 测试你的扩展\n\n将扩展集成到 FrankenPHP 后，你可以为你实现的函数创建一个包含示例的 `index.php` 文件：\n\n```php\n<?php\n\n// 测试基本函数\ngo_print();\n\n// 测试高级函数\necho go_upper(\"hello world\") . \"\\n\";\n```\n\n你现在可以使用 `./frankenphp php-server` 运行带有此文件的 FrankenPHP，你应该看到你的扩展正在工作。\n"
  },
  {
    "path": "docs/cn/github-actions.md",
    "content": "# 使用 GitHub Actions\n\n此存储库构建 Docker 镜像并将其部署到 [Docker Hub](https://hub.docker.com/r/dunglas/frankenphp) 上\n每个批准的拉取请求或设置后在你自己的分支上。\n\n## 设置 GitHub Actions\n\n在存储库设置中的 `secrets` 下，添加以下字段：\n\n- `REGISTRY_LOGIN_SERVER`: 要使用的 Docker registry（如 `docker.io`）。\n- `REGISTRY_USERNAME`: 用于登录 registry 的用户名（如 `dunglas`）。\n- `REGISTRY_PASSWORD`: 用于登录 registry 的密码（如 `access key`）。\n- `IMAGE_NAME`: 镜像的名称（如 `dunglas/frankenphp`）。\n\n## 构建和推送镜像\n\n1. 创建 Pull Request 或推送到你的 Fork 分支。\n2. GitHub Actions 将生成镜像并运行每项测试。\n3. 如果生成成功，则将使用 `pr-x` 推送 registry，其中 `x` 是 PR 编号，作为标记将镜像推送到注册表。\n\n## 部署镜像\n\n1. 合并 Pull Request 后，GitHub Actions 将再次运行测试并生成新镜像。\n2. 如果构建成功，则 Docker 注册表中的 `main` tag 将更新。\n\n## 发布\n\n1. 在项目仓库中创建新 Tag。\n2. GitHub Actions 将生成镜像并运行每项测试。\n3. 如果构建成功，镜像将使用标记名称作为标记推送到 registry（例如，将创建 `v1.2.3` 和 `v1.2`）。\n4. `latest` 标签也将更新。\n"
  },
  {
    "path": "docs/cn/hot-reload.md",
    "content": "# 热重载\n\nFrankenPHP 包含一个内置的**热重载**功能，旨在极大改善开发者的体验。\n\n![Hot Reload](hot-reload.png)\n\n此功能提供了类似于现代 JavaScript 工具（如 Vite 或 webpack）中的 **热模块替换 (HMR)** 的工作流程。\n无需在每次文件更改（PHP 代码、模板、JavaScript 和 CSS 文件等）后手动刷新浏览器，FrankenPHP 会实时更新页面内容。\n\n热重载原生支持 WordPress、Laravel、Symfony 以及任何其他 PHP 应用程序或框架。\n\n启用后，FrankenPHP 会监控您当前工作目录的文件系统变化。\n当文件被修改时，它会将 [Mercure](mercure.md) 更新推送到浏览器。\n\n根据您的设置，浏览器将：\n\n- 如果加载了 [Idiomorph](https://github.com/bigskysoftware/idiomorph)，则**修改 DOM**（保留滚动位置和输入状态）。\n- 如果 Idiomorph 不存在，则**重新加载页面**（标准实时重载）。\n\n## 配置\n\n要启用热重载，请先启用 Mercure，然后在 `Caddyfile` 的 `php_server` 指令中添加 `hot_reload` 子指令。\n\n> [!WARNING]\n>\n> 此功能仅适用于**开发环境**。\n> 请勿在生产环境中启用 `hot_reload`，因为此功能不安全（会暴露敏感的内部细节）并且会降低应用程序的速度。\n>\n```caddyfile\nlocalhost\n\nmercure {\n    anonymous\n}\n\nroot public/\nphp_server {\n    hot_reload\n}\n```\n\n默认情况下，FrankenPHP 会监控当前工作目录中匹配此全局模式的所有文件：`./**/*.{css,env,gif,htm,html,jpg,jpeg,js,mjs,php,png,svg,twig,webp,xml,yaml,yml}`\n\n可以通过全局语法显式设置要监控的文件：\n\n```caddyfile\nlocalhost\n\nmercure {\n    anonymous\n}\n\nroot public/\nphp_server {\n    hot_reload src/**/*{.php,.js} config/**/*.yaml\n}\n```\n\n使用 `hot_reload` 的长格式来指定要使用的 Mercure 主题以及要监控的目录或文件：\n\n```caddyfile\nlocalhost\n\nmercure {\n    anonymous\n}\n\nroot public/\nphp_server {\n    hot_reload {\n        topic hot-reload-topic\n        watch src/**/*.php\n        watch assets/**/*.{ts,json}\n        watch templates/\n        watch public/css/\n    }\n}\n```\n\n## 客户端集成\n\n尽管服务器检测到更改，但浏览器需要订阅这些事件才能更新页面。\nFrankenPHP 通过 `$_SERVER['FRANKENPHP_HOT_RELOAD']` 环境变量公开用于订阅文件更改的 Mercure Hub URL。\n\n还提供了一个方便的 JavaScript 库 [frankenphp-hot-reload](https://www.npmjs.com/package/frankenphp-hot-reload) 来处理客户端逻辑。\n要使用它，请将以下内容添加到您的主布局中：\n\n```php\n<!DOCTYPE html>\n<title>FrankenPHP Hot Reload</title>\n<?php if (isset($_SERVER['FRANKENPHP_HOT_RELOAD'])): ?>\n<meta name=\"frankenphp-hot-reload:url\" content=\"<?=$_SERVER['FRANKENPHP_HOT_RELOAD']?>\">\n<script src=\"https://cdn.jsdelivr.net/npm/idiomorph\"></script>\n<script src=\"https://cdn.jsdelivr.net/npm/frankenphp-hot-reload/+esm\" type=\"module\"></script>\n<?php endif ?>\n```\n\n该库将自动订阅 Mercure hub，在检测到文件更改时在后台获取当前 URL，并修改 DOM。\n它作为 [npm](https://www.npmjs.com/package/frankenphp-hot-reload) 包和在 [GitHub](https://github.com/dunglas/frankenphp-hot-reload) 上提供。\n\n或者，您可以通过使用 `EventSource` 原生 JavaScript 类直接订阅 Mercure hub 来实现自己的客户端逻辑。\n\n### 保留现有 DOM 节点\n\n在极少数情况下，例如使用开发工具（[如 Symfony web 调试工具栏](https://github.com/symfony/symfony/pull/62970)）时，\n您可能希望保留特定的 DOM 节点。\n为此，请将 `data-frankenphp-hot-reload-preserve` 属性添加到相关的 HTML 元素：\n\n```html\n<div data-frankenphp-hot-reload-preserve><!-- 我的调试栏 --></div>\n```\n\n## Worker 模式\n\n如果您的应用程序在 [Worker 模式](https://frankenphp.dev/docs/worker/)下运行，您的应用程序脚本会保留在内存中。\n这意味着即使浏览器重新加载，您对 PHP 代码的更改也不会立即反映。\n\n为了获得最佳的开发者体验，您应该将 `hot_reload` 与 [worker 指令中的 `watch` 子指令](config.md#watching-for-file-changes)结合使用。\n\n- `hot_reload`：文件更改时刷新**浏览器**\n- `worker.watch`：文件更改时重启 worker\n\n```caddy\nlocalhost\n\nmercure {\n    anonymous\n}\n\nroot public/\nphp_server {\n    hot_reload\n    worker {\n        file /path/to/my_worker.php\n        watch\n    }\n}\n```\n\n## 工作原理\n\n1. **监控**：FrankenPHP 使用底层 [e-dant/watcher 库](https://github.com/e-dant/watcher)（我们贡献了 Go 绑定）监控文件系统中的修改。\n2. **重启 (Worker 模式)**：如果 worker 配置中启用了 `watch`，PHP worker 将重新启动以加载新代码。\n3. **推送**：包含更改文件列表的 JSON 有效载荷被发送到内置的 [Mercure hub](https://mercure.rocks)。\n4. **接收**：浏览器通过 JavaScript 库监听，接收 Mercure 事件。\n5. **更新**：\n\n    - 如果检测到 **Idiomorph**，它会获取更新的内容并修改当前的 HTML 以匹配新状态，即时应用更改而不会丢失状态。\n    - 否则，将调用 `window.location.reload()` 来刷新页面。\n"
  },
  {
    "path": "docs/cn/known-issues.md",
    "content": "# 已知问题\n\n## 不支持的 PHP 扩展\n\n已知以下扩展与 FrankenPHP 不兼容：\n\n| 名称                                                                                                        | 原因         | 替代方案                                                                                                             |\n| ----------------------------------------------------------------------------------------------------------- | ------------ | -------------------------------------------------------------------------------------------------------------------- |\n| [imap](https://www.php.net/manual/en/imap.installation.php)                                                 | 不安全的线程 | [javanile/php-imap2](https://github.com/javanile/php-imap2), [webklex/php-imap](https://github.com/Webklex/php-imap) |\n| [newrelic](https://docs.newrelic.com/docs/apm/agents/php-agent/getting-started/introduction-new-relic-php/) | 不安全的线程 | -                                                                                                                    |\n\n## 有缺陷的 PHP 扩展\n\n以下扩展在与 FrankenPHP 一起使用时已知存在错误和意外行为：\n\n| 名称                                                          | 问题                                                                                                                                                                                                                                      |\n| ------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [ext-openssl](https://www.php.net/manual/en/book.openssl.php) | 在使用静态构建的 FrankenPHP（使用 musl libc 构建）时，在重负载下 OpenSSL 扩展可能会崩溃。一个解决方法是使用动态链接的构建（如 Docker 镜像中使用的版本）。此错误正在由 PHP 跟踪。[查看问题](https://github.com/php/php-src/issues/13648)。 |\n\n## get_browser\n\n[get_browser()](https://www.php.net/manual/en/function.get-browser.php) 函数在一段时间后似乎表现不佳。解决方法是缓存（例如使用 [APCu](https://www.php.net/manual/zh/book.apcu.php)）每个 User-Agent，因为它们是不变的。\n\n## 独立的二进制和基于 Alpine 的 Docker 镜像\n\n独立的二进制文件和基于 Alpine 的 Docker 镜像 (`dunglas/frankenphp:*-alpine`) 使用的是 [musl libc](https://musl.libc.org/) 而不是 [glibc and friends](https://www.etalabs.net/compare_libcs.html)，为的是保持较小的二进制大小。这可能会导致一些兼容性问题。特别是，glob 标志 `GLOB_BRACE` [不可用](https://www.php.net/manual/en/function.glob.php)。\n\n## 在 Docker 中使用 `https://127.0.0.1`\n\n默认情况下，FrankenPHP 会为 `localhost` 生成一个 TLS 证书。\n这是本地开发最简单且推荐的选项。\n\n如果确实想使用 `127.0.0.1` 作为主机，可以通过将服务器名称设置为 `127.0.0.1` 来配置它以为其生成证书。\n\n如果你使用 Docker，因为 [Docker 网络](https://docs.docker.com/network/) 问题，只做这些是不够的。\n你将收到类似于以下内容的 TLS 错误 `curl: (35) LibreSSL/3.3.6: error:1404B438:SSL routines:ST_CONNECT:tlsv1 alert internal error`。\n\n如果你使用的是 Linux，解决方案是使用 [使用宿主机网络](https://docs.docker.com/network/network-tutorial-host/)：\n\n```console\ndocker run \\\n    -e SERVER_NAME=\"127.0.0.1\" \\\n    -v $PWD:/app/public \\\n    --network host \\\n    dunglas/frankenphp\n```\n\nMac 和 Windows 不支持 Docker 使用宿主机网络。在这些平台上，你必须猜测容器的 IP 地址并将其包含在服务器名称中。\n\n运行 `docker network inspect bridge` 并查看 `Containers`，找到 `IPv4Address` 当前分配的最后一个 IP 地址，并增加 1。如果没有容器正在运行，则第一个分配的 IP 地址通常为 `172.17.0.2`。\n\n然后将其包含在 `SERVER_NAME` 环境变量中：\n\n```console\ndocker run \\\n    -e SERVER_NAME=\"127.0.0.1, 172.17.0.3\" \\\n    -v $PWD:/app/public \\\n    -p 80:80 -p 443:443 -p 443:443/udp \\\n    dunglas/frankenphp\n```\n\n> [!CAUTION]\n>\n> 请务必将 `172.17.0.3` 替换为将分配给容器的 IP。\n\n你现在应该能够从主机访问 `https://127.0.0.1`。\n\n如果不是这种情况，请在调试模式下启动 FrankenPHP 以尝试找出问题：\n\n```console\ndocker run \\\n    -e CADDY_GLOBAL_OPTIONS=\"debug\" \\\n    -e SERVER_NAME=\"127.0.0.1\" \\\n    -v $PWD:/app/public \\\n    -p 80:80 -p 443:443 -p 443:443/udp \\\n    dunglas/frankenphp\n```\n\n## Composer 脚本引用 `@php`\n\n[Composer 脚本](https://getcomposer.org/doc/articles/scripts.md) 可能想要执行一个 PHP 二进制文件来完成一些任务，例如在 [Laravel 项目](laravel.md) 中运行 `@php artisan package:discover --ansi`。这 [目前失败](https://github.com/php/frankenphp/issues/483#issuecomment-1899890915) 的原因有两个：\n\n- Composer 不知道如何调用 FrankenPHP 二进制文件；\n- Composer 可以在命令中使用 `-d` 标志添加 PHP 设置，而 FrankenPHP 目前尚不支持。\n\n作为一种变通方法，我们可以在 `/usr/local/bin/php` 中创建一个 Shell 脚本，该脚本会去掉不支持的参数，然后调用 FrankenPHP:\n\n```bash\n#!/usr/bin/env bash\nargs=(\"$@\")\nindex=0\nfor i in \"$@\"\ndo\n    if [ \"$i\" == \"-d\" ]; then\n        unset 'args[$index]'\n        unset 'args[$index+1]'\n    fi\n    index=$((index+1))\ndone\n\n/usr/local/bin/frankenphp php-cli ${args[@]}\n```\n\n然后将环境变量 `PHP_BINARY` 设置为我们 `php` 脚本的路径，并运行 Composer：\n\n```console\nexport PHP_BINARY=/usr/local/bin/php\ncomposer install\n```\n\n## 使用静态二进制文件排查 TLS/SSL 问题\n\n在使用静态二进制文件时，您可能会遇到以下与TLS相关的错误，例如在使用STARTTLS发送电子邮件时：\n\n```text\nUnable to connect with STARTTLS: stream_socket_enable_crypto(): SSL operation failed with code 5. OpenSSL Error messages:\nerror:80000002:system library::No such file or directory\nerror:80000002:system library::No such file or directory\nerror:80000002:system library::No such file or directory\nerror:0A000086:SSL routines::certificate verify failed\n```\n\n由于静态二进制不捆绑 TLS 证书，因此您需要将 OpenSSL 指向本地 CA 证书安装。\n\n检查 [`openssl_get_cert_locations()`](https://www.php.net/manual/en/function.openssl-get-cert-locations.php) 的输出，\n以找到 CA 证书必须安装的位置，并将它们存储在该位置。\n\n> [!WARNING]\n>\n> Web 和命令行界面可能有不同的设置。\n> 确保在适当的上下文中运行 `openssl_get_cert_locations()`。\n\n[从Mozilla提取的CA证书可以在curl网站上下载](https://curl.se/docs/caextract.html)。\n\n或者，许多发行版，包括 Debian、Ubuntu 和 Alpine，提供名为 `ca-certificates` 的软件包，其中包含这些证书。\n\n还可以使用 `SSL_CERT_FILE` 和 `SSL_CERT_DIR` 来提示 OpenSSL 在哪里查找 CA 证书：\n\n```console\n# Set TLS certificates environment variables\nexport SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt\nexport SSL_CERT_DIR=/etc/ssl/certs\n```\n"
  },
  {
    "path": "docs/cn/laravel.md",
    "content": "# Laravel\n\n## Docker\n\n使用 FrankenPHP 为 [Laravel](https://laravel.com) Web 应用程序提供服务就像将项目挂载到官方 Docker 镜像的 `/app` 目录中一样简单。\n\n从 Laravel 应用程序的主目录运行以下命令：\n\n```console\ndocker run -p 80:80 -p 443:443 -p 443:443/udp -v $PWD:/app dunglas/frankenphp\n```\n\n尽情享受吧！\n\n## 本地安装\n\n或者，你可以从本地机器上使用 FrankenPHP 运行 Laravel 项目：\n\n1. [下载与你的系统相对应的二进制文件](https://github.com/php/frankenphp/releases)\n2. 将以下配置添加到 Laravel 项目根目录中名为 `Caddyfile` 的文件中：\n\n   ```caddyfile\n   {\n   \tfrankenphp\n   }\n\n   # 服务器的域名\n   localhost {\n   \t# 将 webroot 设置为 public/ 目录\n   \troot public/\n   \t# 启用压缩(可选)\n   \tencode zstd br gzip\n   \t# 执行当前目录中的 PHP 文件并提供资源\n   \tphp_server {\n   \t    try_files {path} index.php\n       }\n   }\n   ```\n\n3. 从 Laravel 项目的根目录启动 FrankenPHP：`frankenphp run`\n\n## Laravel Octane\n\nOctane 可以通过 Composer 包管理器安装：\n\n```console\ncomposer require laravel/octane\n```\n\n安装 Octane 后，你可以执行 `octane:install` Artisan 命令，该命令会将 Octane 的配置文件安装到你的应用程序中：\n\n```console\nphp artisan octane:install --server=frankenphp\n```\n\nOctane 服务可以通过 `octane:frankenphp` Artisan 命令启动。\n\n```console\nphp artisan octane:frankenphp\n```\n\n`octane:frankenphp` 命令可以采用以下选项：\n\n- `--host`: 服务器应绑定到的 IP 地址（默认值: `127.0.0.1`）\n- `--port`: 服务器应可用的端口（默认值: `8000`）\n- `--admin-port`: 管理服务器应可用的端口（默认值: `2019`）\n- `--workers`: 应可用于处理请求的 worker 数（默认值: `auto`）\n- `--max-requests`: 在 worker 重启之前要处理的请求数（默认值: `500`）\n- `--caddyfile`：FrankenPHP `Caddyfile` 文件的路径（默认： [Laravel Octane 中的存根 `Caddyfile`](https://github.com/laravel/octane/blob/2.x/src/Commands/stubs/Caddyfile)）\n- `--https`: 开启 HTTPS、HTTP/2 和 HTTP/3，自动生成和延长证书\n- `--http-redirect`: 启用 HTTP 到 HTTPS 重定向（仅在使用 `--https` 时启用）\n- `--watch`: 修改应用程序时自动重新加载服务器\n- `--poll`: 在监视时使用文件系统轮询，以便通过网络监视文件\n- `--log-level`: 在指定日志级别或高于指定日志级别的日志消息\n\n> [!TIP]\n> 要获取结构化的 JSON 日志（在使用日志分析解决方案时非常有用），请明确传递 `--log-level` 选项。\n\n你可以了解更多关于 [Laravel Octane 官方文档](https://laravel.com/docs/octane)。\n\n## Laravel 应用程序作为独立的可执行文件\n\n使用[FrankenPHP 的应用嵌入功能](embed.md)，可以将 Laravel 应用程序作为\n独立的二进制文件分发。\n\n按照以下步骤将您的Laravel应用程序打包为Linux的独立二进制文件：\n\n1. 在您的应用程序的存储库中创建一个名为 `static-build.Dockerfile` 的文件:\n\n   ```dockerfile\n   FROM --platform=linux/amd64 dunglas/frankenphp:static-builder-gnu\n   # 如果你打算在 musl-libc 系统上运行该二进制文件，请使用 static-builder-musl\n\n   # 复制你的应用\n   WORKDIR /go/src/app/dist/app\n   COPY . .\n\n   # 删除测试和其他不必要的文件以节省空间\n   # 或者，将这些文件添加到 .dockerignore 文件中\n   RUN rm -Rf tests/\n\n   # 复制 .env 文件\n   RUN cp .env.example .env\n   # 将 APP_ENV 和 APP_DEBUG 更改为适合生产环境\n   RUN sed -i'' -e 's/^APP_ENV=.*/APP_ENV=production/' -e 's/^APP_DEBUG=.*/APP_DEBUG=false/' .env\n\n   # 根据需要对您的 .env 文件进行其他更改\n\n   # 安装依赖项\n   RUN composer install --ignore-platform-reqs --no-dev -a\n\n   # 构建静态二进制文件\n   WORKDIR /go/src/app/\n   RUN EMBED=dist/app/ ./build-static.sh\n   ```\n\n   > [!CAUTION]\n   >\n   > 一些 `.dockerignore` 文件\n   > 将忽略 `vendor/` 目录和 `.env` 文件。在构建之前，请确保调整或删除 `.dockerignore` 文件。\n\n2. 构建:\n\n   ```console\n   docker build -t static-laravel-app -f static-build.Dockerfile .\n   ```\n\n3. 提取二进制:\n\n   ```console\n   docker cp $(docker create --name static-laravel-app-tmp static-laravel-app):/go/src/app/dist/frankenphp-linux-x86_64 frankenphp ; docker rm static-laravel-app-tmp\n   ```\n\n4. 填充缓存：\n\n   ```console\n   frankenphp php-cli artisan optimize\n   ```\n\n5. 运行数据库迁移（如果有的话）：\n\n   ```console\n   frankenphp php-cli artisan migrate\n   ```\n\n6. 生成应用程序的密钥：\n\n   ```console\n   frankenphp php-cli artisan key:generate\n   ```\n\n7. 启动服务器：\n\n   ```console\n   frankenphp php-server\n   ```\n\n您的应用程序现在准备好了！\n\n了解有关可用选项的更多信息，以及如何为其他操作系统构建二进制文件，请参见 [应用程序嵌入](embed.md)\n文档。\n\n### 更改存储路径\n\n默认情况下，Laravel 将上传的文件、缓存、日志等存储在应用程序的 `storage/` 目录中。\n这不适合嵌入式应用，因为每个新版本将被提取到不同的临时目录中。\n\n设置 `LARAVEL_STORAGE_PATH` 环境变量（例如，在 `.env` 文件中）或调用 `Illuminate\\Foundation\\Application::useStoragePath()` 方法以使用临时目录之外的目录。\n\n### 使用独立二进制文件运行 Octane\n\n甚至可以将 Laravel Octane 应用打包为独立的二进制文件！\n\n为此，[正确安装 Octane](#laravel-octane) 并遵循 [前一部分](#laravel-应用程序作为独立的可执行文件) 中描述的步骤。\n\n然后，通过 Octane 在工作模式下启动 FrankenPHP，运行：\n\n```console\nPATH=\"$PWD:$PATH\" frankenphp php-cli artisan octane:frankenphp\n```\n\n> [!CAUTION]\n>\n> 为了使命令有效，独立二进制文件**必须**命名为 `frankenphp`\n> 因为 Octane 需要一个名为 `frankenphp` 的程序在路径中可用。\n"
  },
  {
    "path": "docs/cn/mercure.md",
    "content": "# 实时\n\nFrankenPHP 配备了内置的 [Mercure](https://mercure.rocks) 中心！\nMercure 允许将事件实时推送到所有连接的设备：它们将立即收到 JavaScript 事件。\n\n无需 JS 库或 SDK！\n\n![Mercure](../mercure-hub.png)\n\n要启用 Mercure Hub，请按照 [Mercure 网站](https://mercure.rocks/docs/hub/config) 中的说明更新 `Caddyfile`。\n\nMercure hub 的路径是`/.well-known/mercure`.\n在 Docker 中运行 FrankenPHP 时，完整的发送 URL 将类似于 `http://php/.well-known/mercure` （其中 `php` 是运行 FrankenPHP 的容器名称）。\n\n要从你的代码中推送 Mercure 更新，我们推荐 [Symfony Mercure Component](https://symfony.com/components/Mercure)（不需要 Symfony 框架来使用）。\n"
  },
  {
    "path": "docs/cn/metrics.md",
    "content": "# 指标\n\n当启用 [Caddy 指标](https://caddyserver.com/docs/metrics) 时，FrankenPHP 公开以下指标：\n\n- `frankenphp_total_threads`：PHP 线程的总数。\n- `frankenphp_busy_threads`：当前正在处理请求的 PHP 线程数（运行中的 worker 始终占用一个线程）。\n- `frankenphp_queue_depth`：常规排队请求的数量\n- `frankenphp_total_workers{worker=\"[worker_name]\"}`：worker 的总数。\n- `frankenphp_busy_workers{worker=\"[worker_name]\"}`：当前正在处理请求的 worker 数量。\n- `frankenphp_worker_request_time{worker=\"[worker_name]\"}`：所有 worker 处理请求所花费的时间。\n- `frankenphp_worker_request_count{worker=\"[worker_name]\"}`：所有 worker 处理的请求数量。\n- `frankenphp_ready_workers{worker=\"[worker_name]\"}`：至少调用过一次 `frankenphp_handle_request` 的 worker 数量。\n- `frankenphp_worker_crashes{worker=\"[worker_name]\"}`：worker 意外终止的次数。\n- `frankenphp_worker_restarts{worker=\"[worker_name]\"}`：worker 被故意重启的次数。\n- `frankenphp_worker_queue_depth{worker=\"[worker_name]\"}`：排队请求的数量。\n\n对于 worker 指标，`[worker_name]` 占位符被 Caddyfile 中的 worker 名称替换，否则将使用 worker 文件的绝对路径。\n"
  },
  {
    "path": "docs/cn/performance.md",
    "content": "# 性能\n\n默认情况下，FrankenPHP 尝试在性能和易用性之间提供良好的折衷。\n但是，通过使用适当的配置，可以大幅提高性能。\n\n## 线程和 Worker 数量\n\n默认情况下，FrankenPHP 启动的线程和 worker（在 worker 模式下）数量是可用 CPU 核心数的 2 倍。\n\n适当的值很大程度上取决于你的应用程序是如何编写的、它做什么以及你的硬件。\n我们强烈建议更改这些值。为了获得最佳的系统稳定性，建议 `num_threads` x `memory_limit` < `available_memory`。\n\n要找到正确的值，最好运行模拟真实流量的负载测试。\n[k6](https://k6.io) 和 [Gatling](https://gatling.io) 是很好的工具。\n\n要配置线程数，请使用 `php_server` 和 `php` 指令的 `num_threads` 选项。\n要更改 worker 数量，请使用 `frankenphp` 指令的 `worker` 部分的 `num` 选项。\n\n### `max_threads`\n\n虽然准确了解你的流量情况总是更好，但现实应用往往更加\n不可预测。`max_threads` [配置](config.md#caddyfile-config) 允许 FrankenPHP 在运行时自动生成额外线程，直到指定的限制。\n`max_threads` 可以帮助你确定需要多少线程来处理你的流量，并可以使服务器对延迟峰值更具弹性。\n如果设置为 `auto`，限制将基于你的 `php.ini` 中的 `memory_limit` 进行估算。如果无法这样做，\n`auto` 将默认为 2x `num_threads`。请记住，`auto` 可能会严重低估所需的线程数。\n`max_threads` 类似于 PHP FPM 的 [pm.max_children](https://www.php.net/manual/en/install.fpm.configuration.php#pm.max-children)。主要区别是 FrankenPHP 使用线程而不是\n进程，并根据需要自动在不同的 worker 脚本和\"经典模式\"之间委派它们。\n\n## Worker 模式\n\n启用 [worker 模式](worker.md) 大大提高了性能，\n但你的应用必须适配以兼容此模式：\n你需要创建一个 worker 脚本并确保应用不会泄漏内存。\n\n## 不要使用 musl\n\n官方 Docker 镜像的 Alpine Linux 变体和我们提供的默认二进制文件使用 [musl libc](https://musl.libc.org)。\n\n众所周知，当使用这个替代 C 库而不是传统的 GNU 库时，PHP [更慢](https://gitlab.alpinelinux.org/alpine/aports/-/issues/14381)，\n特别是在以 ZTS 模式（线程安全）编译时，这是 FrankenPHP 所必需的。在大量线程环境中，差异可能很显著。\n\n另外，[一些错误只在使用 musl 时发生](https://github.com/php/php-src/issues?q=sort%3Aupdated-desc+is%3Aissue+is%3Aopen+label%3ABug+musl)。\n\n在生产环境中，我们建议使用链接到 glibc 的 FrankenPHP，并使用适当的优化级别进行编译。\n\n这可以通过使用 Debian Docker 镜像、使用[我们的维护者提供的 .deb、.rpm 或 .apk 包](https://pkgs.henderkes.com)，或通过[从源代码编译 FrankenPHP](compile.md) 来实现。\n\n对于更精简或更安全的容器，你可能需要考虑使用[强化的 Debian 镜像](docker.md#hardening-images)而不是 Alpine。\n\n## Go 运行时配置\n\nFrankenPHP 是用 Go 编写的。\n\n一般来说，Go 运行时不需要任何特殊配置，但在某些情况下，\n特定的配置可以提高性能。\n\n你可能想要将 `GODEBUG` 环境变量设置为 `cgocheck=0`（FrankenPHP Docker 镜像中的默认值）。\n\n如果你在容器（Docker、Kubernetes、LXC...）中运行 FrankenPHP 并限制容器的可用内存，\n请将 `GOMEMLIMIT` 环境变量设置为可用内存量。\n\n有关更多详细信息，[专门针对此主题的 Go 文档页面](https://pkg.go.dev/runtime#hdr-Environment_Variables) 是充分利用运行时的必读内容。\n\n## `file_server`\n\n默认情况下，`php_server` 指令自动设置文件服务器来\n提供存储在根目录中的静态文件（资产）。\n\n此功能很方便，但有成本。\n要禁用它，请使用以下配置：\n\n```caddyfile\nphp_server {\n    file_server off\n}\n```\n\n## `try_files`\n\n除了静态文件和 PHP 文件外，`php_server` 还会尝试提供你应用程序的索引\n和目录索引文件（`/path/` -> `/path/index.php`）。如果你不需要目录索引，\n你可以通过明确定义 `try_files` 来禁用它们，如下所示：\n\n```caddyfile\nphp_server {\n    try_files {path} index.php\n    root /root/to/your/app # 在这里明确添加根目录允许更好的缓存\n}\n```\n\n这可以显著减少不必要的文件操作数量。\n上述配置的 worker 等效项为：\n\n```caddyfile\nroute {\n    php_server { # 如果完全不需要文件服务器，请使用 \"php\" 而不是 \"php_server\"\n        root /root/to/your/app\n        worker /path/to/worker.php {\n            match * # 将所有请求直接发送到 worker\n        }\n    }\n}\n```\n\n另一种具有 0 个不必要文件系统操作的方法是改用 `php` 指令并按路径将\n文件与 PHP 分开。如果你的整个应用程序由一个入口文件提供服务，这种方法效果很好。\n一个在 `/assets` 文件夹后面提供静态文件的示例[配置](config.md#caddyfile-config)可能如下所示：\n\n```caddyfile\nroute {\n    @assets {\n        path /assets/*\n    }\n\n    # /assets 后面的所有内容都由文件服务器处理\n    file_server @assets {\n        root /root/to/your/app\n    }\n\n    # 不在 /assets 中的所有内容都由你的索引或 worker PHP 文件处理\n    rewrite index.php\n    php {\n        root /root/to/your/app # 在这里明确添加根目录允许更好的缓存\n    }\n}\n```\n\n## 占位符\n\n你可以在 `root` 和 `env` 指令中使用[占位符](https://caddyserver.com/docs/conventions#placeholders)。\n但是，这会阻止缓存这些值，并带来显著的性能成本。\n\n如果可能，请避免在这些指令中使用占位符。\n\n## `resolve_root_symlink`\n\n默认情况下，如果文档根目录是符号链接，FrankenPHP 会自动解析它（这对于 PHP 正常工作是必要的）。\n如果文档根目录不是符号链接，你可以禁用此功能。\n\n```caddyfile\nphp_server {\n    resolve_root_symlink false\n}\n```\n\n如果 `root` 指令包含[占位符](https://caddyserver.com/docs/conventions#placeholders)，这将提高性能。\n在其他情况下，收益将可以忽略不计。\n\n## 日志\n\n日志显然非常有用，但根据定义，\n它需要 I/O 操作和内存分配，这会大大降低性能。\n确保你[正确设置日志级别](https://caddyserver.com/docs/caddyfile/options#log)，\n并且只记录必要的内容。\n\n## PHP 性能\n\nFrankenPHP 使用官方 PHP 解释器。\n所有常见的 PHP 相关性能优化都适用于 FrankenPHP。\n\n特别是：\n\n- 检查 [OPcache](https://www.php.net/manual/zh/book.opcache.php) 是否已安装、启用并正确配置\n- 启用 [Composer 自动加载器优化](https://getcomposer.org/doc/articles/autoloader-optimization.md)\n- 确保 `realpath` 缓存对于你的应用程序需求足够大\n- 使用[预加载](https://www.php.net/manual/zh/opcache.preloading.php)\n\n有关更多详细信息，请阅读[专门的 Symfony 文档条目](https://symfony.com/doc/current/performance.html)\n（即使你不使用 Symfony，大多数提示也很有用）。\n\n## 拆分线程池\n\n应用程序与慢速外部服务交互是很常见的，例如在高负载下往往不可靠或持续需要 10 秒以上才能响应的 API。\n在这种情况下，将线程池拆分以拥有专用的“慢速”池可能会很有益。这可以防止慢速端点消耗所有服务器资源/线程，并限制指向慢速端点的请求并发性，类似于连接池。\n\n```caddyfile\nexample.com {\n    php_server {\n        root /app/public # 你的应用程序根目录\n        worker index.php {\n            match /slow-endpoint/* # 所有路径为 /slow-endpoint/* 的请求都由这个线程池处理\n            num 1 # 匹配 /slow-endpoint/* 的请求至少有 1 个线程\n            max_threads 20 # 如果需要，允许最多 20 个线程处理匹配 /slow-endpoint/* 的请求\n        }\n        worker index.php {\n            match * # 所有其他请求单独处理\n            num 1 # 其他请求至少有 1 个线程，即使慢速端点开始挂起\n            max_threads 20 # 如果需要，允许最多 20 个线程处理其他请求\n        }\n    }\n}\n```\n\n通常，也建议通过使用消息队列等相关机制，异步处理非常慢的端点。\n"
  },
  {
    "path": "docs/cn/production.md",
    "content": "# 在生产环境中部署\n\n在本教程中，我们将学习如何使用 Docker Compose 在单个服务器上部署 PHP 应用程序。\n\n如果你使用的是 Symfony，请阅读 Symfony Docker 项目（使用 FrankenPHP）的 [在生产环境中部署](https://github.com/dunglas/symfony-docker/blob/main/docs/production.md) 文档条目。\n\n如果你使用的是 API Platform（同样使用 FrankenPHP），请参阅 [框架的部署文档](https://api-platform.com/docs/deployment/)。\n\n## 准备应用\n\n首先，在 PHP 项目的根目录中创建一个 `Dockerfile`：\n\n```dockerfile\nFROM dunglas/frankenphp\n\n# 请将 \"your-domain-name.example.com\" 替换为你的域名\nENV SERVER_NAME=your-domain-name.example.com\n# 如果要禁用 HTTPS，请改用以下值：\n#ENV SERVER_NAME=:80\n\n# 如果你的项目不使用 \"public\" 目录作为 web 根目录，你可以在这里设置：\n# ENV SERVER_ROOT=web/\n\n# 启用 PHP 生产配置\nRUN mv \"$PHP_INI_DIR/php.ini-production\" \"$PHP_INI_DIR/php.ini\"\n\n# 将项目的 PHP 文件复制到 public 目录中\nCOPY . /app/public\n# 如果你使用 Symfony 或 Laravel，你需要复制整个项目：\n#COPY . /app\n```\n\n有关更多详细信息和选项，请参阅 [构建自定义 Docker 镜像](docker.md)。\n要了解如何自定义配置，请安装 PHP 扩展和 Caddy 模块。\n\n如果你的项目使用 Composer，\n请务必将其包含在 Docker 镜像中并安装你的依赖。\n\n然后，添加一个 `compose.yaml` 文件：\n\n```yaml\nservices:\n  php:\n    image: dunglas/frankenphp\n    restart: always\n    ports:\n      - \"80:80\" # HTTP\n      - \"443:443\" # HTTPS\n      - \"443:443/udp\" # HTTP/3\n    volumes:\n      - caddy_data:/data\n      - caddy_config:/config\n\n# Caddy 证书和配置所需的挂载目录\nvolumes:\n  caddy_data:\n  caddy_config:\n```\n\n> [!NOTE]\n>\n> 前面的示例适用于生产用途。\n> 在开发中，你可能希望使用挂载目录，不同的 PHP 配置和不同的 `SERVER_NAME` 环境变量值。\n>\n> 见 [Symfony Docker](https://github.com/dunglas/symfony-docker) 项目\n> （使用 FrankenPHP）作为使用多阶段镜像的更高级示例，\n> Composer、额外的 PHP 扩展等。\n\n最后，如果你使用 Git，请提交这些文件并推送。\n\n## 准备服务器\n\n若要在生产环境中部署应用程序，需要一台服务器。\n在本教程中，我们将使用 DigitalOcean 提供的虚拟机，但任何 Linux 服务器都可以工作。\n如果你已经有安装了 Docker 的 Linux 服务器，你可以直接跳到 [下一节](#配置域名)。\n\n否则，请使用 [此会员链接](https://m.do.co/c/5d8aabe3ab80) 获得 200 美元的免费信用额度，创建一个帐户，然后单击“Create a Droplet”。\n然后，单击“Choose an image”部分下的“Marketplace”选项卡，然后搜索名为“Docker”的应用程序。\n这将配置已安装最新版本的 Docker 和 Docker Compose 的 Ubuntu 服务器！\n\n出于测试目的，最便宜的就足够了。\n对于实际的生产用途，你可能需要在“general purpose”部分中选择一个计划来满足你的需求。\n\n![使用 Docker 在 DigitalOcean 上部署 FrankenPHP](../digitalocean-droplet.png)\n\n你可以保留其他设置的默认值，也可以根据需要进行调整。\n不要忘记添加你的 SSH 密钥或创建密码，然后点击“完成并创建”按钮。\n\n然后，在 Droplet 预配时等待几秒钟。\nDroplet 准备就绪后，使用 SSH 进行连接：\n\n```console\nssh root@<droplet-ip>\n```\n\n## 配置域名\n\n在大多数情况下，你需要将域名与你的网站相关联。\n如果你还没有域名，则必须通过注册商购买。\n\n然后为你的域名创建类型为 `A` 的 DNS 记录，指向服务器的 IP 地址：\n\n```dns\nyour-domain-name.example.com.  IN  A     207.154.233.113\n```\n\nDigitalOcean 域服务示例（“Networking” > “Domains”）：\n\n![在 DigitalOcean 上配置 DNS](../digitalocean-dns.png)\n\n> [!NOTE]\n>\n> Let's Encrypt 是 FrankenPHP 默认用于自动生成 TLS 证书的服务，不支持使用裸 IP 地址。使用域名是使用 Let's Encrypt 的必要条件。\n\n## 部署\n\n使用 `git clone`、`scp` 或任何其他可能适合你需要的工具在服务器上复制你的项目。\n如果使用 GitHub，则可能需要使用 [部署密钥](https://docs.github.com/en/free-pro-team@latest/developers/overview/managing-deploy-keys#deploy-keys)。\n部署密钥也 [由 GitLab 支持](https://docs.gitlab.com/ee/user/project/deploy_keys/)。\n\nGit 示例：\n\n```console\ngit clone git@github.com:<username>/<project-name>.git\n```\n\n进入包含项目 (`<project-name>`) 的目录，并在生产模式下启动应用：\n\n```console\ndocker compose up --wait\n```\n\n你的服务器已启动并运行，并且已自动为你生成 HTTPS 证书。\n去 `https://your-domain-name.example.com` 享受吧！\n\n> [!CAUTION]\n>\n> Docker 有一个缓存层，请确保每个部署都有正确的构建，或者使用 `--no-cache` 选项重新构建项目以避免缓存问题。\n\n## 在多个节点上部署\n\n如果要在计算机集群上部署应用程序，可以使用 [Docker Swarm](https://docs.docker.com/engine/swarm/stack-deploy/)，\n它与提供的 Compose 文件兼容。\n要在 Kubernetes 上部署，请查看 [API 平台提供的 Helm 图表](https://api-platform.com/docs/deployment/kubernetes/)，同样也使用 FrankenPHP。\n"
  },
  {
    "path": "docs/cn/static.md",
    "content": "# 创建静态构建\n\n与其使用本地安装的PHP库，\n由于伟大的 [static-php-cli 项目](https://github.com/crazywhalecc/static-php-cli)，创建一个静态或基本静态的 FrankenPHP 构建是可能的（尽管它的名字，这个项目支持所有的 SAPI，而不仅仅是 CLI）。\n\n使用这种方法，我们可构建一个包含 PHP 解释器、Caddy Web 服务器和 FrankenPHP 的可移植二进制文件！\n\n完全静态的本地可执行文件不需要任何依赖，并且可以在 [`scratch` Docker 镜像](https://docs.docker.com/build/building/base-images/#create-a-minimal-base-image-using-scratch) 上运行。\n然而，它们无法加载动态 PHP 扩展（例如 Xdebug），并且由于使用了 musl libc，有一些限制。\n\n大多数静态二进制文件只需要 `glibc` 并且可以加载动态扩展。\n\n在可能的情况下，我们建议使用基于glibc的、主要是静态构建的版本。\n\nFrankenPHP 还支持 [将 PHP 应用程序嵌入到静态二进制文件中](embed.md)。\n\n## Linux\n\n我们提供了一个 Docker 镜像来构建 Linux 静态二进制文件：\n\n### 基于musl的完全静态构建\n\n对于一个在任何Linux发行版上运行且不需要依赖项的完全静态二进制文件，但不支持动态加载扩展：\n\n```console\ndocker buildx bake --load static-builder-musl\ndocker cp $(docker create --name static-builder-musl dunglas/frankenphp:static-builder-musl):/go/src/app/dist/frankenphp-linux-$(uname -m) frankenphp ; docker rm static-builder-musl\n```\n\n为了在高度并发的场景中获得更好的性能，请考虑使用 [mimalloc](https://github.com/microsoft/mimalloc) 分配器。\n\n```console\ndocker buildx bake --load --set static-builder-musl.args.MIMALLOC=1 static-builder-musl\n```\n\n### 基于glibc的，主要静态构建（支持动态扩展）\n\n对于一个支持动态加载 PHP 扩展的二进制文件，同时又将所选扩展静态编译：\n\n```console\ndocker buildx bake --load static-builder-gnu\ndocker cp $(docker create --name static-builder-gnu dunglas/frankenphp:static-builder-gnu):/go/src/app/dist/frankenphp-linux-$(uname -m) frankenphp ; docker rm static-builder-gnu\n```\n\n该二进制文件支持所有glibc版本2.17及以上，但不支持基于musl的系统（如Alpine Linux）。\n\n生成的主要是静态的（除了 `glibc`）二进制文件名为 `frankenphp`，并且可以在当前目录中找到。\n\n如果你想在没有 Docker 的情况下构建静态二进制文件，请查看 macOS 说明，它也适用于 Linux。\n\n### 自定义扩展\n\n默认情况下，大多数流行的 PHP 扩展都会被编译。\n\n为了减少二进制文件的大小和减少攻击面，您可以选择使用 `PHP_EXTENSIONS` Docker ARG 构建的扩展列表。\n\n例如，运行以下命令仅构建 `opcache` 扩展：\n\n```console\ndocker buildx bake --load --set static-builder-musl.args.PHP_EXTENSIONS=opcache,pdo_sqlite static-builder-musl\n# ...\n```\n\n若要将启用其他功能的库添加到已启用的扩展中，可以使用 `PHP_EXTENSION_LIBS` Docker 参数：\n\n```console\ndocker buildx bake \\\n  --load \\\n  --set static-builder-musl.args.PHP_EXTENSIONS=gd \\\n  --set static-builder-musl.args.PHP_EXTENSION_LIBS=libjpeg,libwebp \\\n  static-builder-musl\n```\n\n### 额外的 Caddy 模块\n\n要向 [xcaddy](https://github.com/caddyserver/xcaddy) 添加额外的 Caddy 模块或传递其他参数，请使用 `XCADDY_ARGS` Docker 参数：\n\n```console\ndocker buildx bake \\\n  --load \\\n  --set static-builder-musl.args.XCADDY_ARGS=\"--with github.com/darkweak/souin/plugins/caddy --with github.com/dunglas/caddy-cbrotli --with github.com/dunglas/mercure/caddy --with github.com/dunglas/vulcain/caddy\" \\\n  static-builder-musl\n```\n\n在本例中，我们为 Caddy 添加了 [Souin](https://souin.io) HTTP 缓存模块，以及 [cbrotli](https://github.com/dunglas/caddy-cbrotli)、[Mercure](https://mercure.rocks) 和 [Vulcain](https://vulcain.rocks) 模块。\n\n> [!TIP]\n>\n> 如果 `XCADDY_ARGS` 为空或未设置，则默认包含 cbrotli、Mercure 和 Vulcain 模块。\n> 如果自定义了 `XCADDY_ARGS` 的值，则必须显式地包含它们。\n\n参见：[自定义构建](#自定义构建)\n\n### GitHub Token\n\n如果遇到了 GitHub API 速率限制，请在 `GITHUB_TOKEN` 的环境变量中设置 GitHub Personal Access Token：\n\n```console\nGITHUB_TOKEN=\"xxx\" docker --load buildx bake static-builder-musl\n# ...\n```\n\n## macOS\n\n运行以下脚本以创建适用于 macOS 的静态二进制文件（需要先安装 [Homebrew](https://brew.sh/)）：\n\n```console\ngit clone https://github.com/php/frankenphp\ncd frankenphp\n./build-static.sh\n```\n\n注意：此脚本也适用于 Linux（可能也适用于其他 Unix 系统），我们提供的用于构建静态二进制的 Docker 镜像也在内部使用这个脚本。\n\n## 自定义构建\n\n以下环境变量可以传递给 `docker build` 和 `build-static.sh`\n脚本来自定义静态构建：\n\n- `FRANKENPHP_VERSION`: 要使用的 FrankenPHP 版本\n- `PHP_VERSION`: 要使用的 PHP 版本\n- `PHP_EXTENSIONS`: 要构建的 PHP 扩展（[支持的扩展列表](https://static-php.dev/zh/guide/extensions.html)）\n- `PHP_EXTENSION_LIBS`: 要构建的额外库，为扩展添加额外的功能\n- `XCADDY_ARGS`：传递给 [xcaddy](https://github.com/caddyserver/xcaddy) 的参数，例如用于添加额外的 Caddy 模块\n- `EMBED`: 要嵌入二进制文件的 PHP 应用程序的路径\n- `CLEAN`: 设置后，libphp 及其所有依赖项都是重新构建的（不使用缓存）\n- `NO_COMPRESS`: 不要使用UPX压缩生成的二进制文件\n- `DEBUG_SYMBOLS`: 设置后，调试符号将被保留在二进制文件内\n- `MIMALLOC`: (实验性，仅限Linux) 用[mimalloc](https://github.com/microsoft/mimalloc)替换musl的mallocng，以提高性能。我们仅建议在musl目标构建中使用此选项，对于glibc，建议禁用此选项，并在运行二进制文件时使用[`LD_PRELOAD`](https://microsoft.github.io/mimalloc/overrides.html)。\n- `RELEASE`: （仅限维护者）设置后，生成的二进制文件将上传到 GitHub 上\n\n## 扩展\n\n使用glibc或基于macOS的二进制文件，您可以动态加载PHP扩展。然而，这些扩展必须使用ZTS支持进行编译。\n由于大多数软件包管理器目前不提供其扩展的 ZTS 版本，因此您必须自己编译它们。\n\n为此，您可以构建并运行 `static-builder-gnu` Docker 容器，远程进入它，并使用 `./configure --with-php-config=/go/src/app/dist/static-php-cli/buildroot/bin/php-config` 编译扩展。\n\n关于 [Xdebug 扩展](https://xdebug.org) 的示例步骤：\n\n```console\ndocker build -t gnu-ext -f static-builder-gnu.Dockerfile --build-arg FRANKENPHP_VERSION=1.0 .\ndocker create --name static-builder-gnu -it gnu-ext /bin/sh\ndocker start static-builder-gnu\ndocker exec -it static-builder-gnu /bin/sh\ncd /go/src/app/dist/static-php-cli/buildroot/bin\ngit clone https://github.com/xdebug/xdebug.git && cd xdebug\nsource scl_source enable devtoolset-10\n../phpize\n./configure --with-php-config=/go/src/app/dist/static-php-cli/buildroot/bin/php-config\nmake\nexit\ndocker cp static-builder-gnu:/go/src/app/dist/static-php-cli/buildroot/bin/xdebug/modules/xdebug.so xdebug-zts.so\ndocker cp static-builder-gnu:/go/src/app/dist/frankenphp-linux-$(uname -m) ./frankenphp\ndocker stop static-builder-gnu\ndocker rm static-builder-gnu\ndocker rmi gnu-ext\n```\n\n这将在当前目录中创建 `frankenphp` 和 `xdebug-zts.so`。\n如果你将 `xdebug-zts.so` 移动到你的扩展目录中，添加 `zend_extension=xdebug-zts.so` 到你的 php.ini 并运行 FrankenPHP，它将加载 Xdebug。\n"
  },
  {
    "path": "docs/cn/worker.md",
    "content": "# 使用 FrankenPHP Workers\n\n启动一次应用程序并将其保存在内存中。\nFrankenPHP 将在几毫秒内处理传入请求。\n\n## 启动 Worker 脚本\n\n### Docker\n\n将 `FRANKENPHP_CONFIG` 环境变量的值设置为 `worker /path/to/your/worker/script.php`：\n\n```console\ndocker run \\\n    -e FRANKENPHP_CONFIG=\"worker /app/path/to/your/worker/script.php\" \\\n    -v $PWD:/app \\\n    -p 80:80 -p 443:443 -p 443:443/udp \\\n    dunglas/frankenphp\n```\n\n### 独立二进制文件\n\n使用 `php-server` 命令的 `--worker` 选项通过 worker 为当前目录的内容提供服务：\n\n```console\nfrankenphp php-server --worker /path/to/your/worker/script.php\n```\n\n如果你的 PHP 应用程序已[嵌入到二进制文件中](embed.md)，你可以在应用程序的根目录中添加自定义的 `Caddyfile`。\n它将被自动使用。\n\n还可以使用 `--watch` 选项在[文件更改时重启 worker](config.md#watching-for-file-changes)。\n如果 `/path/to/your/app/` 目录或子目录中任何以 `.php` 结尾的文件被修改，以下命令将触发重启：\n\n```console\nfrankenphp php-server --worker /path/to/your/worker/script.php --watch=\"/path/to/your/app/**/*.php\"\n```\n\n此功能通常与[热重载](hot-reload.md)结合使用。\n\n## Symfony Runtime\n\n> [!TIP]\n> 以下部分仅在 Symfony 7.4 之前是必需的，因为 Symfony 7.4 引入了对 FrankenPHP worker 模式的原生支持。\n\nFrankenPHP 的 worker 模式由 [Symfony Runtime Component](https://symfony.com/doc/current/components/runtime.html) 支持。\n要在 worker 中启动任何 Symfony 应用程序，请安装 [PHP Runtime](https://github.com/php-runtime/runtime) 的 FrankenPHP 包：\n\n```console\ncomposer require runtime/frankenphp-symfony\n```\n\n通过定义 `APP_RUNTIME` 环境变量来使用 FrankenPHP Symfony Runtime 启动你的应用服务器：\n\n```console\ndocker run \\\n    -e FRANKENPHP_CONFIG=\"worker ./public/index.php\" \\\n    -e APP_RUNTIME=Runtime\\\\FrankenPhpSymfony\\\\Runtime \\\n    -v $PWD:/app \\\n    -p 80:80 -p 443:443 -p 443:443/udp \\\n    dunglas/frankenphp\n```\n\n## Laravel Octane\n\n请参阅[专门的文档](laravel.md#laravel-octane)。\n\n## 自定义应用程序\n\n以下示例展示了如何创建自己的 worker 脚本而不依赖第三方库：\n\n```php\n<?php\n// public/index.php\n\n// 启动你的应用程序\nrequire __DIR__.'/vendor/autoload.php';\n\n$myApp = new \\App\\Kernel();\n$myApp->boot();\n\n// 在循环外的处理器以获得更好的性能（减少工作量）\n$handler = static function () use ($myApp) {\n    try {\n        // 当收到请求时调用，\n        // 超全局变量、php://input 等都会被重置\n        echo $myApp->handle($_GET, $_POST, $_COOKIE, $_FILES, $_SERVER);\n    } catch (\\Throwable $exception) {\n        // `set_exception_handler` 仅在 worker 脚本结束时调用，\n        // 这可能不是您所期望的，因此在此处捕获并处理异常\n        (new \\MyCustomExceptionHandler)->handleException($exception);\n    }\n};\n\n$maxRequests = (int)($_SERVER['MAX_REQUESTS'] ?? 0);\nfor ($nbRequests = 0; !$maxRequests || $nbRequests < $maxRequests; ++$nbRequests) {\n    $keepRunning = \\frankenphp_handle_request($handler);\n\n    // 在发送 HTTP 响应后做一些事情\n    $myApp->terminate();\n\n    // 调用垃圾收集器以减少在页面生成过程中触发垃圾收集的可能性\n    gc_collect_cycles();\n\n    if (!$keepRunning) break;\n}\n\n// 清理\n$myApp->shutdown();\n```\n\n然后，启动你的应用程序并使用 `FRANKENPHP_CONFIG` 环境变量配置你的 worker：\n\n```console\ndocker run \\\n    -e FRANKENPHP_CONFIG=\"worker ./public/index.php\" \\\n    -v $PWD:/app \\\n    -p 80:80 -p 443:443 -p 443:443/udp \\\n    dunglas/frankenphp\n```\n\n默认情况下，每个 CPU 启动 2 个 worker。\n你也可以配置要启动的 worker 数量：\n\n```console\ndocker run \\\n    -e FRANKENPHP_CONFIG=\"worker ./public/index.php 42\" \\\n    -v $PWD:/app \\\n    -p 80:80 -p 443:443 -p 443:443/udp \\\n    dunglas/frankenphp\n```\n\n### 在处理一定数量的请求后重启 Worker\n\n由于 PHP 最初不是为长时间运行的进程而设计的，仍有许多库和传统代码会泄漏内存。\n在 worker 模式下使用此类代码的一个解决方法是在处理一定数量的请求后重启 worker 脚本：\n\n前面的 worker 代码片段允许通过设置名为 `MAX_REQUESTS` 的环境变量来配置要处理的最大请求数。\n\n### 手动重启 Workers\n\n虽然可以在[文件更改时重启 workers](config.md#watching-for-file-changes)，但也可以通过 [Caddy admin API](https://caddyserver.com/docs/api) 优雅地重启所有 workers。如果在你的 [Caddyfile](config.md#caddyfile-config) 中启用了 admin，你可以通过简单的 POST 请求 ping 重启端点，如下所示：\n\n```console\ncurl -X POST http://localhost:2019/frankenphp/workers/restart\n```\n\n### Worker 故障\n\n如果 worker 脚本因非零退出代码而崩溃，FrankenPHP 将使用指数退避策略重启它。\n如果 worker 脚本保持运行的时间超过上次退避 × 2，\n它将不会惩罚 worker 脚本并再次重启它。\n但是，如果 worker 脚本在短时间内继续以非零退出代码失败\n（例如，脚本中有拼写错误），FrankenPHP 将崩溃并出现错误：`too many consecutive failures`。\n\n可以在你的 [Caddyfile](config.md#caddyfile-config) 中使用 `max_consecutive_failures` 选项配置连续失败的次数：\n\n```caddyfile\nfrankenphp {\n    worker {\n        # ...\n        max_consecutive_failures 10\n    }\n}\n```\n\n## 超全局变量行为\n\n[PHP 超全局变量](https://www.php.net/manual/zh/language.variables.superglobals.php)（`$_SERVER`、`$_ENV`、`$_GET`...）\n行为如下：\n\n- 在第一次调用 `frankenphp_handle_request()` 之前，超全局变量包含绑定到 worker 脚本本身的值\n- 在调用 `frankenphp_handle_request()` 期间和之后，超全局变量包含从处理的 HTTP 请求生成的值，每次调用 `frankenphp_handle_request()` 都会更改超全局变量的值\n\n要在回调内访问 worker 脚本的超全局变量，必须复制它们并将副本导入到回调的作用域中：\n\n```php\n<?php\n// 在第一次调用 frankenphp_handle_request() 之前复制 worker 的 $_SERVER 超全局变量\n$workerServer = $_SERVER;\n\n$handler = static function () use ($workerServer) {\n    var_dump($_SERVER); // 与请求绑定的 $_SERVER\n    var_dump($workerServer); // worker 脚本的 $_SERVER\n};\n\n// ...\n"
  },
  {
    "path": "docs/cn/x-sendfile.md",
    "content": "# 高效服务大型静态文件 (`X-Sendfile`/`X-Accel-Redirect`)\n\n通常，静态文件可以直接由 Web 服务器提供服务，\n但有时在发送它们之前需要执行一些 PHP 代码：\n访问控制、统计、自定义 HTTP 头...\n\n不幸的是，与直接使用 Web 服务器相比，使用 PHP 服务大型静态文件效率低下\n（内存过载、性能降低...）。\n\nFrankenPHP 让你在执行自定义 PHP 代码**之后**将静态文件的发送委托给 Web 服务器。\n\n为此，你的 PHP 应用程序只需定义一个包含要服务的文件路径的自定义 HTTP 头。FrankenPHP 处理其余部分。\n\n此功能在 Apache 中称为 **`X-Sendfile`**，在 NGINX 中称为 **`X-Accel-Redirect`**。\n\n在以下示例中，我们假设项目的文档根目录是 `public/` 目录，\n并且我们想要使用 PHP 来服务存储在 `public/` 目录外的文件，\n来自名为 `private-files/` 的目录。\n\n## 配置\n\n首先，将以下配置添加到你的 `Caddyfile` 以启用此功能：\n\n```patch\n\troot public/\n\t# ...\n\n+\t# Symfony、Laravel 和其他使用 Symfony HttpFoundation 组件的项目需要\n+\trequest_header X-Sendfile-Type x-accel-redirect\n+\trequest_header X-Accel-Mapping ../private-files=/private-files\n+\n+\tintercept {\n+\t\t@accel header X-Accel-Redirect *\n+\t\thandle_response @accel {\n+\t\t\troot private-files/\n+\t\t\trewrite * {resp.header.X-Accel-Redirect}\n+\t\t\tmethod * GET\n+\n+\t\t\t# 删除 PHP 设置的 X-Accel-Redirect 头以提高安全性\n+\t\t\theader -X-Accel-Redirect\n+\n+\t\t\tfile_server\n+\t\t}\n+\t}\n\n\tphp_server\n```\n\n## 纯 PHP\n\n将相对文件路径（从 `private-files/`）设置为 `X-Accel-Redirect` 头的值：\n\n```php\nheader('X-Accel-Redirect: file.txt');\n```\n\n## 使用 Symfony HttpFoundation 组件的项目（Symfony、Laravel、Drupal...）\n\nSymfony HttpFoundation [原生支持此功能](https://symfony.com/doc/current/components/http_foundation.html#serving-files)。\n它将自动确定 `X-Accel-Redirect` 头的正确值并将其添加到响应中。\n\n```php\nuse Symfony\\Component\\HttpFoundation\\BinaryFileResponse;\n\nBinaryFileResponse::trustXSendfileTypeHeader();\n$response = new BinaryFileResponse(__DIR__.'/../private-files/file.txt');\n\n// ...\n```\n"
  },
  {
    "path": "docs/compile.md",
    "content": "# Compile From Sources\n\nThis document explains how to create a FrankenPHP binary that will load PHP as a dynamic library.\nThis is the recommended method.\n\nAlternatively, [fully and mostly static builds](static.md) can also be created.\n\n## Install PHP\n\nFrankenPHP is compatible with PHP 8.2 and superior.\n\n### With Homebrew (Linux and Mac)\n\nThe easiest way to install a version of libphp compatible with FrankenPHP is to use the ZTS packages provided by [Homebrew PHP](https://github.com/shivammathur/homebrew-php).\n\nFirst, if not already done, install [Homebrew](https://brew.sh).\n\nThen, install the ZTS variant of PHP, Brotli (optional, for compression support) and watcher (optional, for file change detection):\n\n```console\nbrew install shivammathur/php/php-zts brotli watcher\nbrew link --overwrite --force shivammathur/php/php-zts\n```\n\n### By Compiling PHP\n\nAlternatively, you can compile PHP from sources with the options needed by FrankenPHP by following these steps.\n\nFirst, [get the PHP sources](https://www.php.net/downloads.php) and extract them:\n\n```console\ntar xf php-*\ncd php-*/\n```\n\nThen, run the `configure` script with the options needed for your platform.\nThe following `./configure` flags are mandatory, but you can add others, for example, to compile extensions or additional features.\n\n#### Linux\n\n```console\n./configure \\\n    --enable-embed \\\n    --enable-zts \\\n    --disable-zend-signals \\\n    --enable-zend-max-execution-timers\n```\n\n#### Mac\n\nUse the [Homebrew](https://brew.sh/) package manager to install the required and optional dependencies:\n\n```console\nbrew install libiconv bison brotli re2c pkg-config watcher\necho 'export PATH=\"/opt/homebrew/opt/bison/bin:$PATH\"' >> ~/.zshrc\n```\n\nThen run the configure script:\n\n```console\n./configure \\\n    --enable-embed \\\n    --enable-zts \\\n    --disable-zend-signals \\\n    --with-iconv=/opt/homebrew/opt/libiconv/\n```\n\n#### Compile PHP\n\nFinally, compile and install PHP:\n\n```console\nmake -j\"$(getconf _NPROCESSORS_ONLN)\"\nsudo make install\n```\n\n## Install Optional Dependencies\n\nSome FrankenPHP features depend on optional system dependencies that must be installed.\nAlternatively, these features can be disabled by passing build tags to the Go compiler.\n\n| Feature                        | Dependency                                                                                                   | Build tag to disable it |\n| ------------------------------ | ------------------------------------------------------------------------------------------------------------ | ----------------------- |\n| Brotli compression             | [Brotli](https://github.com/google/brotli)                                                                   | nobrotli                |\n| Restart workers on file change | [Watcher C](https://github.com/e-dant/watcher/tree/release/watcher-c)                                        | nowatcher               |\n| [Mercure](mercure.md)          | [Mercure Go library](https://pkg.go.dev/github.com/dunglas/mercure) (automatically installed, AGPL licensed) | nomercure               |\n\n## Compile the Go App\n\nYou can now build the final binary.\n\n### Using xcaddy\n\nThe recommended way is to use [xcaddy](https://github.com/caddyserver/xcaddy) to compile FrankenPHP.\n`xcaddy` also allows to easily add [custom Caddy modules](https://caddyserver.com/docs/modules/) and FrankenPHP extensions:\n\n```console\nCGO_ENABLED=1 \\\nXCADDY_GO_BUILD_FLAGS=\"-ldflags='-w -s' -tags=nobadger,nomysql,nopgx\" \\\nCGO_CFLAGS=$(php-config --includes) \\\nCGO_LDFLAGS=\"$(php-config --ldflags) $(php-config --libs)\" \\\nxcaddy build \\\n    --output frankenphp \\\n    --with github.com/dunglas/frankenphp/caddy \\\n    --with github.com/dunglas/mercure/caddy \\\n    --with github.com/dunglas/vulcain/caddy \\\n    --with github.com/dunglas/caddy-cbrotli\n    # Add extra Caddy modules and FrankenPHP extensions here\n    # optionally, if you would like to compile from your frankenphp sources:\n    # --with github.com/dunglas/frankenphp=$(pwd) \\\n    # --with github.com/dunglas/frankenphp/caddy=$(pwd)/caddy\n\n```\n\n> [!TIP]\n>\n> If you're using musl libc (the default on Alpine Linux) and Symfony,\n> you may need to increase the default stack size.\n> Otherwise, you may get errors like `PHP Fatal error: Maximum call stack size of 83360 bytes reached during compilation. Try splitting expression`\n>\n> To do so, change the `XCADDY_GO_BUILD_FLAGS` environment variable to something like\n> `XCADDY_GO_BUILD_FLAGS=$'-ldflags \"-w -s -extldflags \\'-Wl,-z,stack-size=0x80000\\'\"'`\n> (change the stack size value according to your app needs).\n\n### Without xcaddy\n\nAlternatively, it's possible to compile FrankenPHP without `xcaddy` by using the `go` command directly:\n\n```console\ncurl -L https://github.com/php/frankenphp/archive/refs/heads/main.tar.gz | tar xz\ncd frankenphp-main/caddy/frankenphp\nCGO_CFLAGS=$(php-config --includes) CGO_LDFLAGS=\"$(php-config --ldflags) $(php-config --libs)\" go build -tags=nobadger,nomysql,nopgx\n```\n"
  },
  {
    "path": "docs/config.md",
    "content": "# Configuration\n\nFrankenPHP, Caddy as well as the [Mercure](mercure.md) and [Vulcain](https://vulcain.rocks) modules can be configured using [the formats supported by Caddy](https://caddyserver.com/docs/getting-started#your-first-config).\n\nThe most common format is the `Caddyfile`, which is a simple, human-readable text format.\nBy default, FrankenPHP will look for a `Caddyfile` in the current directory.\nYou can specify a custom path with the `-c` or `--config` option.\n\nA minimal `Caddyfile` to serve a PHP application is shown below:\n\n```caddyfile\n# The hostname to respond to\nlocalhost\n\n# Optionally, the directory to serve files from, otherwise defaults to the current directory\n#root public/\nphp_server\n```\n\nA more advanced `Caddyfile` enabling more features and providing convenient environment variables is provided [in the FrankenPHP repository](https://github.com/php/frankenphp/blob/main/caddy/frankenphp/Caddyfile),\nand with Docker images.\n\nPHP itself can be configured [using a `php.ini` file](https://www.php.net/manual/en/configuration.file.php).\n\nDepending on your installation method, FrankenPHP and the PHP interpreter will look for configuration files in locations described below.\n\n## Docker\n\nFrankenPHP:\n\n- `/etc/frankenphp/Caddyfile`: the main configuration file\n- `/etc/frankenphp/Caddyfile.d/*.caddyfile`: additional configuration files that are loaded automatically\n\nPHP:\n\n- `php.ini`: `/usr/local/etc/php/php.ini` (no `php.ini` is provided by default)\n- additional configuration files: `/usr/local/etc/php/conf.d/*.ini`\n- PHP extensions: `/usr/local/lib/php/extensions/no-debug-zts-<YYYYMMDD>/`\n- You should copy an official template provided by the PHP project:\n\n```dockerfile\nFROM dunglas/frankenphp\n\n# Production:\nRUN cp $PHP_INI_DIR/php.ini-production $PHP_INI_DIR/php.ini\n\n# Or development:\nRUN cp $PHP_INI_DIR/php.ini-development $PHP_INI_DIR/php.ini\n```\n\n## RPM and Debian packages\n\nFrankenPHP:\n\n- `/etc/frankenphp/Caddyfile`: the main configuration file\n- `/etc/frankenphp/Caddyfile.d/*.caddyfile`: additional configuration files that are loaded automatically\n\nPHP:\n\n- `php.ini`: `/etc/php-zts/php.ini` (a `php.ini` file with production presets is provided by default)\n- additional configuration files: `/etc/php-zts/conf.d/*.ini`\n\n## Static binary\n\nFrankenPHP:\n\n- In the current working directory: `Caddyfile`\n\nPHP:\n\n- `php.ini`: The directory in which `frankenphp run` or `frankenphp php-server` is executed, then `/etc/frankenphp/php.ini`\n- additional configuration files: `/etc/frankenphp/php.d/*.ini`\n- PHP extensions: cannot be loaded, bundle them in the binary itself\n- copy one of `php.ini-production` or `php.ini-development` provided [in the PHP sources](https://github.com/php/php-src/).\n\n## Caddyfile Config\n\nThe `php_server` or the `php` [HTTP directives](https://caddyserver.com/docs/caddyfile/concepts#directives) may be used within the site blocks to serve your PHP app.\n\nMinimal example:\n\n```caddyfile\nlocalhost {\n\t# Enable compression (optional)\n\tencode zstd br gzip\n\t# Execute PHP files in the current directory and serve assets\n\tphp_server\n}\n```\n\nYou can also explicitly configure FrankenPHP using the [global option](https://caddyserver.com/docs/caddyfile/concepts#global-options) `frankenphp`:\n\n```caddyfile\n{\n\tfrankenphp {\n\t\tnum_threads <num_threads> # Sets the number of PHP threads to start. Default: 2x the number of available CPUs.\n\t\tmax_threads <num_threads> # Limits the number of additional PHP threads that can be started at runtime. Default: num_threads. Can be set to 'auto'.\n\t\tmax_wait_time <duration> # Sets the maximum time a request may wait for a free PHP thread before timing out. Default: disabled.\n\t\tmax_idle_time <duration> # Sets the maximum time an autoscaled thread may be idle before being deactivated. Default: 5s.\n\t\tphp_ini <key> <value> # Set a php.ini directive. Can be used several times to set multiple directives.\n\t\tworker {\n\t\t\tfile <path> # Sets the path to the worker script.\n\t\t\tnum <num> # Sets the number of PHP threads to start, defaults to 2x the number of available CPUs.\n\t\t\tenv <key> <value> # Sets an extra environment variable to the given value. Can be specified more than once for multiple environment variables.\n\t\t\twatch <path> # Sets the path to watch for file changes. Can be specified more than once for multiple paths.\n\t\t\tname <name> # Sets the name of the worker, used in logs and metrics. Default: absolute path of worker file\n\t\t\tmax_consecutive_failures <num> # Sets the maximum number of consecutive failures before the worker is considered unhealthy, -1 means the worker will always restart. Default: 6.\n\t\t}\n\t}\n}\n\n# ...\n```\n\nAlternatively, you may use the one-line short form of the `worker` option:\n\n```caddyfile\n{\n\tfrankenphp {\n\t\tworker <file> <num>\n\t}\n}\n\n# ...\n```\n\nYou can also define multiple workers if you serve multiple apps on the same server:\n\n```caddyfile\napp.example.com {\n    root /path/to/app/public\n\tphp_server {\n\t\troot /path/to/app/public # allows for better caching\n\t\tworker index.php <num>\n\t}\n}\n\nother.example.com {\n    root /path/to/other/public\n\tphp_server {\n\t\troot /path/to/other/public\n\t\tworker index.php <num>\n\t}\n}\n\n# ...\n```\n\nUsing the `php_server` directive is generally what you need,\nbut if you need full control, you can use the lower-level `php` directive.\nThe `php` directive passes all input to PHP, instead of first checking whether\nit's a PHP file or not. Read more about it in the [performance page](performance.md#try_files).\n\nUsing the `php_server` directive is equivalent to this configuration:\n\n```caddyfile\nroute {\n\t# Add trailing slash for directory requests\n\t@canonicalPath {\n\t\tfile {path}/index.php\n\t\tnot path */\n\t}\n\tredir @canonicalPath {path}/ 308\n\t# If the requested file does not exist, try index files\n\t@indexFiles file {\n\t\ttry_files {path} {path}/index.php index.php\n\t\tsplit_path .php\n\t}\n\trewrite @indexFiles {http.matchers.file.relative}\n\t# FrankenPHP!\n\t@phpFiles path *.php\n\tphp @phpFiles\n\tfile_server\n}\n```\n\nThe `php_server` and the `php` directives have the following options:\n\n```caddyfile\nphp_server [<matcher>] {\n\troot <directory> # Sets the root folder to the site. Default: `root` directive.\n\tsplit_path <delim...> # Sets the substrings for splitting the URI into two parts. The first matching substring will be used to split the \"path info\" from the path. The first piece is suffixed with the matching substring and will be assumed as the actual resource (CGI script) name. The second piece will be set to PATH_INFO for the script to use. Default: `.php`\n\tresolve_root_symlink false # Disables resolving the `root` directory to its actual value by evaluating a symbolic link, if one exists (enabled by default).\n\tenv <key> <value> # Sets an extra environment variable to the given value. Can be specified more than once for multiple environment variables.\n\tfile_server off # Disables the built-in file_server directive.\n\tworker { # Creates a worker specific to this server. Can be specified more than once for multiple workers.\n\t\tfile <path> # Sets the path to the worker script, can be relative to the php_server root\n\t\tnum <num> # Sets the number of PHP threads to start, defaults to 2x the number of available\n\t\tname <name> # Sets the name for the worker, used in logs and metrics. Default: absolute path of worker file. Always starts with m# when defined in a php_server block.\n\t\twatch <path> # Sets the path to watch for file changes. Can be specified more than once for multiple paths.\n\t\tenv <key> <value> # Sets an extra environment variable to the given value. Can be specified more than once for multiple environment variables. Environment variables for this worker are also inherited from the php_server parent, but can be overwritten here.\n\t\tmatch <path> # match the worker to a path pattern. Overrides try_files and can only be used in the php_server directive.\n\t}\n\tworker <other_file> <num> # Can also use the short form like in the global frankenphp block.\n}\n```\n\n### Watching for File Changes\n\nSince workers only boot your application once and keep it in memory, any changes\nto your PHP files will not be reflected immediately.\n\nWorkers can instead be restarted on file changes via the `watch` directive.\nThis is useful for development environments.\n\n```caddyfile\n{\n\tfrankenphp {\n\t\tworker {\n\t\t\tfile  /path/to/app/public/worker.php\n\t\t\twatch\n\t\t}\n\t}\n}\n```\n\nThis feature is often used in combination with [hot reload](hot-reload.md).\n\nIf the `watch` directory is not specified, it will fall back to `./**/*.{env,php,twig,yaml,yml}`,\nwhich watches all `.env`, `.php`, `.twig`, `.yaml` and `.yml` files in the directory and subdirectories\nwhere the FrankenPHP process was started. You can instead also specify one or more directories via a\n[shell filename pattern](https://pkg.go.dev/path/filepath#Match):\n\n```caddyfile\n{\n\tfrankenphp {\n\t\tworker {\n\t\t\tfile  /path/to/app/public/worker.php\n\t\t\twatch /path/to/app # watches all files in all subdirectories of /path/to/app\n\t\t\twatch /path/to/app/*.php # watches files ending in .php in /path/to/app\n\t\t\twatch /path/to/app/**/*.php # watches PHP files in /path/to/app and subdirectories\n\t\t\twatch /path/to/app/**/*.{php,twig} # watches PHP and Twig files in /path/to/app and subdirectories\n\t\t}\n\t}\n}\n```\n\n- The `**` pattern signifies recursive watching\n- Directories can also be relative (to where the FrankenPHP process is started from)\n- If you have multiple workers defined, all of them will be restarted when a file changes\n- Be wary about watching files that are created at runtime (like logs) since they might cause unwanted worker restarts.\n\nThe file watcher is based on [e-dant/watcher](https://github.com/e-dant/watcher).\n\n## Matching the Worker To a Path\n\nIn traditional PHP applications, scripts are always placed in the public directory.\nThis is also true for worker scripts, which are treated like any other PHP script.\nIf you want to instead put the worker script outside the public directory, you can do so via the `match` directive.\n\nThe `match` directive is an optimized alternative to `try_files` only available inside `php_server` and `php`.\nThe following example will always serve a file in the public directory if present\nand otherwise forward the request to the worker matching the path pattern.\n\n```caddyfile\n{\n\tfrankenphp {\n\t\tphp_server {\n\t\t\tworker {\n\t\t\t\tfile /path/to/worker.php # file can be outside of public path\n\t\t\t\tmatch /api/* # all requests starting with /api/ will be handled by this worker\n\t\t\t}\n\t\t}\n\t}\n}\n```\n\n## Environment Variables\n\nThe following environment variables can be used to inject Caddy directives in the `Caddyfile` without modifying it:\n\n- `SERVER_NAME`: change [the addresses on which to listen](https://caddyserver.com/docs/caddyfile/concepts#addresses), the provided hostnames will also be used for the generated TLS certificate\n- `SERVER_ROOT`: change the root directory of the site, defaults to `public/`\n- `CADDY_GLOBAL_OPTIONS`: inject [global options](https://caddyserver.com/docs/caddyfile/options)\n- `FRANKENPHP_CONFIG`: inject config under the `frankenphp` directive\n\nAs for FPM and CLI SAPIs, environment variables are exposed by default in the `$_SERVER` superglobal.\n\nThe `S` value of [the `variables_order` PHP directive](https://www.php.net/manual/en/ini.core.php#ini.variables-order) is always equivalent to `ES` regardless of the placement of `E` elsewhere in this directive.\n\n## PHP config\n\nTo load [additional PHP configuration files](https://www.php.net/manual/en/configuration.file.php#configuration.file.scan),\nthe `PHP_INI_SCAN_DIR` environment variable can be used.\nWhen set, PHP will load all the file with the `.ini` extension present in the given directories.\n\nYou can also change the PHP configuration using the `php_ini` directive in the `Caddyfile`:\n\n```caddyfile\n{\n    frankenphp {\n        php_ini memory_limit 256M\n\n        # or\n\n        php_ini {\n            memory_limit 256M\n            max_execution_time 15\n        }\n    }\n}\n```\n\n### Disabling HTTPS\n\nBy default, FrankenPHP will automatically enable HTTPS using for all the hostnames, including `localhost`.\nIf you want to disable HTTPS (for example in a development environment), you can set the `SERVER_NAME` environment variable to `http://` or `:80`:\n\nAlternatively, you can use all other methods described in the [Caddy documentation](https://caddyserver.com/docs/automatic-https#activation).\n\nIf you want to use HTTPS with the `127.0.0.1` IP address instead of the `localhost` hostname, please read the [known issues](known-issues.md#using-https127001-with-docker) section.\n\n### Full Duplex (HTTP/1)\n\nWhen using HTTP/1.x, it may be desirable to enable full-duplex mode to allow writing a response before the entire body\nhas been read. (for example: [Mercure](mercure.md), WebSocket, Server-Sent Events, etc.)\n\nThis is an opt-in configuration that needs to be added to the global options in the `Caddyfile`:\n\n```caddyfile\n{\n  servers {\n    enable_full_duplex\n  }\n}\n```\n\n> [!CAUTION]\n>\n> Enabling this option may cause old HTTP/1.x clients that don't support full-duplex to deadlock.\n> This can also be configured using the `CADDY_GLOBAL_OPTIONS` environment config:\n\n```sh\nCADDY_GLOBAL_OPTIONS=\"servers {\n  enable_full_duplex\n}\"\n```\n\nYou can find more information about this setting in the [Caddy documentation](https://caddyserver.com/docs/caddyfile/options#enable-full-duplex).\n\n## Enable the Debug Mode\n\nWhen using the Docker image, set the `CADDY_GLOBAL_OPTIONS` environment variable to `debug` to enable the debug mode:\n\n```console\ndocker run -v $PWD:/app/public \\\n    -e CADDY_GLOBAL_OPTIONS=debug \\\n    -p 80:80 -p 443:443 -p 443:443/udp \\\n    dunglas/frankenphp\n```\n\n## Shell Completion\n\nFrankenPHP provides built-in shell completion support for Bash, Zsh, Fish, and PowerShell. This enables autocompletion for all commands (including custom commands like `php-server`, `php-cli`, and `extension-init`) and their flags.\n\n### Bash\n\nTo load completions in your current shell session:\n\n```console\nsource <(frankenphp completion bash)\n```\n\nTo load completions for every new session, run:\n\n**Linux:**\n\n```console\nfrankenphp completion bash > /usr/share/bash-completion/completions/frankenphp\n```\n\n**macOS:**\n\n```console\nfrankenphp completion bash > $(brew --prefix)/share/bash-completion/completions/frankenphp\n```\n\n### Zsh\n\nIf shell completion is not already enabled in your environment, you will need to enable it. You can execute the following once:\n\n```console\necho \"autoload -U compinit; compinit\" >> ~/.zshrc\n```\n\nTo load completions for each session, execute once:\n\n```console\nfrankenphp completion zsh > \"${fpath[1]}/_frankenphp\"\n```\n\nYou will need to start a new shell for this setup to take effect.\n\n### Fish\n\nTo load completions in your current shell session:\n\n```console\nfrankenphp completion fish | source\n```\n\nTo load completions for every new session, execute once:\n\n```console\nfrankenphp completion fish > ~/.config/fish/completions/frankenphp.fish\n```\n\n### PowerShell\n\nTo load completions in your current shell session:\n\n```powershell\nfrankenphp completion powershell | Out-String | Invoke-Expression\n```\n\nTo load completions for every new session, execute once:\n\n```powershell\nfrankenphp completion powershell | Out-File -FilePath (Join-Path (Split-Path $PROFILE) \"frankenphp.ps1\")\nAdd-Content -Path $PROFILE -Value '. (Join-Path (Split-Path $PROFILE) \"frankenphp.ps1\")'\n```\n\nYou will need to start a new shell for this setup to take effect.\n\nYou will need to start a new shell for this setup to take effect.\n"
  },
  {
    "path": "docs/docker.md",
    "content": "# Building Custom Docker Image\n\n[FrankenPHP Docker images](https://hub.docker.com/r/dunglas/frankenphp) are based on [official PHP images](https://hub.docker.com/_/php/).\nDebian and Alpine Linux variants are provided for popular architectures.\nDebian variants are recommended.\n\nVariants for PHP 8.2, 8.3, 8.4 and 8.5 are provided.\n\nThe tags follow this pattern: `dunglas/frankenphp:<frankenphp-version>-php<php-version>-<os>`\n\n- `<frankenphp-version>` and `<php-version>` are version numbers of FrankenPHP and PHP respectively, ranging from major (e.g. `1`), minor (e.g. `1.2`) to patch versions (e.g. `1.2.3`).\n- `<os>` is either `trixie` (for Debian Trixie), `bookworm` (for Debian Bookworm), or `alpine` (for the latest stable version of Alpine).\n\n[Browse tags](https://hub.docker.com/r/dunglas/frankenphp/tags).\n\n## How to Use The Images\n\nCreate a `Dockerfile` in your project:\n\n```dockerfile\nFROM dunglas/frankenphp\n\nCOPY . /app/public\n```\n\nThen, run these commands to build and run the Docker image:\n\n```console\ndocker build -t my-php-app .\ndocker run -it --rm --name my-running-app my-php-app\n```\n\n## How to Tweak the Configuration\n\nFor convenience, [a default `Caddyfile`](https://github.com/php/frankenphp/blob/main/caddy/frankenphp/Caddyfile) containing\nuseful environment variables is provided in the image.\n\n## How to Install More PHP Extensions\n\nThe [`docker-php-extension-installer`](https://github.com/mlocati/docker-php-extension-installer) script is provided in the base image.\nAdding additional PHP extensions is straightforward:\n\n```dockerfile\nFROM dunglas/frankenphp\n\n# add additional extensions here:\nRUN install-php-extensions \\\n\tpdo_mysql \\\n\tgd \\\n\tintl \\\n\tzip \\\n\topcache\n```\n\n## How to Install More Caddy Modules\n\nFrankenPHP is built on top of Caddy, and all [Caddy modules](https://caddyserver.com/docs/modules/) can be used with FrankenPHP.\n\nThe easiest way to install custom Caddy modules is to use [xcaddy](https://github.com/caddyserver/xcaddy):\n\n```dockerfile\nFROM dunglas/frankenphp:builder AS builder\n\n# Copy xcaddy in the builder image\nCOPY --from=caddy:builder /usr/bin/xcaddy /usr/bin/xcaddy\n\n# CGO must be enabled to build FrankenPHP\nRUN CGO_ENABLED=1 \\\n    XCADDY_SETCAP=1 \\\n    XCADDY_GO_BUILD_FLAGS=\"-ldflags='-w -s' -tags=nobadger,nomysql,nopgx\" \\\n    CGO_CFLAGS=$(php-config --includes) \\\n    CGO_LDFLAGS=\"$(php-config --ldflags) $(php-config --libs)\" \\\n    xcaddy build \\\n        --output /usr/local/bin/frankenphp \\\n        --with github.com/dunglas/frankenphp=./ \\\n        --with github.com/dunglas/frankenphp/caddy=./caddy/ \\\n        --with github.com/dunglas/caddy-cbrotli \\\n        # Mercure and Vulcain are included in the official build, but feel free to remove them\n        --with github.com/dunglas/mercure/caddy \\\n        --with github.com/dunglas/vulcain/caddy\n        # Add extra Caddy modules here\n\nFROM dunglas/frankenphp AS runner\n\n# Replace the official binary by the one contained your custom modules\nCOPY --from=builder /usr/local/bin/frankenphp /usr/local/bin/frankenphp\n```\n\nThe `builder` image provided by FrankenPHP contains a compiled version of `libphp`.\n[Builders images](https://hub.docker.com/r/dunglas/frankenphp/tags?name=builder) are provided for all versions of FrankenPHP and PHP, both for Debian and Alpine.\n\n> [!TIP]\n>\n> If you're using Alpine Linux and Symfony,\n> you may need to [increase the default stack size](compile.md#using-xcaddy).\n\n## Enabling the Worker Mode by Default\n\nSet the `FRANKENPHP_CONFIG` environment variable to start FrankenPHP with a worker script:\n\n```dockerfile\nFROM dunglas/frankenphp\n\n# ...\n\nENV FRANKENPHP_CONFIG=\"worker ./public/index.php\"\n```\n\n## Using a Volume in Development\n\nTo develop easily with FrankenPHP, mount the directory from your host containing the source code of the app as a volume in the Docker container:\n\n```console\ndocker run -v $PWD:/app/public -p 80:80 -p 443:443 -p 443:443/udp --tty my-php-app\n```\n\n> [!TIP]\n>\n> The `--tty` option allows to have nice human-readable logs instead of JSON logs.\n\nWith Docker Compose:\n\n```yaml\n# compose.yaml\n\nservices:\n  php:\n    image: dunglas/frankenphp\n    # uncomment the following line if you want to use a custom Dockerfile\n    #build: .\n    # uncomment the following line if you want to run this in a production environment\n    # restart: always\n    ports:\n      - \"80:80\" # HTTP\n      - \"443:443\" # HTTPS\n      - \"443:443/udp\" # HTTP/3\n    volumes:\n      - ./:/app/public\n      - caddy_data:/data\n      - caddy_config:/config\n    # comment the following line in production, it allows to have nice human-readable logs in dev\n    tty: true\n\n# Volumes needed for Caddy certificates and configuration\nvolumes:\n  caddy_data:\n  caddy_config:\n```\n\n## Running as a Non-Root User\n\nFrankenPHP can run as non-root user in Docker.\n\nHere is a sample `Dockerfile` doing this:\n\n```dockerfile\nFROM dunglas/frankenphp\n\nARG USER=appuser\n\nRUN \\\n\t# Use \"adduser -D ${USER}\" for alpine based distros\n\tuseradd ${USER}; \\\n\t# Add additional capability to bind to port 80 and 443\n\tsetcap CAP_NET_BIND_SERVICE=+eip /usr/local/bin/frankenphp; \\\n\t# Give write access to /config/caddy and /data/caddy\n\tchown -R ${USER}:${USER} /config/caddy /data/caddy\n\nUSER ${USER}\n```\n\n### Running With No Capabilities\n\nEven when running rootless, FrankenPHP needs the `CAP_NET_BIND_SERVICE` capability to bind the\nweb server on privileged ports (80 and 443).\n\nIf you expose FrankenPHP on a non-privileged port (1024 and above), it's possible to run\nthe webserver as a non-root user, and without the need for any capability:\n\n```dockerfile\nFROM dunglas/frankenphp\n\nARG USER=appuser\n\nRUN \\\n\t# Use \"adduser -D ${USER}\" for alpine based distros\n\tuseradd ${USER}; \\\n\t# Remove default capability\n\tsetcap -r /usr/local/bin/frankenphp; \\\n\t# Give write access to /config/caddy and /data/caddy\n\tchown -R ${USER}:${USER} /config/caddy /data/caddy\n\nUSER ${USER}\n```\n\nNext, set the `SERVER_NAME` environment variable to use an unprivileged port.\nExample: `:8000`\n\n## Updates\n\nThe Docker images are built:\n\n- when a new release is tagged\n- daily at 4 am UTC, if new versions of the official PHP images are available\n\n## Hardening Images\n\nTo further reduce the attack surface and size of your FrankenPHP Docker images, it's also possible to build them on top of a\n[Google distroless](https://github.com/GoogleContainerTools/distroless) or\n[Docker hardened](https://www.docker.com/products/hardened-images) image.\n\n> [!WARNING]\n> These minimal base images do not include a shell or package manager, which makes debugging more difficult.\n> They are therefore recommended only for production if security is a high priority.\n\nWhen adding additional PHP extensions, you will need an intermediate build stage:\n\n```dockerfile\nFROM dunglas/frankenphp AS builder\n\n# Add additional PHP extensions here\nRUN install-php-extensions pdo_mysql pdo_pgsql #...\n\n# Copy shared libs of frankenphp and all installed extensions to temporary location\n# You can also do this step manually by analyzing ldd output of frankenphp binary and each extension .so file\nRUN apt-get update && apt-get install -y libtree && \\\n    EXT_DIR=\"$(php -r 'echo ini_get(\"extension_dir\");')\" && \\\n    FRANKENPHP_BIN=\"$(which frankenphp)\"; \\\n    LIBS_TMP_DIR=\"/tmp/libs\"; \\\n    mkdir -p \"$LIBS_TMP_DIR\"; \\\n    for target in \"$FRANKENPHP_BIN\" $(find \"$EXT_DIR\" -maxdepth 2 -type f -name \"*.so\"); do \\\n        libtree -pv \"$target\" | sed 's/.*── \\(.*\\) \\[.*/\\1/' | grep -v \"^$target\" | while IFS= read -r lib; do \\\n            [ -z \"$lib\" ] && continue; \\\n            base=$(basename \"$lib\"); \\\n            destfile=\"$LIBS_TMP_DIR/$base\"; \\\n            if [ ! -f \"$destfile\" ]; then \\\n                cp \"$lib\" \"$destfile\"; \\\n            fi; \\\n        done; \\\n    done\n\n\n# Distroless debian base image, make sure this is the same debian version as the base image\nFROM gcr.io/distroless/base-debian13\n# Docker hardened image alternative\n# FROM dhi.io/debian:13\n\n# Location of your app and Caddyfile to be copied into the container\nARG PATH_TO_APP=\".\"\nARG PATH_TO_CADDYFILE=\"./Caddyfile\"\n\n# Copy your app into /app\n# For further hardening make sure only writable paths are owned by the nonroot user\nCOPY --chown=nonroot:nonroot \"$PATH_TO_APP\" /app\nCOPY \"$PATH_TO_CADDYFILE\" /etc/caddy/Caddyfile\n\n# Copy frankenphp and necessary libs\nCOPY --from=builder /usr/local/bin/frankenphp /usr/local/bin/frankenphp\nCOPY --from=builder /usr/local/lib/php/extensions /usr/local/lib/php/extensions\nCOPY --from=builder /tmp/libs /usr/lib\n\n# Copy php.ini configuration files\nCOPY --from=builder /usr/local/etc/php/conf.d /usr/local/etc/php/conf.d\nCOPY --from=builder /usr/local/etc/php/php.ini-production /usr/local/etc/php/php.ini\n\n# Caddy data dirs — must be writable for nonroot, even on a read-only root filesystem\nENV XDG_CONFIG_HOME=/config \\\n    XDG_DATA_HOME=/data\nCOPY --from=builder --chown=nonroot:nonroot /data/caddy /data/caddy\nCOPY --from=builder --chown=nonroot:nonroot /config/caddy /config/caddy\n\nUSER nonroot\n\nWORKDIR /app\n\n# entrypoint to run frankenphp with the provided Caddyfile\nENTRYPOINT [\"/usr/local/bin/frankenphp\", \"run\", \"-c\", \"/etc/caddy/Caddyfile\"]\n```\n\n## Development Versions\n\nDevelopment versions are available in the [`dunglas/frankenphp-dev`](https://hub.docker.com/repository/docker/dunglas/frankenphp-dev) Docker repository.\nA new build is triggered every time a commit is pushed to the main branch of the GitHub repository.\n\nThe `latest*` tags point to the head of the `main` branch.\nTags of the form `sha-<git-commit-hash>` are also available.\n"
  },
  {
    "path": "docs/early-hints.md",
    "content": "# Early Hints\n\nFrankenPHP natively supports the [103 Early Hints status code](https://developer.chrome.com/blog/early-hints/).\nUsing Early Hints can improve the load time of your web pages by 30%.\n\n```php\n<?php\n\nheader('Link: </style.css>; rel=preload; as=style');\nheaders_send(103);\n\n// your slow algorithms and SQL queries 🤪\n\necho <<<'HTML'\n<!DOCTYPE html>\n<title>Hello FrankenPHP</title>\n<link rel=\"stylesheet\" href=\"style.css\">\nHTML;\n```\n\nEarly Hints are supported both by the normal and the [worker](worker.md) modes.\n"
  },
  {
    "path": "docs/embed.md",
    "content": "# PHP Apps As Standalone Binaries\n\nFrankenPHP has the ability to embed the source code and assets of PHP applications in a static, self-contained binary.\n\nThanks to this feature, PHP applications can be distributed as standalone binaries that include the application itself, the PHP interpreter, and Caddy, a production-level web server.\n\nLearn more about this feature [in the presentation made by Kévin at SymfonyCon 2023](https://dunglas.dev/2023/12/php-and-symfony-apps-as-standalone-binaries/).\n\nFor embedding Laravel applications, [read this specific documentation entry](laravel.md#laravel-apps-as-standalone-binaries).\n\n## Preparing Your App\n\nBefore creating the self-contained binary be sure that your app is ready for embedding.\n\nFor instance, you likely want to:\n\n- Install the production dependencies of the app\n- Dump the autoloader\n- Enable the production mode of your application (if any)\n- Strip unneeded files such as `.git` or tests to reduce the size of your final binary\n\nFor instance, for a Symfony app, you can use the following commands:\n\n```console\n# Export the project to get rid of .git/, etc\nmkdir $TMPDIR/my-prepared-app\ngit archive HEAD | tar -x -C $TMPDIR/my-prepared-app\ncd $TMPDIR/my-prepared-app\n\n# Set proper environment variables\necho APP_ENV=prod > .env.local\necho APP_DEBUG=0 >> .env.local\n\n# Remove the tests and other unneeded files to save space\n# Alternatively, add these files with the export-ignore attribute in your .gitattributes file\nrm -Rf tests/\n\n# Install the dependencies\ncomposer install --ignore-platform-reqs --no-dev -a\n\n# Optimize .env\ncomposer dump-env prod\n```\n\n### Customizing the Configuration\n\nTo customize [the configuration](config.md), you can put a `Caddyfile` as well as a `php.ini` file\nin the main directory of the app to be embedded (`$TMPDIR/my-prepared-app` in the previous example).\n\n## Creating a Linux Binary\n\nThe easiest way to create a Linux binary is to use the Docker-based builder we provide.\n\n1. Create a file named `static-build.Dockerfile` in the repository of your app:\n\n   ```dockerfile\n   FROM --platform=linux/amd64 dunglas/frankenphp:static-builder-gnu\n   # If you intend to run the binary on musl-libc systems, use static-builder-musl instead\n\n   # Copy your app\n   WORKDIR /go/src/app/dist/app\n   COPY . .\n\n   # Build the static binary\n   WORKDIR /go/src/app/\n   RUN EMBED=dist/app/ ./build-static.sh\n   ```\n\n   > [!CAUTION]\n   >\n   > Some `.dockerignore` files (e.g. default [Symfony Docker `.dockerignore`](https://github.com/dunglas/symfony-docker/blob/main/.dockerignore))\n   > will ignore the `vendor/` directory and `.env` files. Be sure to adjust or remove the `.dockerignore` file before the build.\n\n2. Build:\n\n   ```console\n   docker build -t static-app -f static-build.Dockerfile .\n   ```\n\n3. Extract the binary:\n\n   ```console\n   docker cp $(docker create --name static-app-tmp static-app):/go/src/app/dist/frankenphp-linux-x86_64 my-app ; docker rm static-app-tmp\n   ```\n\nThe resulting binary is the file named `my-app` in the current directory.\n\n## Creating a Binary for Other OSes\n\nIf you don't want to use Docker, or want to build a macOS binary, use the shell script we provide:\n\n```console\ngit clone https://github.com/php/frankenphp\ncd frankenphp\nEMBED=/path/to/your/app ./build-static.sh\n```\n\nThe resulting binary is the file named `frankenphp-<os>-<arch>` in the `dist/` directory.\n\n## Using The Binary\n\nThis is it! The `my-app` file (or `dist/frankenphp-<os>-<arch>` on other OSes) contains your self-contained app!\n\nTo start the web app run:\n\n```console\n./my-app php-server\n```\n\nIf your app contains a [worker script](worker.md), start the worker with something like:\n\n```console\n./my-app php-server --worker public/index.php\n```\n\nTo enable HTTPS (a Let's Encrypt certificate is automatically created), HTTP/2, and HTTP/3, specify the domain name to use:\n\n```console\n./my-app php-server --domain localhost\n```\n\nYou can also run the PHP CLI scripts embedded in your binary:\n\n```console\n./my-app php-cli bin/console\n```\n\n## PHP Extensions\n\nBy default, the script will build extensions required by the `composer.json` file of your project, if any.\nIf the `composer.json` file doesn't exist, the default extensions are built, as documented in [the static builds entry](static.md).\n\nTo customize the extensions, use the `PHP_EXTENSIONS` environment variable.\n\n## Customizing The Build\n\n[Read the static build documentation](static.md) to see how to customize the binary (extensions, PHP version...).\n\n## Distributing The Binary\n\nOn Linux, the created binary is compressed using [UPX](https://upx.github.io).\n\nOn Mac, to reduce the size of the file before sending it, you can compress it.\nWe recommend `xz`.\n"
  },
  {
    "path": "docs/es/CONTRIBUTING.md",
    "content": "# Contribuir\n\n## Compilar PHP\n\n### Con Docker (Linux)\n\nConstruya la imagen Docker de desarrollo:\n\n```console\ndocker build -t frankenphp-dev -f dev.Dockerfile .\ndocker run --cap-add=SYS_PTRACE --security-opt seccomp=unconfined -p 8080:8080 -p 443:443 -p 443:443/udp -v $PWD:/go/src/app -it frankenphp-dev\n```\n\nLa imagen contiene las herramientas de desarrollo habituales (Go, GDB, Valgrind, Neovim...) y utiliza las siguientes ubicaciones de configuración de PHP:\n\n- php.ini: `/etc/frankenphp/php.ini` Se proporciona un archivo php.ini con ajustes preestablecidos de desarrollo por defecto.\n- archivos de configuración adicionales: `/etc/frankenphp/php.d/*.ini`\n- extensiones php: `/usr/lib/frankenphp/modules/`\n\nSi su versión de Docker es inferior a 23.0, la construcción fallará debido a un [problema de patrón](https://github.com/moby/moby/pull/42676) en `.dockerignore`. Agregue los directorios a `.dockerignore`:\n\n```patch\n !testdata/*.php\n !testdata/*.txt\n+!caddy\n+!internal\n```\n\n### Sin Docker (Linux y macOS)\n\n[Siga las instrucciones para compilar desde las fuentes](compile.md) y pase la bandera de configuración `--debug`.\n\n## Ejecutar la suite de pruebas\n\n```console\ngo test -tags watcher -race -v ./...\n```\n\n## Módulo Caddy\n\nConstruir Caddy con el módulo FrankenPHP:\n\n```console\ncd caddy/frankenphp/\ngo build -tags watcher,brotli,nobadger,nomysql,nopgx\ncd ../../\n```\n\nEjecutar Caddy con el módulo FrankenPHP:\n\n```console\ncd testdata/\n../caddy/frankenphp/frankenphp run\n```\n\nEl servidor está configurado para escuchar en la dirección `127.0.0.1:80`:\n\n> [!NOTE]\n>\n> Si está usando Docker, deberá enlazar el puerto 80 del contenedor o ejecutar desde dentro del contenedor.\n\n```console\ncurl -vk http://127.0.0.1/phpinfo.php\n```\n\n## Servidor de prueba mínimo\n\nConstruir el servidor de prueba mínimo:\n\n```console\ncd internal/testserver/\ngo build\ncd ../../\n```\n\nIniciar el servidor de prueba:\n\n```console\ncd testdata/\n../internal/testserver/testserver\n```\n\nEl servidor está configurado para escuchar en la dirección `127.0.0.1:8080`:\n\n```console\ncurl -v http://127.0.0.1:8080/phpinfo.php\n```\n\n## Construir localmente las imágenes Docker\n\nMostrar el plan de compilación:\n\n```console\ndocker buildx bake -f docker-bake.hcl --print\n```\n\nConstruir localmente las imágenes FrankenPHP para amd64:\n\n```console\ndocker buildx bake -f docker-bake.hcl --pull --load --set \"*.platform=linux/amd64\"\n```\n\nConstruir localmente las imágenes FrankenPHP para arm64:\n\n```console\ndocker buildx bake -f docker-bake.hcl --pull --load --set \"*.platform=linux/arm64\"\n```\n\nConstruir desde cero las imágenes FrankenPHP para arm64 y amd64 y subirlas a Docker Hub:\n\n```console\ndocker buildx bake -f docker-bake.hcl --pull --no-cache --push\n```\n\n## Depurar errores de segmentación con las compilaciones estáticas\n\n1. Descargue la versión de depuración del binario FrankenPHP desde GitHub o cree su propia compilación estática incluyendo símbolos de depuración:\n\n   ```console\n   docker buildx bake \\\n       --load \\\n       --set static-builder.args.DEBUG_SYMBOLS=1 \\\n       --set \"static-builder.platform=linux/amd64\" \\\n       static-builder\n   docker cp $(docker create --name static-builder-musl dunglas/frankenphp:static-builder-musl):/go/src/app/dist/frankenphp-linux-$(uname -m) frankenphp\n   ```\n\n2. Reemplace su versión actual de `frankenphp` por el ejecutable de depuración de FrankenPHP.\n3. Inicie FrankenPHP como de costumbre (alternativamente, puede iniciar FrankenPHP directamente con GDB: `gdb --args frankenphp run`).\n4. Adjunte el proceso con GDB:\n\n   ```console\n   gdb -p `pidof frankenphp`\n   ```\n\n5. Si es necesario, escriba `continue` en el shell de GDB.\n6. Haga que FrankenPHP falle.\n7. Escriba `bt` en el shell de GDB.\n8. Copie la salida.\n\n## Depurar errores de segmentación en GitHub Actions\n\n1. Abrir `.github/workflows/tests.yml`\n2. Activar los símbolos de depuración de la biblioteca PHP:\n\n   ```patch\n       - uses: shivammathur/setup-php@v2\n         # ...\n         env:\n           phpts: ts\n   +       debug: true\n   ```\n\n3. Activar `tmate` para conectarse al contenedor:\n\n   ```patch\n       - name: Set CGO flags\n         run: echo \"CGO_CFLAGS=$(php-config --includes)\" >> \"$GITHUB_ENV\"\n   +   - run: |\n   +       sudo apt install gdb\n   +       mkdir -p /home/runner/.config/gdb/\n   +       printf \"set auto-load safe-path /\\nhandle SIG34 nostop noprint pass\" > /home/runner/.config/gdb/gdbinit\n   +   - uses: mxschmitt/action-tmate@v3\n   ```\n\n4. Conectarse al contenedor.\n5. Abrir `frankenphp.go`.\n6. Activar `cgosymbolizer`:\n\n   ```patch\n   - //_ \"github.com/ianlancetaylor/cgosymbolizer\"\n   + _ \"github.com/ianlancetaylor/cgosymbolizer\"\n   ```\n\n7. Descargar el módulo: `go get`.\n8. Dentro del contenedor, puede usar GDB y similares:\n\n   ```console\n   go test -tags watcher -c -ldflags=-w\n   gdb --args frankenphp.test -test.run ^MyTest$\n   ```\n\n9. Cuando el error esté corregido, revierta todos los cambios.\n\n## Recursos diversos para el desarrollo\n\n- [Integración de PHP en uWSGI](https://github.com/unbit/uwsgi/blob/master/plugins/php/php_plugin.c)\n- [Integración de PHP en NGINX Unit](https://github.com/nginx/unit/blob/master/src/nxt_php_sapi.c)\n- [Integración de PHP en Go (go-php)](https://github.com/deuill/go-php)\n- [Integración de PHP en Go (GoEmPHP)](https://github.com/mikespook/goemphp)\n- [Integración de PHP en C++](https://gist.github.com/paresy/3cbd4c6a469511ac7479aa0e7c42fea7)\n- [Extending and Embedding PHP por Sara Golemon](https://books.google.fr/books?id=zMbGvK17_tYC&pg=PA254&lpg=PA254#v=onepage&q&f=false)\n- [¿Qué es TSRMLS_CC, exactamente?](http://blog.golemon.com/2006/06/what-heck-is-tsrmlscc-anyway.html)\n- [Integración de PHP en Mac](https://gist.github.com/jonnywang/61427ffc0e8dde74fff40f479d147db4)\n- [Bindings SDL](https://pkg.go.dev/github.com/veandco/go-sdl2@v0.4.21/sdl#Main)\n\n## Recursos relacionados con Docker\n\n- [Definición del archivo Bake](https://docs.docker.com/build/customize/bake/file-definition/)\n- [`docker buildx build`](https://docs.docker.com/engine/reference/commandline/buildx_build/)\n\n## Comando útil\n\n```console\napk add strace util-linux gdb\nstrace -e 'trace=!futex,epoll_ctl,epoll_pwait,tgkill,rt_sigreturn' -p 1\n```\n\n## Traducir la documentación\n\nPara traducir la documentación y el sitio a un nuevo idioma, siga estos pasos:\n\n1. Cree un nuevo directorio con el código ISO de 2 caracteres del idioma en el directorio `docs/` de este repositorio.\n2. Copie todos los archivos `.md` de la raíz del directorio `docs/` al nuevo directorio (siempre use la versión en inglés como fuente de traducción, ya que siempre está actualizada).\n3. Copie los archivos `README.md` y `CONTRIBUTING.md` del directorio raíz al nuevo directorio.\n4. Traduzca el contenido de los archivos, pero no cambie los nombres de los archivos, tampoco traduzca las cadenas que comiencen por `> [!` (es un marcado especial para GitHub).\n5. Cree una Pull Request con las traducciones.\n6. En el [repositorio del sitio](https://github.com/dunglas/frankenphp-website/tree/main), copie y traduzca los archivos de traducción en los directorios `content/`, `data/` y `i18n/`.\n7. Traduzca los valores en el archivo YAML creado.\n8. Abra una Pull Request en el repositorio del sitio.\n"
  },
  {
    "path": "docs/es/README.md",
    "content": "# FrankenPHP: el servidor de aplicaciones PHP moderno, escrito en Go\n\n<h1 align=\"center\"><a href=\"https://frankenphp.dev\"><img src=\"../../frankenphp.png\" alt=\"FrankenPHP\" width=\"600\"></a></h1>\n\nFrankenPHP es un servidor de aplicaciones moderno para PHP construido sobre el servidor web [Caddy](https://caddyserver.com/).\n\nFrankenPHP otorga superpoderes a tus aplicaciones PHP gracias a sus características de vanguardia: [_Early Hints_](early-hints.md), [modo worker](worker.md), [funcionalidades en tiempo real](mercure.md), HTTPS automático, soporte para HTTP/2 y HTTP/3...\n\nFrankenPHP funciona con cualquier aplicación PHP y hace que tus proyectos Laravel y Symfony sean más rápidos que nunca gracias a sus integraciones oficiales con el modo worker.\n\nFrankenPHP también puede usarse como una biblioteca Go autónoma que permite integrar PHP en cualquier aplicación usando `net/http`.\n\nDescubre más detalles sobre este servidor de aplicaciones en la grabación de esta conferencia dada en el Forum PHP 2022:\n\n<a href=\"https://dunglas.dev/2022/10/frankenphp-the-modern-php-app-server-written-in-go/\"><img src=\"https://dunglas.dev/wp-content/uploads/2022/10/frankenphp.png\" alt=\"Diapositivas\" width=\"600\"></a>\n\n## Para Comenzar\n\nEn Windows, usa [WSL](https://learn.microsoft.com/es-es/windows/wsl/) para ejecutar FrankenPHP.\n\n### Script de instalación\n\nPuedes copiar esta línea en tu terminal para instalar automáticamente\nuna versión adaptada a tu plataforma:\n\n```console\ncurl https://frankenphp.dev/install.sh | sh\n```\n\n### Binario autónomo\n\nProporcionamos binarios estáticos de FrankenPHP para desarrollo, para Linux y macOS,\nconteniendo [PHP 8.4](https://www.php.net/releases/8.4/es.php) y la mayoría de las extensiones PHP populares.\n\n[Descargar FrankenPHP](https://github.com/php/frankenphp/releases)\n\n**Instalación de extensiones:** Las extensiones más comunes están incluidas. No es posible instalar más.\n\n### Paquetes rpm\n\nNuestros mantenedores proponen paquetes rpm para todos los sistemas que usan `dnf`. Para instalar, ejecuta:\n\n```console\nsudo dnf install https://rpm.henderkes.com/static-php-1-0.noarch.rpm\nsudo dnf module enable php-zts:static-8.4 # 8.2-8.5 disponibles\nsudo dnf install frankenphp\n```\n\n**Instalación de extensiones:** `sudo dnf install php-zts-<extension>`\n\nPara extensiones no disponibles por defecto, usa [PIE](https://github.com/php/pie):\n\n```console\nsudo dnf install pie-zts\nsudo pie-zts install asgrim/example-pie-extension\n```\n\n### Paquetes deb\n\nNuestros mantenedores proponen paquetes deb para todos los sistemas que usan `apt`. Para instalar, ejecuta:\n\n```console\nsudo curl -fsSL https://key.henderkes.com/static-php.gpg -o /usr/share/keyrings/static-php.gpg && \\\necho \"deb [signed-by=/usr/share/keyrings/static-php.gpg] https://deb.henderkes.com/ stable main\" | sudo tee /etc/apt/sources.list.d/static-php.list && \\\nsudo apt update\nsudo apt install frankenphp\n```\n\n**Instalación de extensiones:** `sudo apt install php-zts-<extension>`\n\nPara extensiones no disponibles por defecto, usa [PIE](https://github.com/php/pie):\n\n```console\nsudo apt install pie-zts\nsudo pie-zts install asgrim/example-pie-extension\n```\n\n### Docker\n\nLas [imágenes Docker](https://frankenphp.dev/docs/es/docker/) también están disponibles:\n\n```console\ndocker run -v .:/app/public \\\n    -p 80:80 -p 443:443 -p 443:443/udp \\\n    dunglas/frankenphp\n```\n\nVe a `https://localhost`, ¡listo!\n\n> [!TIP]\n>\n> No intentes usar `https://127.0.0.1`. Usa `https://localhost` y acepta el certificado auto-firmado.\n> Usa [la variable de entorno `SERVER_NAME`](config.md#variables-de-entorno) para cambiar el dominio a usar.\n\n### Homebrew\n\nFrankenPHP también está disponible como paquete [Homebrew](https://brew.sh) para macOS y Linux.\n\nPara instalarlo:\n\n```console\nbrew install dunglas/frankenphp/frankenphp\n```\n\n**Instalación de extensiones:** Usa [PIE](https://github.com/php/pie).\n\n### Uso\n\nPara servir el contenido del directorio actual, ejecuta:\n\n```console\nfrankenphp php-server\n```\n\nTambién puedes ejecutar scripts en línea de comandos con:\n\n```console\nfrankenphp php-cli /ruta/a/tu/script.php\n```\n\nPara los paquetes deb y rpm, también puedes iniciar el servicio systemd:\n\n```console\nsudo systemctl start frankenphp\n```\n\n## Documentación\n\n- [El modo clásico](classic.md)\n- [El modo worker](worker.md)\n- [Soporte para Early Hints (código de estado HTTP 103)](early-hints.md)\n- [Tiempo real](mercure.md)\n- [Hot reloading](https://frankenphp.dev/docs/hot-reload/)\n- [Registro de actividad](https://frankenphp.dev/docs/logging/)\n- [Servir eficientemente archivos estáticos grandes](x-sendfile.md)\n- [Configuración](config.md)\n- [Escribir extensiones PHP en Go](extensions.md)\n- [Imágenes Docker](docker.md)\n- [Despliegue en producción](production.md)\n- [Optimización del rendimiento](performance.md)\n- [Crear aplicaciones PHP **autónomas**, auto-ejecutables](embed.md)\n- [Crear una compilación estática](static.md)\n- [Compilar desde las fuentes](compile.md)\n- [Monitoreo de FrankenPHP](metrics.md)\n- [Integración con WordPress](https://frankenphp.dev/docs/wordpress/)\n- [Integración con Laravel](laravel.md)\n- [Problemas conocidos](known-issues.md)\n- [Aplicación de demostración (Symfony) y benchmarks](https://github.com/dunglas/frankenphp-demo)\n- [Documentación de la biblioteca Go](https://pkg.go.dev/github.com/dunglas/frankenphp)\n- [Contribuir y depurar](CONTRIBUTING.md)\n\n## Ejemplos y esqueletos\n\n- [Symfony](https://github.com/dunglas/symfony-docker)\n- [API Platform](https://api-platform.com/docs/distribution/)\n- [Laravel](laravel.md)\n- [Sulu](https://sulu.io/blog/running-sulu-with-frankenphp)\n- [WordPress](https://github.com/StephenMiracle/frankenwp)\n- [Drupal](https://github.com/dunglas/frankenphp-drupal)\n- [Joomla](https://github.com/alexandreelise/frankenphp-joomla)\n- [TYPO3](https://github.com/ochorocho/franken-typo3)\n- [Magento2](https://github.com/ekino/frankenphp-magento2)\n"
  },
  {
    "path": "docs/es/classic.md",
    "content": "# Usando el Modo Clásico\n\nSin ninguna configuración adicional, FrankenPHP opera en modo clásico. En este modo, FrankenPHP funciona como un servidor PHP tradicional, sirviendo directamente archivos PHP. Esto lo convierte en un reemplazo directo para PHP-FPM o Apache con mod_php.\n\nAl igual que Caddy, FrankenPHP acepta un número ilimitado de conexiones y utiliza un [número fijo de hilos](config.md#caddyfile-config) para atenderlas. La cantidad de conexiones aceptadas y en cola está limitada únicamente por los recursos disponibles del sistema.\nEl *pool* de hilos de PHP opera con un número fijo de hilos inicializados al inicio, comparable al modo estático de PHP-FPM. También es posible permitir que los hilos [escale automáticamente en tiempo de ejecución](performance.md#max_threads), similar al modo dinámico de PHP-FPM.\n\nLas conexiones en cola esperarán indefinidamente hasta que un hilo de PHP esté disponible para atenderlas. Para evitar esto, puedes usar la configuración `max_wait_time` en la [configuración global de FrankenPHP](config.md#caddyfile-config) para limitar la duración que una petición puede esperar por un hilo de PHP libre antes de ser rechazada.\nAdicionalmente, puedes establecer un [tiempo límite de escritura razonable en Caddy](https://caddyserver.com/docs/caddyfile/options#timeouts).\n\nCada instancia de Caddy iniciará solo un *pool* de hilos de FrankenPHP, el cual será compartido entre todos los bloques `php_server`.\n"
  },
  {
    "path": "docs/es/compile.md",
    "content": "# Compilar desde fuentes\n\nEste documento explica cómo crear un binario de FrankenPHP que cargará PHP como una biblioteca dinámica.\nEsta es la forma recomendada.\n\nAlternativamente, también se pueden crear [compilaciones estáticas y mayormente estáticas](static.md).\n\n## Instalar PHP\n\nFrankenPHP es compatible con PHP 8.2 y versiones superiores.\n\n### Con Homebrew (Linux y Mac)\n\nLa forma más sencilla de instalar una versión de libphp compatible con FrankenPHP es usar los paquetes ZTS proporcionados por [Homebrew PHP](https://github.com/shivammathur/homebrew-php).\n\nPrimero, si no lo ha hecho ya, instale [Homebrew](https://brew.sh).\n\nLuego, instale la variante ZTS de PHP, Brotli (opcional, para soporte de compresión) y watcher (opcional, para detección de cambios en archivos):\n\n```console\nbrew install shivammathur/php/php-zts brotli watcher\nbrew link --overwrite --force shivammathur/php/php-zts\n```\n\n### Compilando PHP\n\nAlternativamente, puede compilar PHP desde las fuentes con las opciones necesarias para FrankenPHP siguiendo estos pasos.\n\nPrimero, [obtenga las fuentes de PHP](https://www.php.net/downloads.php) y extráigalas:\n\n```console\ntar xf php-*\ncd php-*/\n```\n\nLuego, ejecute el script `configure` con las opciones necesarias para su plataforma.\nLas siguientes banderas de `./configure` son obligatorias, pero puede agregar otras, por ejemplo, para compilar extensiones o características adicionales.\n\n#### Linux\n\n```console\n./configure \\\n    --enable-embed \\\n    --enable-zts \\\n    --disable-zend-signals \\\n    --enable-zend-max-execution-timers\n```\n\n#### Mac\n\nUse el gestor de paquetes [Homebrew](https://brew.sh/) para instalar las dependencias requeridas y opcionales:\n\n```console\nbrew install libiconv bison brotli re2c pkg-config watcher\necho 'export PATH=\"/opt/homebrew/opt/bison/bin:$PATH\"' >> ~/.zshrc\n```\n\nLuego ejecute el script de configuración:\n\n```console\n./configure \\\n    --enable-embed \\\n    --enable-zts \\\n    --disable-zend-signals \\\n    --with-iconv=/opt/homebrew/opt/libiconv/\n```\n\n#### Compilar PHP\n\nFinalmente, compile e instale PHP:\n\n```console\nmake -j\"$(getconf _NPROCESSORS_ONLN)\"\nsudo make install\n```\n\n## Instalar dependencias opcionales\n\nAlgunas características de FrankenPHP dependen de dependencias opcionales del sistema que deben instalarse.\nAlternativamente, estas características pueden deshabilitarse pasando etiquetas de compilación al compilador Go.\n\n| Característica                     | Dependencia                                                                                                   | Etiqueta de compilación para deshabilitarla |\n| ----------------------------------- | ------------------------------------------------------------------------------------------------------------- | -------------------------------------------- |\n| Compresión Brotli                  | [Brotli](https://github.com/google/brotli)                                                                   | nobrotli                                    |\n| Reiniciar workers al cambiar archivos | [Watcher C](https://github.com/e-dant/watcher/tree/release/watcher-c)                                        | nowatcher                                   |\n| [Mercure](mercure.md)              | [Biblioteca Mercure Go](https://pkg.go.dev/github.com/dunglas/mercure) (instalada automáticamente, licencia AGPL) | nomercure                                   |\n\n## Compilar la aplicación Go\n\nAhora puede construir el binario final.\n\n### Usando xcaddy\n\nLa forma recomendada es usar [xcaddy](https://github.com/caddyserver/xcaddy) para compilar FrankenPHP.\n`xcaddy` también permite agregar fácilmente [módulos personalizados de Caddy](https://caddyserver.com/docs/modules/) y extensiones de FrankenPHP:\n\n```console\nCGO_ENABLED=1 \\\nXCADDY_GO_BUILD_FLAGS=\"-ldflags='-w -s' -tags=nobadger,nomysql,nopgx\" \\\nCGO_CFLAGS=$(php-config --includes) \\\nCGO_LDFLAGS=\"$(php-config --ldflags) $(php-config --libs)\" \\\nxcaddy build \\\n    --output frankenphp \\\n    --with github.com/dunglas/frankenphp/caddy \\\n    --with github.com/dunglas/mercure/caddy \\\n    --with github.com/dunglas/vulcain/caddy \\\n    --with github.com/dunglas/caddy-cbrotli\n    # Agregue módulos adicionales de Caddy y extensiones de FrankenPHP aquí\n    # opcionalmente, si desea compilar desde sus fuentes de frankenphp:\n    # --with github.com/dunglas/frankenphp=$(pwd) \\\n    # --with github.com/dunglas/frankenphp/caddy=$(pwd)/caddy\n\n```\n\n> [!TIP]\n>\n> Si está usando musl libc (predeterminado en Alpine Linux) y Symfony,\n> es posible que deba aumentar el tamaño de pila predeterminado.\n> De lo contrario, podría obtener errores como `PHP Fatal error: Maximum call stack size of 83360 bytes reached during compilation. Try splitting expression`\n>\n> Para hacerlo, cambie la variable de entorno `XCADDY_GO_BUILD_FLAGS` a algo como:\n> `XCADDY_GO_BUILD_FLAGS=$'-ldflags \"-w -s -extldflags \\'-Wl,-z,stack-size=0x80000\\'\"'`\n> (cambie el valor del tamaño de pila según las necesidades de su aplicación).\n\n### Sin xcaddy\n\nAlternativamente, es posible compilar FrankenPHP sin `xcaddy` usando directamente el comando `go`:\n\n```console\ncurl -L https://github.com/php/frankenphp/archive/refs/heads/main.tar.gz | tar xz\ncd frankenphp-main/caddy/frankenphp\nCGO_CFLAGS=$(php-config --includes) CGO_LDFLAGS=\"$(php-config --ldflags) $(php-config --libs)\" go build -tags=nobadger,nomysql,nopgx\n```\n"
  },
  {
    "path": "docs/es/config.md",
    "content": "# Configuración\n\nFrankenPHP, Caddy así como los módulos [Mercure](mercure.md) y [Vulcain](https://vulcain.rocks) pueden configurarse usando [los formatos soportados por Caddy](https://caddyserver.com/docs/getting-started#your-first-config).\n\nEl formato más común es el `Caddyfile`, que es un formato de texto simple y legible.\nPor defecto, FrankenPHP buscará un `Caddyfile` en el directorio actual.\nPuede especificar una ruta personalizada con la opción `-c` o `--config`.\n\nUn `Caddyfile` mínimo para servir una aplicación PHP se muestra a continuación:\n\n```caddyfile\n# El nombre de host al que responder\nlocalhost\n\n# Opcionalmente, el directorio desde el que servir archivos, por defecto es el directorio actual\n#root public/\nphp_server\n```\n\nUn `Caddyfile` más avanzado que habilita más características y proporciona variables de entorno convenientes está disponible [en el repositorio de FrankenPHP](https://github.com/php/frankenphp/blob/main/caddy/frankenphp/Caddyfile),\ny con las imágenes de Docker.\n\nPHP en sí puede configurarse [usando un archivo `php.ini`](https://www.php.net/manual/es/configuration.file.php).\n\nDependiendo de su método de instalación, FrankenPHP y el intérprete de PHP buscarán archivos de configuración en las ubicaciones descritas a continuación.\n\n## Docker\n\nFrankenPHP:\n\n- `/etc/frankenphp/Caddyfile`: el archivo de configuración principal\n- `/etc/frankenphp/Caddyfile.d/*.caddyfile`: archivos de configuración adicionales que se cargan automáticamente\n\nPHP:\n\n- `php.ini`: `/usr/local/etc/php/php.ini` (no se proporciona ningún `php.ini` por defecto)\n- archivos de configuración adicionales: `/usr/local/etc/php/conf.d/*.ini`\n- extensiones de PHP: `/usr/local/lib/php/extensions/no-debug-zts-<YYYYMMDD>/`\n- Debe copiar una plantilla oficial proporcionada por el proyecto PHP:\n\n```dockerfile\nFROM dunglas/frankenphp\n\n# Producción:\nRUN cp $PHP_INI_DIR/php.ini-production $PHP_INI_DIR/php.ini\n\n# O desarrollo:\nRUN cp $PHP_INI_DIR/php.ini-development $PHP_INI_DIR/php.ini\n```\n\n## Paquetes RPM y Debian\n\nFrankenPHP:\n\n- `/etc/frankenphp/Caddyfile`: el archivo de configuración principal\n- `/etc/frankenphp/Caddyfile.d/*.caddyfile`: archivos de configuración adicionales que se cargan automáticamente\n\nPHP:\n\n- `php.ini`: `/etc/php-zts/php.ini` (se proporciona un archivo `php.ini` con ajustes de producción por defecto)\n- archivos de configuración adicionales: `/etc/php-zts/conf.d/*.ini`\n\n## Binario estático\n\nFrankenPHP:\n\n- En el directorio de trabajo actual: `Caddyfile`\n\nPHP:\n\n- `php.ini`: El directorio en el que se ejecuta `frankenphp run` o `frankenphp php-server`, luego `/etc/frankenphp/php.ini`\n- archivos de configuración adicionales: `/etc/frankenphp/php.d/*.ini`\n- extensiones de PHP: no pueden cargarse, debe incluirlas en el binario mismo\n- copie uno de `php.ini-production` o `php.ini-development` proporcionados [en las fuentes de PHP](https://github.com/php/php-src/).\n\n## Configuración de Caddyfile\n\nLas directivas `php_server` o `php` [de HTTP](https://caddyserver.com/docs/caddyfile/concepts#directives) pueden usarse dentro de los bloques de sitio para servir su aplicación PHP.\n\nEjemplo mínimo:\n\n```caddyfile\nlocalhost {\n\t# Habilitar compresión (opcional)\n\tencode zstd br gzip\n\t# Ejecutar archivos PHP en el directorio actual y servir activos\n\tphp_server\n}\n```\n\nTambién puede configurar explícitamente FrankenPHP usando la [opción global](https://caddyserver.com/docs/caddyfile/concepts#global-options) `frankenphp`:\n\n```caddyfile\n{\n\tfrankenphp {\n\t\tnum_threads <num_threads> # Establece el número de hilos de PHP para iniciar. Por defecto: 2x el número de CPUs disponibles.\n\t\tmax_threads <num_threads> # Limita el número de hilos de PHP adicionales que pueden iniciarse en tiempo de ejecución. Por defecto: num_threads. Puede establecerse como 'auto'.\n\t\tmax_wait_time <duration> # Establece el tiempo máximo que una solicitud puede esperar por un hilo de PHP libre antes de agotar el tiempo de espera. Por defecto: deshabilitado.\n\t\tphp_ini <key> <value> # Establece una directiva php.ini. Puede usarse varias veces para establecer múltiples directivas.\n\t\tworker {\n\t\t\tfile <path> # Establece la ruta al script del worker.\n\t\t\tnum <num> # Establece el número de hilos de PHP para iniciar, por defecto es 2x el número de CPUs disponibles.\n\t\t\tenv <key> <value> # Establece una variable de entorno adicional con el valor dado. Puede especificarse más de una vez para múltiples variables de entorno.\n\t\t\twatch <path> # Establece la ruta para observar cambios en archivos. Puede especificarse más de una vez para múltiples rutas.\n\t\t\tname <name> # Establece el nombre del worker, usado en logs y métricas. Por defecto: ruta absoluta del archivo del worker\n\t\t\tmax_consecutive_failures <num> # Establece el número máximo de fallos consecutivos antes de que el worker se considere no saludable, -1 significa que el worker siempre se reiniciará. Por defecto: 6.\n\t\t}\n\t}\n}\n\n# ...\n```\n\nAlternativamente, puede usar la forma corta de una línea de la opción `worker`:\n\n```caddyfile\n{\n\tfrankenphp {\n\t\tworker <file> <num>\n\t}\n}\n\n# ...\n```\n\nTambién puede definir múltiples workers si sirve múltiples aplicaciones en el mismo servidor:\n\n```caddyfile\napp.example.com {\n    root /path/to/app/public\n\tphp_server {\n\t\troot /path/to/app/public # permite un mejor almacenamiento en caché\n\t\tworker index.php <num>\n\t}\n}\n\nother.example.com {\n    root /path/to/other/public\n\tphp_server {\n\t\troot /path/to/other/public\n\t\tworker index.php <num>\n\t}\n}\n\n# ...\n```\n\nUsar la directiva `php_server` es generalmente lo que necesita,\npero si necesita un control total, puede usar la directiva de bajo nivel `php`.\nLa directiva `php` pasa toda la entrada a PHP, en lugar de verificar primero si\nes un archivo PHP o no. Lea más sobre esto en la [página de rendimiento](performance.md#try_files).\n\nUsar la directiva `php_server` es equivalente a esta configuración:\n\n```caddyfile\nroute {\n\t# Agrega barra final para solicitudes de directorio\n\t@canonicalPath {\n\t\tfile {path}/index.php\n\t\tnot path */\n\t}\n\tredir @canonicalPath {path}/ 308\n\t# Si el archivo solicitado no existe, intenta archivos índice\n\t@indexFiles file {\n\t\ttry_files {path} {path}/index.php index.php\n\t\tsplit_path .php\n\t}\n\trewrite @indexFiles {http.matchers.file.relative}\n\t# ¡FrankenPHP!\n\t@phpFiles path *.php\n\tphp @phpFiles\n\tfile_server\n}\n```\n\nLas directivas `php_server` y `php` tienen las siguientes opciones:\n\n```caddyfile\nphp_server [<matcher>] {\n\troot <directory> # Establece la carpeta raíz del sitio. Por defecto: directiva `root`.\n\tsplit_path <delim...> # Establece las subcadenas para dividir la URI en dos partes. La primera subcadena coincidente se usará para dividir la \"información de ruta\" del path. La primera parte se sufija con la subcadena coincidente y se asumirá como el nombre del recurso real (script CGI). La segunda parte se establecerá como PATH_INFO para que el script la use. Por defecto: `.php`\n\tresolve_root_symlink false # Desactiva la resolución del directorio `root` a su valor real evaluando un enlace simbólico, si existe (habilitado por defecto).\n\tenv <key> <value> # Establece una variable de entorno adicional con el valor dado. Puede especificarse más de una vez para múltiples variables de entorno.\n\tfile_server off # Desactiva la directiva incorporada file_server.\n\tworker { # Crea un worker específico para este servidor. Puede especificarse más de una vez para múltiples workers.\n\t\tfile <path> # Establece la ruta al script del worker, puede ser relativa a la raíz de php_server\n\t\tnum <num> # Establece el número de hilos de PHP para iniciar, por defecto es 2x el número de CPUs disponibles.\n\t\tname <name> # Establece el nombre para el worker, usado en logs y métricas. Por defecto: ruta absoluta del archivo del worker. Siempre comienza con m# cuando se define en un bloque php_server.\n\t\twatch <path> # Establece la ruta para observar cambios en archivos. Puede especificarse más de una vez para múltiples rutas.\n\t\tenv <key> <value> # Establece una variable de entorno adicional con el valor dado. Puede especificarse más de una vez para múltiples variables de entorno. Las variables de entorno para este worker también se heredan del php_server padre, pero pueden sobrescribirse aquí.\n\t\tmatch <path> # hace coincidir el worker con un patrón de ruta. Anula try_files y solo puede usarse en la directiva php_server.\n\t}\n\tworker <other_file> <num> # También puede usar la forma corta como en el bloque global frankenphp.\n}\n```\n\n### Observando cambios en archivos\n\nDado que los workers solo inician su aplicación una vez y la mantienen en memoria, cualquier cambio\nen sus archivos PHP no se reflejará inmediatamente.\n\nLos workers pueden reiniciarse al cambiar archivos mediante la directiva `watch`.\nEsto es útil para entornos de desarrollo.\n\n```caddyfile\n{\n\tfrankenphp {\n\t\tworker {\n\t\t\tfile  /path/to/app/public/worker.php\n\t\t\twatch\n\t\t}\n\t}\n}\n```\n\nEsta función se utiliza frecuentemente en combinación con [hot reload](hot-reload.md).\n\nSi el directorio `watch` no está especificado, retrocederá a `./**/*.{env,php,twig,yaml,yml}`,\nlo cual vigila todos los archivos `.env`, `.php`, `.twig`, `.yaml` y `.yml` en el directorio y subdirectorios\ndonde se inició el proceso de FrankenPHP. También puede especificar uno o más directorios mediante un\n[patrón de nombres de ficheros de shell](https://pkg.go.dev/path/filepath#Match):\n\n```caddyfile\n{\n\tfrankenphp {\n\t\tworker {\n\t\t\tfile  /path/to/app/public/worker.php\n\t\t\twatch /path/to/app # observa todos los archivos en todos los subdirectorios de /path/to/app\n\t\t\twatch /path/to/app/*.php # observa archivos que terminan en .php en /path/to/app\n\t\t\twatch /path/to/app/**/*.php # observa archivos PHP en /path/to/app y subdirectorios\n\t\t\twatch /path/to/app/**/*.{php,twig} # observa archivos PHP y Twig en /path/to/app y subdirectorios\n\t\t}\n\t}\n}\n```\n\n- El patrón `**` significa observación recursiva\n- Los directorios también pueden ser relativos (al lugar donde se inicia el proceso de FrankenPHP)\n- Si tiene múltiples workers definidos, todos ellos se reiniciarán cuando cambie un archivo\n- Tenga cuidado al observar archivos que se crean en tiempo de ejecución (como logs) ya que podrían causar reinicios no deseados de workers.\n\nEl observador de archivos se basa en [e-dant/watcher](https://github.com/e-dant/watcher).\n\n## Coincidencia del worker con una ruta\n\nEn aplicaciones PHP tradicionales, los scripts siempre se colocan en el directorio público.\nEsto también es cierto para los scripts de workers, que se tratan como cualquier otro script PHP.\nSi desea colocar el script del worker fuera del directorio público, puede hacerlo mediante la directiva `match`.\n\nLa directiva `match` es una alternativa optimizada a `try_files` solo disponible dentro de `php_server` y `php`.\nEl siguiente ejemplo siempre servirá un archivo en el directorio público si está presente\ny de lo contrario reenviará la solicitud al worker que coincida con el patrón de ruta.\n\n```caddyfile\n{\n\tfrankenphp {\n\t\tphp_server {\n\t\t\tworker {\n\t\t\t\tfile /path/to/worker.php # el archivo puede estar fuera de la ruta pública\n\t\t\t\tmatch /api/* # todas las solicitudes que comiencen con /api/ serán manejadas por este worker\n\t\t\t}\n\t\t}\n\t}\n}\n```\n\n## Variables de entorno\n\nLas siguientes variables de entorno pueden usarse para inyectar directivas de Caddy en el `Caddyfile` sin modificarlo:\n\n- `SERVER_NAME`: cambia [las direcciones en las que escuchar](https://caddyserver.com/docs/caddyfile/concepts#addresses), los nombres de host proporcionados también se usarán para el certificado TLS generado\n- `SERVER_ROOT`: cambia el directorio raíz del sitio, por defecto es `public/`\n- `CADDY_GLOBAL_OPTIONS`: inyecta [opciones globales](https://caddyserver.com/docs/caddyfile/options)\n- `FRANKENPHP_CONFIG`: inyecta configuración bajo la directiva `frankenphp`\n\nAl igual que en FPM y SAPIs CLI, las variables de entorno se exponen por defecto en la superglobal `$_SERVER`.\n\nEl valor `S` de [la directiva `variables_order` de PHP](https://www.php.net/manual/en/ini.core.php#ini.variables-order) siempre es equivalente a `ES` independientemente de la ubicación de `E` en otro lugar de esta directiva.\n\n## Configuración de PHP\n\nPara cargar [archivos de configuración adicionales de PHP](https://www.php.net/manual/en/configuration.file.php#configuration.file.scan),\npuede usarse la variable de entorno `PHP_INI_SCAN_DIR`.\nCuando se establece, PHP cargará todos los archivos con la extensión `.ini` presentes en los directorios dados.\n\nTambién puede cambiar la configuración de PHP usando la directiva `php_ini` en el `Caddyfile`:\n\n```caddyfile\n{\n    frankenphp {\n        php_ini memory_limit 256M\n\n        # o\n\n        php_ini {\n            memory_limit 256M\n            max_execution_time 15\n        }\n    }\n}\n```\n\n### Deshabilitar HTTPS\n\nPor defecto, FrankenPHP habilitará automáticamente HTTPS para todos los nombres de host, incluyendo `localhost`.\nSi desea deshabilitar HTTPS (por ejemplo en un entorno de desarrollo), puede establecer la variable de entorno `SERVER_NAME` a `http://` o `:80`:\n\nAlternativamente, puede usar todos los otros métodos descritos en la [documentación de Caddy](https://caddyserver.com/docs/automatic-https#activation).\n\nSi desea usar HTTPS con la dirección IP `127.0.0.1` en lugar del nombre de host `localhost`, lea la sección de [problemas conocidos](known-issues.md#using-https127001-with-docker).\n\n### Dúplex completo (HTTP/1)\n\nAl usar HTTP/1.x, puede ser deseable habilitar el modo dúplex completo para permitir escribir una respuesta antes de que se haya leído todo el cuerpo\n(por ejemplo: [Mercure](mercure.md), WebSocket, Eventos enviados por el servidor, etc.).\n\nEsta es una configuración opcional que debe agregarse a las opciones globales en el `Caddyfile`:\n\n```caddyfile\n{\n  servers {\n    enable_full_duplex\n  }\n}\n```\n\n> [!CAUTION]\n>\n> Habilitar esta opción puede causar que clientes HTTP/1.x antiguos que no soportan dúplex completo se bloqueen.\n> Esto también puede configurarse usando la configuración de entorno `CADDY_GLOBAL_OPTIONS`:\n\n```sh\nCADDY_GLOBAL_OPTIONS=\"servers {\n  enable_full_duplex\n}\"\n```\n\nPuede encontrar más información sobre esta configuración en la [documentación de Caddy](https://caddyserver.com/docs/caddyfile/options#enable-full-duplex).\n\n## Habilitar el modo de depuración\n\nAl usar la imagen de Docker, establezca la variable de entorno `CADDY_GLOBAL_OPTIONS` a `debug` para habilitar el modo de depuración:\n\n```console\ndocker run -v $PWD:/app/public \\\n    -e CADDY_GLOBAL_OPTIONS=debug \\\n    -p 80:80 -p 443:443 -p 443:443/udp \\\n    dunglas/frankenphp\n```\n"
  },
  {
    "path": "docs/es/docker.md",
    "content": "# Construir una imagen Docker personalizada\n\nLas [imágenes Docker de FrankenPHP](https://hub.docker.com/r/dunglas/frankenphp) están basadas en [imágenes oficiales de PHP](https://hub.docker.com/_/php/).\nSe proporcionan variantes para Debian y Alpine Linux en arquitecturas populares.\nSe recomiendan las variantes de Debian.\n\nSe proporcionan variantes para PHP 8.2, 8.3, 8.4 y 8.5.\n\nLas etiquetas siguen este patrón: `dunglas/frankenphp:<versión-frankenphp>-php<versión-php>-<sistema-operativo>`\n\n- `<versión-frankenphp>` y `<versión-php>` son los números de versión de FrankenPHP y PHP respectivamente, que van desde versiones principales (ej. `1`), menores (ej. `1.2`) hasta versiones de parche (ej. `1.2.3`).\n- `<sistema-operativo>` es `trixie` (para Debian Trixie), `bookworm` (para Debian Bookworm) o `alpine` (para la última versión estable de Alpine).\n\n[Explorar etiquetas](https://hub.docker.com/r/dunglas/frankenphp/tags).\n\n## Cómo usar las imágenes\n\nCree un archivo `Dockerfile` en su proyecto:\n\n```dockerfile\nFROM dunglas/frankenphp\n\nCOPY . /app/public\n```\n\nLuego, ejecute estos comandos para construir y ejecutar la imagen Docker:\n\n```console\ndocker build -t my-php-app .\ndocker run -it --rm --name my-running-app my-php-app\n```\n\n## Cómo ajustar la configuración\n\nPara mayor comodidad, se proporciona en la imagen un [archivo `Caddyfile` predeterminado](https://github.com/php/frankenphp/blob/main/caddy/frankenphp/Caddyfile) que contiene variables de entorno útiles.\n\n## Cómo instalar más extensiones de PHP\n\nEl script [`docker-php-extension-installer`](https://github.com/mlocati/docker-php-extension-installer) está disponible en la imagen base.\nAgregar extensiones adicionales de PHP es sencillo:\n\n```dockerfile\nFROM dunglas/frankenphp\n\n# agregue extensiones adicionales aquí:\nRUN install-php-extensions \\\n\tpdo_mysql \\\n\tgd \\\n\tintl \\\n\tzip \\\n\topcache\n```\n\n## Cómo instalar más módulos de Caddy\n\nFrankenPHP está construido sobre Caddy, y todos los [módulos de Caddy](https://caddyserver.com/docs/modules/) pueden usarse con FrankenPHP.\n\nLa forma más fácil de instalar módulos personalizados de Caddy es usar [xcaddy](https://github.com/caddyserver/xcaddy):\n\n```dockerfile\nFROM dunglas/frankenphp:builder AS builder\n\n# Copie xcaddy en la imagen del constructor\nCOPY --from=caddy:builder /usr/bin/xcaddy /usr/bin/xcaddy\n\n# CGO debe estar habilitado para construir FrankenPHP\nRUN CGO_ENABLED=1 \\\n    XCADDY_SETCAP=1 \\\n    XCADDY_GO_BUILD_FLAGS=\"-ldflags='-w -s' -tags=nobadger,nomysql,nopgx\" \\\n    CGO_CFLAGS=$(php-config --includes) \\\n    CGO_LDFLAGS=\"$(php-config --ldflags) $(php-config --libs)\" \\\n    xcaddy build \\\n        --output /usr/local/bin/frankenphp \\\n        --with github.com/dunglas/frankenphp=./ \\\n        --with github.com/dunglas/frankenphp/caddy=./caddy/ \\\n        --with github.com/dunglas/caddy-cbrotli \\\n        # Mercure y Vulcain están incluidos en la compilación oficial, pero puede eliminarlos si lo desea\n        --with github.com/dunglas/mercure/caddy \\\n        --with github.com/dunglas/vulcain/caddy\n        # Agregue módulos adicionales de Caddy aquí\n\nFROM dunglas/frankenphp AS runner\n\n# Reemplace el binario oficial por el que contiene sus módulos personalizados\nCOPY --from=builder /usr/local/bin/frankenphp /usr/local/bin/frankenphp\n```\n\nLa imagen `builder` proporcionada por FrankenPHP contiene una versión compilada de `libphp`.\nSe proporcionan [imágenes de constructor](https://hub.docker.com/r/dunglas/frankenphp/tags?name=builder) para todas las versiones de FrankenPHP y PHP, tanto para Debian como para Alpine.\n\n> [!TIP]\n>\n> Si está usando Alpine Linux y Symfony,\n> es posible que deba [aumentar el tamaño de pila predeterminado](compile.md#using-xcaddy).\n\n## Habilitar el modo Worker por defecto\n\nEstablezca la variable de entorno `FRANKENPHP_CONFIG` para iniciar FrankenPHP con un script de worker:\n\n```dockerfile\nFROM dunglas/frankenphp\n\n# ...\n\nENV FRANKENPHP_CONFIG=\"worker ./public/index.php\"\n```\n\n## Usar un volumen en desarrollo\n\nPara desarrollar fácilmente con FrankenPHP, monte el directorio de su host que contiene el código fuente de la aplicación como un volumen en el contenedor Docker:\n\n```console\ndocker run -v $PWD:/app/public -p 80:80 -p 443:443 -p 443:443/udp --tty my-php-app\n```\n\n> [!TIP]\n>\n> La opción `--tty` permite tener logs legibles en lugar de logs en formato JSON.\n\nCon Docker Compose:\n\n```yaml\n# compose.yaml\n\nservices:\n  php:\n    image: dunglas/frankenphp\n    # descomente la siguiente línea si desea usar un Dockerfile personalizado\n    #build: .\n    # descomente la siguiente línea si desea ejecutar esto en un entorno de producción\n    # restart: always\n    ports:\n      - \"80:80\" # HTTP\n      - \"443:443\" # HTTPS\n      - \"443:443/udp\" # HTTP/3\n    volumes:\n      - ./:/app/public\n      - caddy_data:/data\n      - caddy_config:/config\n    # comente la siguiente línea en producción, permite tener logs legibles en desarrollo\n    tty: true\n\n# Volúmenes necesarios para los certificados y configuración de Caddy\nvolumes:\n  caddy_data:\n  caddy_config:\n```\n\n## Ejecutar como usuario no root\n\nFrankenPHP puede ejecutarse como usuario no root en Docker.\n\nAquí hay un ejemplo de `Dockerfile` que hace esto:\n\n```dockerfile\nFROM dunglas/frankenphp\n\nARG USER=appuser\n\nRUN \\\n\t# Use \"adduser -D ${USER}\" para distribuciones basadas en alpine\n\tuseradd ${USER}; \\\n\t# Agregar capacidad adicional para enlazar a los puertos 80 y 443\n\tsetcap CAP_NET_BIND_SERVICE=+eip /usr/local/bin/frankenphp; \\\n\t# Dar acceso de escritura a /config/caddy y /data/caddy\n\tchown -R ${USER}:${USER} /config/caddy /data/caddy\n\nUSER ${USER}\n```\n\n### Ejecutar sin capacidades\n\nIncluso cuando se ejecuta sin root, FrankenPHP necesita la capacidad `CAP_NET_BIND_SERVICE` para enlazar el servidor web en puertos privilegiados (80 y 443).\n\nSi expone FrankenPHP en un puerto no privilegiado (1024 y superior), es posible ejecutar el servidor web como usuario no root, y sin necesidad de ninguna capacidad:\n\n```dockerfile\nFROM dunglas/frankenphp\n\nARG USER=appuser\n\nRUN \\\n\t# Use \"adduser -D ${USER}\" para distribuciones basadas en alpine\n\tuseradd ${USER}; \\\n\t# Eliminar la capacidad predeterminada\n\tsetcap -r /usr/local/bin/frankenphp; \\\n\t# Dar acceso de escritura a /config/caddy y /data/caddy\n\tchown -R ${USER}:${USER} /config/caddy /data/caddy\n\nUSER ${USER}\n```\n\nLuego, establezca la variable de entorno `SERVER_NAME` para usar un puerto no privilegiado.\nEjemplo: `:8000`\n\n## Actualizaciones\n\nLas imágenes Docker se construyen:\n\n- Cuando se etiqueta una nueva versión\n- Diariamente a las 4 am UTC, si hay nuevas versiones de las imágenes oficiales de PHP disponibles\n\n## Endurecimiento de Imágenes\n\nPara reducir aún más la superficie de ataque y el tamaño de tus imágenes Docker de FrankenPHP, también es posible construirlas sobre una imagen\n[Google distroless](https://github.com/GoogleContainerTools/distroless) o\n[Docker hardened](https://www.docker.com/products/hardened-images).\n\n> [!WARNING]\n> Estas imágenes base mínimas no incluyen un shell ni gestor de paquetes, lo que hace que la depuración sea más difícil.\n> Por lo tanto, se recomiendan solo para producción si la seguridad es una alta prioridad.\n\nCuando agregues extensiones PHP adicionales, necesitarás una etapa de construcción intermedia:\n\n```dockerfile\nFROM dunglas/frankenphp AS builder\n\n# Agregar extensiones PHP adicionales aquí\nRUN install-php-extensions pdo_mysql pdo_pgsql #...\n\n# Copiar bibliotecas compartidas de frankenphp y todas las extensiones instaladas a una ubicación temporal\n# También puedes hacer este paso manualmente analizando la salida de ldd del binario frankenphp y cada archivo .so de extensión\nRUN apt-get update && apt-get install -y libtree && \\\n    EXT_DIR=\"$(php -r 'echo ini_get(\"extension_dir\");')\" && \\\n    FRANKENPHP_BIN=\"$(which frankenphp)\"; \\\n    LIBS_TMP_DIR=\"/tmp/libs\"; \\\n    mkdir -p \"$LIBS_TMP_DIR\"; \\\n    for target in \"$FRANKENPHP_BIN\" $(find \"$EXT_DIR\" -maxdepth 2 -type f -name \"*.so\"); do \\\n        libtree -pv \"$target\" | sed 's/.*── \\(.*\\) \\[.*/\\1/' | grep -v \"^$target\" | while IFS= read -r lib; do \\\n            [ -z \"$lib\" ] && continue; \\\n            base=$(basename \"$lib\"); \\\n            destfile=\"$LIBS_TMP_DIR/$base\"; \\\n            if [ ! -f \"$destfile\" ]; then \\\n                cp \"$lib\" \"$destfile\"; \\\n            fi; \\\n        done; \\\n    done\n\n\n# Imagen base distroless de Debian, asegúrate de que sea la misma versión de Debian que la imagen base\nFROM gcr.io/distroless/base-debian13\n# Alternativa de imagen endurecida de Docker\n# FROM dhi.io/debian:13\n\n# Ubicación de tu aplicación y Caddyfile que se copiará al contenedor\nARG PATH_TO_APP=\".\"\nARG PATH_TO_CADDYFILE=\"./Caddyfile\"\n\n# Copiar tu aplicación en /app\n# Para mayor endurecimiento asegúrate de que solo las rutas escribibles sean propiedad del usuario nonroot\nCOPY --chown=nonroot:nonroot \"$PATH_TO_APP\" /app\nCOPY \"$PATH_TO_CADDYFILE\" /etc/caddy/Caddyfile\n\n# Copiar frankenphp y bibliotecas necesarias\nCOPY --from=builder /usr/local/bin/frankenphp /usr/local/bin/frankenphp\nCOPY --from=builder /usr/local/lib/php/extensions /usr/local/lib/php/extensions\nCOPY --from=builder /tmp/libs /usr/lib\n\n# Copiar archivos de configuración php.ini\nCOPY --from=builder /usr/local/etc/php/conf.d /usr/local/etc/php/conf.d\nCOPY --from=builder /usr/local/etc/php/php.ini-production /usr/local/etc/php/php.ini\n\n# Directorios de datos de Caddy — deben ser escribibles para nonroot, incluso en un sistema de archivos raíz de solo lectura\nENV XDG_CONFIG_HOME=/config \\\n    XDG_DATA_HOME=/data\nCOPY --from=builder --chown=nonroot:nonroot /data/caddy /data/caddy\nCOPY --from=builder --chown=nonroot:nonroot /config/caddy /config/caddy\n\nUSER nonroot\n\nWORKDIR /app\n\n# punto de entrada para ejecutar frankenphp con el Caddyfile proporcionado\nENTRYPOINT [\"/usr/local/bin/frankenphp\", \"run\", \"-c\", \"/etc/caddy/Caddyfile\"]\n```\n\n## Versiones de desarrollo\n\nLas versiones de desarrollo están disponibles en el [repositorio Docker `dunglas/frankenphp-dev`](https://hub.docker.com/repository/docker/dunglas/frankenphp-dev).\nSe activa una nueva compilación cada vez que se envía un commit a la rama principal del repositorio de GitHub.\n\nLas etiquetas `latest*` apuntan a la cabeza de la rama `main`.\nTambién están disponibles etiquetas de la forma `sha-<hash-del-commit-git>`.\n"
  },
  {
    "path": "docs/es/early-hints.md",
    "content": "# Early Hints (Pistas Tempranas)\n\nFrankenPHP soporta nativamente el [código de estado 103 Early Hints](https://developer.chrome.com/blog/early-hints/).\nEl uso de Early Hints puede mejorar el tiempo de carga de sus páginas web hasta en un 30%.\n\n```php\n<?php\n\nheader('Link: </style.css>; rel=preload; as=style');\nheaders_send(103);\n\n// sus algoritmos lentos y consultas SQL 🤪\n\necho <<<'HTML'\n<!DOCTYPE html>\n<title>Hola FrankenPHP</title>\n<link rel=\"stylesheet\" href=\"style.css\">\nHTML;\n```\n\nEarly Hints están soportados tanto en el modo normal como en el modo [worker](worker.md).\n"
  },
  {
    "path": "docs/es/embed.md",
    "content": "# Aplicaciones PHP como Binarios Autónomos\n\nFrankenPHP tiene la capacidad de incrustar el código fuente y los activos de aplicaciones PHP en un binario estático y autónomo.\n\nGracias a esta característica, las aplicaciones PHP pueden distribuirse como binarios autónomos que incluyen la aplicación en sí, el intérprete de PHP y Caddy, un servidor web de nivel de producción.\n\nObtenga más información sobre esta característica [en la presentación realizada por Kévin en SymfonyCon 2023](https://dunglas.dev/2023/12/php-and-symfony-apps-as-standalone-binaries/).\n\nPara incrustar aplicaciones Laravel, [lea esta entrada específica de documentación](laravel.md#laravel-apps-as-standalone-binaries).\n\n## Preparando su Aplicación\n\nAntes de crear el binario autónomo, asegúrese de que su aplicación esté lista para ser incrustada.\n\nPor ejemplo, probablemente querrá:\n\n- Instalar las dependencias de producción de la aplicación\n- Volcar el autoload\n- Activar el modo de producción de su aplicación (si lo hay)\n- Eliminar archivos innecesarios como `.git` o pruebas para reducir el tamaño de su binario final\n\nPor ejemplo, para una aplicación Symfony, puede usar los siguientes comandos:\n\n```console\n# Exportar el proyecto para deshacerse de .git/, etc.\nmkdir $TMPDIR/my-prepared-app\ngit archive HEAD | tar -x -C $TMPDIR/my-prepared-app\ncd $TMPDIR/my-prepared-app\n\n# Establecer las variables de entorno adecuadas\necho APP_ENV=prod > .env.local\necho APP_DEBUG=0 >> .env.local\n\n# Eliminar las pruebas y otros archivos innecesarios para ahorrar espacio\n# Alternativamente, agregue estos archivos con el atributo export-ignore en su archivo .gitattributes\nrm -Rf tests/\n\n# Instalar las dependencias\ncomposer install --ignore-platform-reqs --no-dev -a\n\n# Optimizar .env\ncomposer dump-env prod\n```\n\n### Personalizar la Configuración\n\nPara personalizar [la configuración](config.md), puede colocar un archivo `Caddyfile` así como un archivo `php.ini`\nen el directorio principal de la aplicación a incrustar (`$TMPDIR/my-prepared-app` en el ejemplo anterior).\n\n## Crear un Binario para Linux\n\nLa forma más fácil de crear un binario para Linux es usar el constructor basado en Docker que proporcionamos.\n\n1. Cree un archivo llamado `static-build.Dockerfile` en el repositorio de su aplicación:\n\n   ```dockerfile\n   FROM --platform=linux/amd64 dunglas/frankenphp:static-builder-gnu\n   # Si tiene la intención de ejecutar el binario en sistemas musl-libc, use static-builder-musl en su lugar\n\n   # Copie su aplicación\n   WORKDIR /go/src/app/dist/app\n   COPY . .\n\n   # Construya el binario estático\n   WORKDIR /go/src/app/\n   RUN EMBED=dist/app/ ./build-static.sh\n   ```\n\n    > [!CAUTION]\n   >\n   > Algunos archivos `.dockerignore` (por ejemplo, el [`.dockerignore` predeterminado de Symfony Docker](https://github.com/dunglas/symfony-docker/blob/main/.dockerignore))\n   > ignorarán el directorio `vendor/` y los archivos `.env`. Asegúrese de ajustar o eliminar el archivo `.dockerignore` antes de la construcción.\n\n2. Construya:\n\n   ```console\n   docker build -t static-app -f static-build.Dockerfile .\n   ```\n\n3. Extraiga el binario:\n\n   ```console\n   docker cp $(docker create --name static-app-tmp static-app):/go/src/app/dist/frankenphp-linux-x86_64 my-app ; docker rm static-app-tmp\n   ```\n\nEl binario resultante es el archivo llamado `my-app` en el directorio actual.\n\n## Crear un Binario para Otros Sistemas Operativos\n\nSi no desea usar Docker o desea construir un binario para macOS, use el script de shell que proporcionamos:\n\n```console\ngit clone https://github.com/php/frankenphp\ncd frankenphp\nEMBED=/path/to/your/app ./build-static.sh\n```\n\nEl binario resultante es el archivo llamado `frankenphp-<os>-<arch>` en el directorio `dist/`.\n\n## Usar el Binario\n\n¡Listo! El archivo `my-app` (o `dist/frankenphp-<os>-<arch>` en otros sistemas operativos) contiene su aplicación autónoma.\n\nPara iniciar la aplicación web, ejecute:\n\n```console\n./my-app php-server\n```\n\nSi su aplicación contiene un [script worker](worker.md), inicie el worker con algo como:\n\n```console\n./my-app php-server --worker public/index.php\n```\n\nPara habilitar HTTPS (se crea automáticamente un certificado de Let's Encrypt), HTTP/2 y HTTP/3, especifique el nombre de dominio a usar:\n\n```console\n./my-app php-server --domain localhost\n```\n\nTambién puede ejecutar los scripts CLI de PHP incrustados en su binario:\n\n```console\n./my-app php-cli bin/console\n```\n\n## Extensiones de PHP\n\nPor defecto, el script construirá las extensiones requeridas por el archivo `composer.json` de su proyecto, si existe.\nSi el archivo `composer.json` no existe, se construirán las extensiones predeterminadas, como se documenta en [la entrada de compilaciones estáticas](static.md).\n\nPara personalizar las extensiones, use la variable de entorno `PHP_EXTENSIONS`.\n\n## Personalizar la Compilación\n\n[Lea la documentación de compilación estática](static.md) para ver cómo personalizar el binario (extensiones, versión de PHP, etc.).\n\n## Distribuir el Binario\n\nEn Linux, el binario creado se comprime usando [UPX](https://upx.github.io).\n\nEn Mac, para reducir el tamaño del archivo antes de enviarlo, puede comprimirlo.\nRecomendamos `xz`.\n"
  },
  {
    "path": "docs/es/extension-workers.md",
    "content": "# Extension Workers\n\nLos Extension Workers permiten que tu [extensión FrankenPHP](https://frankenphp.dev/docs/extensions/) gestione un pool dedicado de hilos PHP para ejecutar tareas en segundo plano, manejar eventos asíncronos o implementar protocolos personalizados. Útil para sistemas de colas, listeners de eventos, programadores, etc.\n\n## Registrando el Worker\n\n### Registro Estático\n\nSi no necesitas hacer que el worker sea configurable por el usuario (ruta de script fija, número fijo de hilos), simplemente puedes registrar el worker en la función `init()`.\n\n```go\npackage myextension\n\nimport (\n\t\"github.com/dunglas/frankenphp\"\n\t\"github.com/dunglas/frankenphp/caddy\"\n)\n\n// Manejador global para comunicarse con el pool de workers\nvar worker frankenphp.Workers\n\nfunc init() {\n\t// Registrar el worker cuando se carga el módulo.\n\tworker = caddy.RegisterWorkers(\n\t\t\"my-internal-worker\", // Nombre único\n\t\t\"worker.php\",         // Ruta del script (relativa a la ejecución o absoluta)\n\t\t2,                    // Número fijo de hilos\n\t\t// Hooks de ciclo de vida opcionales\n\t\tfrankenphp.WithWorkerOnServerStartup(func() {\n\t\t\t// Lógica de configuración global...\n\t\t}),\n\t)\n}\n```\n\n### En un Módulo Caddy (Configurable por el usuario)\n\nSi planeas compartir tu extensión (como una cola genérica o listener de eventos), deberías envolverla en un módulo Caddy. Esto permite a los usuarios configurar la ruta del script y el número de hilos a través de su `Caddyfile`. Esto requiere implementar la interfaz `caddy.Provisioner` y analizar el Caddyfile ([ver un ejemplo](https://github.com/dunglas/frankenphp-queue/blob/989120d394d66dd6c8e2101cac73dd622fade334/caddy.go)).\n\n### En una Aplicación Go Pura (Embedding)\n\nSi estás [embebiendo FrankenPHP en una aplicación Go estándar sin caddy](https://pkg.go.dev/github.com/dunglas/frankenphp#example-ServeHTTP), puedes registrar extension workers usando `frankenphp.WithExtensionWorkers` al inicializar las opciones.\n\n## Interactuando con Workers\n\nUna vez que el pool de workers está activo, puedes enviar tareas a él. Esto se puede hacer dentro de [funciones nativas exportadas a PHP](https://frankenphp.dev/docs/extensions/#writing-the-extension), o desde cualquier lógica Go como un programador cron, un listener de eventos (MQTT, Kafka), o cualquier otra goroutine.\n\n### Modo Sin Cabeza: `SendMessage`\n\nUsa `SendMessage` para pasar datos sin procesar directamente a tu script worker. Esto es ideal para colas o comandos simples.\n\n#### Ejemplo: Una Extensión de Cola Asíncrona\n\n```go\n// #include <Zend/zend_types.h>\nimport \"C\"\nimport (\n\t\"context\"\n\t\"unsafe\"\n\t\"github.com/dunglas/frankenphp\"\n)\n\n//export_php:function my_queue_push(mixed $data): bool\nfunc my_queue_push(data *C.zval) bool {\n\t// 1. Asegurar que el worker esté listo\n\tif worker == nil {\n\t\treturn false\n\t}\n\n\t// 2. Enviar al worker en segundo plano\n\t_, err := worker.SendMessage(\n\t\tcontext.Background(), // Contexto estándar de Go\n\t\tunsafe.Pointer(data), // Datos para pasar al worker\n\t\tnil, // http.ResponseWriter opcional\n\t)\n\n\treturn err == nil\n}\n```\n\n### Emulación HTTP: `SendRequest`\n\nUsa `SendRequest` si tu extensión necesita invocar un script PHP que espera un entorno web estándar (poblando `$_SERVER`, `$_GET`, etc.).\n\n```go\n// #include <Zend/zend_types.h>\nimport \"C\"\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"unsafe\"\n\t\"github.com/dunglas/frankenphp\"\n)\n\n//export_php:function my_worker_http_request(string $path): string\nfunc my_worker_http_request(path *C.zend_string) unsafe.Pointer {\n\t// 1. Preparar la solicitud y el grabador\n\turl := frankenphp.GoString(unsafe.Pointer(path))\n\treq, _ := http.NewRequest(\"GET\", url, http.NoBody)\n\trr := httptest.NewRecorder()\n\n\t// 2. Enviar al worker\n\tif err := worker.SendRequest(rr, req); err != nil {\n\t\treturn nil\n\t}\n\n\t// 3. Devolver la respuesta capturada\n\treturn frankenphp.PHPString(rr.Body.String(), false)\n}\n```\n\n## Script Worker\n\nEl script worker PHP se ejecuta en un bucle y puede manejar tanto mensajes sin procesar como solicitudes HTTP.\n\n```php\n<?php\n// Manejar tanto mensajes sin procesar como solicitudes HTTP en el mismo bucle\n$handler = function ($payload = null) {\n    // Caso 1: Modo Mensaje\n    if ($payload !== null) {\n        return \"Payload recibido: \" . $payload;\n    }\n\n    // Caso 2: Modo HTTP (las superglobales estándar de PHP están pobladas)\n    echo \"Hola desde la página: \" . $_SERVER['REQUEST_URI'];\n};\n\nwhile (frankenphp_handle_request($handler)) {\n    gc_collect_cycles();\n}\n```\n\n## Hooks de Ciclo de Vida\n\nFrankenPHP proporciona hooks para ejecutar código Go en puntos específicos del ciclo de vida.\n\n| Tipo de Hook | Nombre de Opción             | Firma                | Contexto y Caso de Uso                                                      |\n| :----------- | :--------------------------- | :------------------- | :-------------------------------------------------------------------------- |\n| **Server**   | `WithWorkerOnServerStartup`  | `func()`             | Configuración global. Se ejecuta **Una vez**. Ejemplo: Conectar a NATS/Redis. |\n| **Server**   | `WithWorkerOnServerShutdown` | `func()`             | Limpieza global. Se ejecuta **Una vez**. Ejemplo: Cerrar conexiones compartidas. |\n| **Thread**   | `WithWorkerOnReady`          | `func(threadID int)` | Configuración por hilo. Llamado cuando un hilo inicia. Recibe el ID del hilo. |\n| **Thread**   | `WithWorkerOnShutdown`       | `func(threadID int)` | Limpieza por hilo. Recibe el ID del hilo.                                   |\n\n### Ejemplo\n\n```go\npackage myextension\n\nimport (\n    \"fmt\"\n    \"github.com/dunglas/frankenphp\"\n    frankenphpCaddy \"github.com/dunglas/frankenphp/caddy\"\n)\n\nfunc init() {\n    workerHandle = frankenphpCaddy.RegisterWorkers(\n        \"my-worker\", \"worker.php\", 2,\n\n        // Inicio del Servidor (Global)\n        frankenphp.WithWorkerOnServerStartup(func() {\n            fmt.Println(\"Extension: Servidor iniciando...\")\n        }),\n\n        // Hilo Listo (Por Hilo)\n        // Nota: La función acepta un entero que representa el ID del hilo\n        frankenphp.WithWorkerOnReady(func(id int) {\n            fmt.Printf(\"Extension: Hilo worker #%d está listo.\\n\", id)\n        }),\n    )\n}\n```\n"
  },
  {
    "path": "docs/es/extensions.md",
    "content": "# Escribir Extensiones PHP en Go\n\nCon FrankenPHP, puedes **escribir extensiones PHP en Go**, lo que te permite crear **funciones nativas de alto rendimiento** que pueden ser llamadas directamente desde PHP. Tus aplicaciones pueden aprovechar cualquier biblioteca Go existente o nueva, así como el famoso modelo de concurrencia de **goroutines directamente desde tu código PHP**.\n\nEscribir extensiones PHP típicamente se hace en C, pero también es posible escribirlas en otros lenguajes con un poco de trabajo adicional. Las extensiones PHP te permiten aprovechar el poder de lenguajes de bajo nivel para extender las funcionalidades de PHP, por ejemplo, añadiendo funciones nativas o optimizando operaciones específicas.\n\nGracias a los módulos de Caddy, puedes escribir extensiones PHP en Go e integrarlas muy rápidamente en FrankenPHP.\n\n## Dos Enfoques\n\nFrankenPHP proporciona dos formas de crear extensiones PHP en Go:\n\n1. **Usando el Generador de Extensiones** - El enfoque recomendado que genera todo el código repetitivo necesario para la mayoría de los casos de uso, permitiéndote enfocarte en escribir tu código Go.\n2. **Implementación Manual** - Control total sobre la estructura de la extensión para casos de uso avanzados.\n\nComenzaremos con el enfoque del generador ya que es la forma más fácil de empezar, luego mostraremos la implementación manual para aquellos que necesitan un control completo.\n\n## Usando el Generador de Extensiones\n\nFrankenPHP incluye una herramienta que te permite **crear una extensión PHP** usando solo Go. **No necesitas escribir código C** ni usar CGO directamente: FrankenPHP también incluye una **API de tipos públicos** para ayudarte a escribir tus extensiones en Go sin tener que preocuparte por **la manipulación de tipos entre PHP/C y Go**.\n\n> [!TIP]\n> Si quieres entender cómo se pueden escribir extensiones en Go desde cero, puedes leer la sección de implementación manual a continuación que demuestra cómo escribir una extensión PHP en Go sin usar el generador.\n\nTen en cuenta que esta herramienta **no es un generador de extensiones completo**. Está diseñada para ayudarte a escribir extensiones simples en Go, pero no proporciona las características más avanzadas de las extensiones PHP. Si necesitas escribir una extensión más **compleja y optimizada**, es posible que necesites escribir algo de código C o usar CGO directamente.\n\n### Requisitos Previos\n\nComo se cubre en la sección de implementación manual a continuación, necesitas [obtener las fuentes de PHP](https://www.php.net/downloads.php) y crear un nuevo módulo Go.\n\n#### Crear un Nuevo Módulo y Obtener las Fuentes de PHP\n\nEl primer paso para escribir una extensión PHP en Go es crear un nuevo módulo Go. Puedes usar el siguiente comando para esto:\n\n```console\ngo mod init ejemplo.com/ejemplo\n```\n\nEl segundo paso es [obtener las fuentes de PHP](https://www.php.net/downloads.php) para los siguientes pasos. Una vez que las tengas, descomprímelas en el directorio de tu elección, no dentro de tu módulo Go:\n\n```console\ntar xf php-*\n```\n\n### Escribiendo la Extensión\n\nTodo está listo para escribir tu función nativa en Go. Crea un nuevo archivo llamado `stringext.go`. Nuestra primera función tomará una cadena como argumento, el número de veces para repetirla, un booleano para indicar si invertir la cadena, y devolverá la cadena resultante. Esto debería verse así:\n\n```go\npackage ejemplo\n\n// #include <Zend/zend_types.h>\nimport \"C\"\nimport (\n    \"strings\"\n\t\"unsafe\"\n\n\t\"github.com/dunglas/frankenphp\"\n)\n\n//export_php:function repeat_this(string $str, int $count, bool $reverse): string\nfunc repeat_this(s *C.zend_string, count int64, reverse bool) unsafe.Pointer {\n    str := frankenphp.GoString(unsafe.Pointer(s))\n\n    result := strings.Repeat(str, int(count))\n    if reverse {\n        runes := []rune(result)\n        for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {\n            runes[i], runes[j] = runes[j], runes[i]\n        }\n        result = string(runes)\n    }\n\n    return frankenphp.PHPString(result, false)\n}\n```\n\nHay dos cosas importantes a tener en cuenta aquí:\n\n- Un comentario de directiva `//export_php:function` define la firma de la función en PHP. Así es como el generador sabe cómo generar la función PHP con los parámetros y tipo de retorno correctos;\n- La función debe devolver un `unsafe.Pointer`. FrankenPHP proporciona una API para ayudarte con la manipulación de tipos entre C y Go.\n\nMientras que el primer punto se explica por sí mismo, el segundo puede ser más difícil de entender. Profundicemos en la manipulación de tipos en la siguiente sección.\n\n### Manipulación de Tipos\n\nAunque algunos tipos de variables tienen la misma representación en memoria entre C/PHP y Go, algunos tipos requieren más lógica para ser usados directamente. Esta es quizá la parte más difícil cuando se trata de escribir extensiones porque requiere entender los internos del motor Zend y cómo se almacenan las variables internamente en PHP.\nEsta tabla resume lo que necesitas saber:\n\n| Tipo PHP            | Tipo Go                        | Conversión directa | Helper de C a Go                     | Helper de Go a C                      | Soporte para Métodos de Clase |\n|---------------------|--------------------------------|---------------------|---------------------------------------|----------------------------------------|-------------------------------|\n| `int`               | `int64`                        | ✅                   | -                                     | -                                      | ✅                             |\n| `?int`              | `*int64`                       | ✅                   | -                                     | -                                      | ✅                             |\n| `float`             | `float64`                      | ✅                   | -                                     | -                                      | ✅                             |\n| `?float`            | `*float64`                     | ✅                   | -                                     | -                                      | ✅                             |\n| `bool`              | `bool`                         | ✅                   | -                                     | -                                      | ✅                             |\n| `?bool`             | `*bool`                        | ✅                   | -                                     | -                                      | ✅                             |\n| `string`/`?string` | `*C.zend_string`              | ❌                   | `frankenphp.GoString()`              | `frankenphp.PHPString()`              | ✅                             |\n| `array`             | `frankenphp.AssociativeArray`  | ❌                   | `frankenphp.GoAssociativeArray()`    | `frankenphp.PHPAssociativeArray()`    | ✅                             |\n| `array`             | `map[string]any`               | ❌                   | `frankenphp.GoMap()`                 | `frankenphp.PHPMap()`                 | ✅                             |\n| `array`             | `[]any`                        | ❌                   | `frankenphp.GoPackedArray()`         | `frankenphp.PHPPackedArray()`         | ✅                             |\n| `mixed`             | `any`                          | ❌                   | `GoValue()`                          | `PHPValue()`                          | ❌                             |\n| `callable`          | `*C.zval`                      | ❌                   | -                                     | frankenphp.CallPHPCallable()          | ❌                             |\n| `object`            | `struct`                       | ❌                   | _Aún no implementado_                | _Aún no implementado_                 | ❌                             |\n\n> [!NOTE]\n>\n> Esta tabla aún no es exhaustiva y se completará a medida que la API de tipos de FrankenPHP se vuelva más completa.\n>\n> Para métodos de clase específicamente, los tipos primitivos y los arrays están actualmente soportados. Los objetos aún no pueden usarse como parámetros de métodos o tipos de retorno.\n\nSi te refieres al fragmento de código de la sección anterior, puedes ver que se usan helpers para convertir el primer parámetro y el valor de retorno. El segundo y tercer parámetro de nuestra función `repeat_this()` no necesitan ser convertidos ya que la representación en memoria de los tipos subyacentes es la misma para C y Go.\n\n#### Trabajando con Arrays\n\nFrankenPHP proporciona soporte nativo para arrays PHP a través de `frankenphp.AssociativeArray` o conversión directa a un mapa o slice.\n\n`AssociativeArray` representa un [mapa hash](https://es.wikipedia.org/wiki/Tabla_hash) compuesto por un campo `Map: map[string]any` y un campo opcional `Order: []string` (a diferencia de los \"arrays asociativos\" de PHP, los mapas de Go no están ordenados).\n\nSi no se necesita orden o asociación, también es posible convertir directamente a un slice `[]any` o un mapa no ordenado `map[string]any`.\n\n**Creando y manipulando arrays en Go:**\n\n```go\npackage ejemplo\n\n// #include <Zend/zend_types.h>\nimport \"C\"\nimport (\n    \"unsafe\"\n\n    \"github.com/dunglas/frankenphp\"\n)\n\n// export_php:function process_data_ordered(array $input): array\nfunc process_data_ordered_map(arr *C.zend_array) unsafe.Pointer {\n\t// Convertir array asociativo PHP a Go manteniendo el orden\n\tassociativeArray, err := frankenphp.GoAssociativeArray[any](unsafe.Pointer(arr))\n    if err != nil {\n        // manejar error\n    }\n\n\t// iterar sobre las entradas en orden\n\tfor _, key := range associativeArray.Order {\n\t\tvalue, _ = associativeArray.Map[key]\n\t\t// hacer algo con key y value\n\t}\n\n\t// devolver un array ordenado\n\t// si 'Order' no está vacío, solo se respetarán los pares clave-valor en 'Order'\n\treturn frankenphp.PHPAssociativeArray[string](frankenphp.AssociativeArray[string]{\n\t\tMap: map[string]string{\n\t\t\t\"clave1\": \"valor1\",\n\t\t\t\"clave2\": \"valor2\",\n\t\t},\n\t\tOrder: []string{\"clave1\", \"clave2\"},\n\t})\n}\n\n// export_php:function process_data_unordered(array $input): array\nfunc process_data_unordered_map(arr *C.zend_array) unsafe.Pointer {\n\t// Convertir array asociativo PHP a un mapa Go sin mantener el orden\n\t// ignorar el orden será más eficiente\n\tgoMap, err := frankenphp.GoMap[any](unsafe.Pointer(arr))\n    if err != nil {\n        // manejar error\n    }\n\n\t// iterar sobre las entradas sin un orden específico\n\tfor key, value := range goMap {\n\t\t// hacer algo con key y value\n\t}\n\n\t// devolver un array no ordenado\n\treturn frankenphp.PHPMap(map[string]string {\n\t\t\"clave1\": \"valor1\",\n\t\t\"clave2\": \"valor2\",\n\t})\n}\n\n// export_php:function process_data_packed(array $input): array\nfunc process_data_packed(arr *C.zend_array) unsafe.Pointer {\n\t// Convertir array empaquetado PHP a Go\n\tgoSlice, err := frankenphp.GoPackedArray(unsafe.Pointer(arr))\n    if err != nil {\n        // manejar error\n    }\n\n\t// iterar sobre el slice en orden\n\tfor index, value := range goSlice {\n\t\t// hacer algo con index y value\n\t}\n\n\t// devolver un array empaquetado\n\treturn frankenphp.PHPPackedArray([]string{\"valor1\", \"valor2\", \"valor3\"})\n}\n```\n\n**Características clave de la conversión de arrays:**\n\n- **Pares clave-valor ordenados** - Opción para mantener el orden del array asociativo\n- **Optimizado para múltiples casos** - Opción para prescindir del orden para un mejor rendimiento o convertir directamente a un slice\n- **Detección automática de listas** - Al convertir a PHP, detecta automáticamente si el array debe ser una lista empaquetada o un mapa hash\n- **Arrays Anidados** - Los arrays pueden estar anidados y convertirán automáticamente todos los tipos soportados (`int64`, `float64`, `string`, `bool`, `nil`, `AssociativeArray`, `map[string]any`, `[]any`)\n- **Objetos no soportados** - Actualmente, solo se pueden usar tipos escalares y arrays como valores. Proporcionar un objeto resultará en un valor `null` en el array PHP.\n\n##### Métodos Disponibles: Empaquetados y Asociativos\n\n- `frankenphp.PHPAssociativeArray(arr frankenphp.AssociativeArray) unsafe.Pointer` - Convertir a un array PHP ordenado con pares clave-valor\n- `frankenphp.PHPMap(arr map[string]any) unsafe.Pointer` - Convertir un mapa a un array PHP no ordenado con pares clave-valor\n- `frankenphp.PHPPackedArray(slice []any) unsafe.Pointer` - Convertir un slice a un array PHP empaquetado con solo valores indexados\n- `frankenphp.GoAssociativeArray(arr unsafe.Pointer, ordered bool) frankenphp.AssociativeArray` - Convertir un array PHP a un `AssociativeArray` de Go ordenado (mapa con orden)\n- `frankenphp.GoMap(arr unsafe.Pointer) map[string]any` - Convertir un array PHP a un mapa Go no ordenado\n- `frankenphp.GoPackedArray(arr unsafe.Pointer) []any` - Convertir un array PHP a un slice Go\n- `frankenphp.IsPacked(zval *C.zend_array) bool` - Verificar si un array PHP está empaquetado (solo indexado) o es asociativo (pares clave-valor)\n\n### Trabajando con Callables\n\nFrankenPHP proporciona una forma de trabajar con callables de PHP usando el helper `frankenphp.CallPHPCallable`. Esto te permite llamar a funciones o métodos de PHP desde código Go.\n\nPara mostrar esto, creemos nuestra propia función `array_map()` que toma un callable y un array, aplica el callable a cada elemento del array, y devuelve un nuevo array con los resultados:\n\n```go\n// export_php:function my_array_map(array $data, callable $callback): array\nfunc my_array_map(arr *C.zend_array, callback *C.zval) unsafe.Pointer {\n\tgoSlice, err := frankenphp.GoPackedArray[any](unsafe.Pointer(arr))\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tresult := make([]any, len(goSlice))\n\n\tfor index, value := range goSlice {\n\t\tresult[index] = frankenphp.CallPHPCallable(unsafe.Pointer(callback), []interface{}{value})\n\t}\n\n\treturn frankenphp.PHPPackedArray(result)\n}\n```\n\nObserva cómo usamos `frankenphp.CallPHPCallable()` para llamar al callable de PHP pasado como parámetro. Esta función toma un puntero al callable y un array de argumentos, y devuelve el resultado de la ejecución del callable. Puedes usar la sintaxis de callable a la que estás acostumbrado:\n\n```php\n<?php\n\n$result = my_array_map([1, 2, 3], function($x) { return $x * 2; });\n// $result será [2, 4, 6]\n\n$result = my_array_map(['hola', 'mundo'], 'strtoupper');\n// $result será ['HOLA', 'MUNDO']\n```\n\n### Declarando una Clase Nativa de PHP\n\nEl generador soporta la declaración de **clases opacas** como estructuras Go, que pueden usarse para crear objetos PHP. Puedes usar el comentario de directiva `//export_php:class` para definir una clase PHP. Por ejemplo:\n\n```go\npackage ejemplo\n\n//export_php:class User\ntype UserStruct struct {\n    Name string\n    Age  int\n}\n```\n\n#### ¿Qué son las Clases Opaque?\n\nLas **clases opacas** son clases donde la estructura interna (propiedades) está oculta del código PHP. Esto significa:\n\n- **Sin acceso directo a propiedades**: No puedes leer o escribir propiedades directamente desde PHP (`$user->name` no funcionará)\n- **Interfaz solo de métodos** - Todas las interacciones deben pasar a través de los métodos que defines\n- **Mejor encapsulación** - La estructura de datos interna está completamente controlada por el código Go\n- **Seguridad de tipos** - Sin riesgo de que el código PHP corrompa el estado interno con tipos incorrectos\n- **API más limpia** - Obliga a diseñar una interfaz pública adecuada\n\nEste enfoque proporciona una mejor encapsulación y evita que el código PHP corrompa accidentalmente el estado interno de tus objetos Go. Todas las interacciones con el objeto deben pasar a través de los métodos que defines explícitamente.\n\n#### Añadiendo Métodos a las Clases\n\nDado que las propiedades no son directamente accesibles, **debes definir métodos** para interactuar con tus clases opacas. Usa la directiva `//export_php:method` para definir el comportamiento:\n\n```go\npackage ejemplo\n\n// #include <Zend/zend_types.h>\nimport \"C\"\nimport (\n    \"unsafe\"\n\n    \"github.com/dunglas/frankenphp\"\n)\n\n//export_php:class User\ntype UserStruct struct {\n    Name string\n    Age  int\n}\n\n//export_php:method User::getName(): string\nfunc (us *UserStruct) GetUserName() unsafe.Pointer {\n    return frankenphp.PHPString(us.Name, false)\n}\n\n//export_php:method User::setAge(int $age): void\nfunc (us *UserStruct) SetUserAge(age int64) {\n    us.Age = int(age)\n}\n\n//export_php:method User::getAge(): int\nfunc (us *UserStruct) GetUserAge() int64 {\n    return int64(us.Age)\n}\n\n//export_php:method User::setNamePrefix(string $prefix = \"User\"): void\nfunc (us *UserStruct) SetNamePrefix(prefix *C.zend_string) {\n    us.Name = frankenphp.GoString(unsafe.Pointer(prefix)) + \": \" + us.Name\n}\n```\n\n#### Parámetros Nulos\n\nEl generador soporta parámetros nulos usando el prefijo `?` en las firmas de PHP. Cuando un parámetro es nulo, se convierte en un puntero en tu función Go, permitiéndote verificar si el valor era `null` en PHP:\n\n```go\npackage ejemplo\n\n// #include <Zend/zend_types.h>\nimport \"C\"\nimport (\n\t\"unsafe\"\n\n\t\"github.com/dunglas/frankenphp\"\n)\n\n//export_php:method User::updateInfo(?string $name, ?int $age, ?bool $active): void\nfunc (us *UserStruct) UpdateInfo(name *C.zend_string, age *int64, active *bool) {\n    // Verificar si se proporcionó name (no es null)\n    if name != nil {\n        us.Name = frankenphp.GoString(unsafe.Pointer(name))\n    }\n\n    // Verificar si se proporcionó age (no es null)\n    if age != nil {\n        us.Age = int(*age)\n    }\n\n    // Verificar si se proporcionó active (no es null)\n    if active != nil {\n        us.Active = *active\n    }\n}\n```\n\n**Puntos clave sobre parámetros nulos:**\n\n- **Tipos primitivos nulos** (`?int`, `?float`, `?bool`) se convierten en punteros (`*int64`, `*float64`, `*bool`) en Go\n- **Strings nulos** (`?string`) permanecen como `*C.zend_string` pero pueden ser `nil`\n- **Verificar `nil`** antes de desreferenciar valores de puntero\n- **`null` de PHP se convierte en `nil` de Go** - cuando PHP pasa `null`, tu función Go recibe un puntero `nil`\n\n> [!CAUTION]\n>\n> Actualmente, los métodos de clase tienen las siguientes limitaciones. **Los objetos no están soportados** como tipos de parámetro o tipos de retorno. **Los arrays están completamente soportados** para ambos parámetros y tipos de retorno. Tipos soportados: `string`, `int`, `float`, `bool`, `array`, y `void` (para tipo de retorno). **Los tipos de parámetros nulos están completamente soportados** para todos los tipos escalares (`?string`, `?int`, `?float`, `?bool`).\n\nDespués de generar la extensión, podrás usar la clase y sus métodos en PHP. Ten en cuenta que **no puedes acceder a las propiedades directamente**:\n\n```php\n<?php\n\n$user = new User();\n\n// ✅ Esto funciona - usando métodos\n$user->setAge(25);\necho $user->getName();           // Salida: (vacío, valor por defecto)\necho $user->getAge();            // Salida: 25\n$user->setNamePrefix(\"Empleado\");\n\n// ✅ Esto también funciona - parámetros nulos\n$user->updateInfo(\"John\", 30, true);        // Todos los parámetros proporcionados\n$user->updateInfo(\"Jane\", null, false);     // Age es null\n$user->updateInfo(null, 25, null);          // Name y active son null\n\n// ❌ Esto NO funcionará - acceso directo a propiedades\n// echo $user->name;             // Error: No se puede acceder a la propiedad privada\n// $user->age = 30;              // Error: No se puede acceder a la propiedad privada\n```\n\nEste diseño asegura que tu código Go tenga control completo sobre cómo se accede y modifica el estado del objeto, proporcionando una mejor encapsulación y seguridad de tipos.\n\n### Declarando Constantes\n\nEl generador soporta exportar constantes Go a PHP usando dos directivas: `//export_php:const` para constantes globales y `//export_php:classconst` para constantes de clase. Esto te permite compartir valores de configuración, códigos de estado y otras constantes entre código Go y PHP.\n\n#### Constantes Globales\n\nUsa la directiva `//export_php:const` para crear constantes globales de PHP:\n\n```go\npackage ejemplo\n\n//export_php:const\nconst MAX_CONNECTIONS = 100\n\n//export_php:const\nconst API_VERSION = \"1.2.3\"\n\n//export_php:const\nconst STATUS_OK = iota\n\n//export_php:const\nconst STATUS_ERROR = iota\n```\n\n#### Constantes de Clase\n\nUsa la directiva `//export_php:classconst ClassName` para crear constantes que pertenecen a una clase PHP específica:\n\n```go\npackage ejemplo\n\n//export_php:classconst User\nconst STATUS_ACTIVE = 1\n\n//export_php:classconst User\nconst STATUS_INACTIVE = 0\n\n//export_php:classconst User\nconst ROLE_ADMIN = \"admin\"\n\n//export_php:classconst Order\nconst STATE_PENDING = iota\n\n//export_php:classconst Order\nconst STATE_PROCESSING = iota\n\n//export_php:classconst Order\nconst STATE_COMPLETED = iota\n```\n\nLas constantes de clase son accesibles usando el ámbito del nombre de clase en PHP:\n\n```php\n<?php\n\n// Constantes globales\necho MAX_CONNECTIONS;    // 100\necho API_VERSION;        // \"1.2.3\"\n\n// Constantes de clase\necho User::STATUS_ACTIVE;    // 1\necho User::ROLE_ADMIN;       // \"admin\"\necho Order::STATE_PENDING;   // 0\n```\n\nLa directiva soporta varios tipos de valores incluyendo strings, enteros, booleanos, floats y constantes iota. Cuando se usa `iota`, el generador asigna automáticamente valores secuenciales (0, 1, 2, etc.). Las constantes globales se vuelven disponibles en tu código PHP como constantes globales, mientras que las constantes de clase tienen alcance a sus respectivas clases usando visibilidad pública. Cuando se usan enteros, se soportan diferentes notaciones posibles (binario, hexadecimal, octal) y se vuelcan tal cual en el archivo stub de PHP.\n\nPuedes usar constantes tal como estás acostumbrado en el código Go. Por ejemplo, tomemos la función `repeat_this()` que declaramos anteriormente y cambiemos el último argumento a un entero:\n\n```go\npackage ejemplo\n\n// #include <Zend/zend_types.h>\nimport \"C\"\nimport (\n\t\"strings\"\n\t\"unsafe\"\n\n\t\"github.com/dunglas/frankenphp\"\n)\n\n//export_php:const\nconst STR_REVERSE = iota\n\n//export_php:const\nconst STR_NORMAL = iota\n\n//export_php:classconst StringProcessor\nconst MODE_LOWERCASE = 1\n\n//export_php:classconst StringProcessor\nconst MODE_UPPERCASE = 2\n\n//export_php:function repeat_this(string $str, int $count, int $mode): string\nfunc repeat_this(s *C.zend_string, count int64, mode int) unsafe.Pointer {\n\tstr := frankenphp.GoString(unsafe.Pointer(s))\n\n\tresult := strings.Repeat(str, int(count))\n\tif mode == STR_REVERSE {\n\t\t// invertir la cadena\n\t}\n\n\tif mode == STR_NORMAL {\n\t\t// no hacer nada, solo para mostrar la constante\n\t}\n\n\treturn frankenphp.PHPString(result, false)\n}\n\n//export_php:class StringProcessor\ntype StringProcessorStruct struct {\n\t// campos internos\n}\n\n//export_php:method StringProcessor::process(string $input, int $mode): string\nfunc (sp *StringProcessorStruct) Process(input *C.zend_string, mode int64) unsafe.Pointer {\n\tstr := frankenphp.GoString(unsafe.Pointer(input))\n\n\tswitch mode {\n\tcase MODE_LOWERCASE:\n\t\tstr = strings.ToLower(str)\n\tcase MODE_UPPERCASE:\n\t\tstr = strings.ToUpper(str)\n\t}\n\n\treturn frankenphp.PHPString(str, false)\n}\n```\n\n### Usando Espacios de Nombres\n\nEl generador soporta organizar las funciones, clases y constantes de tu extensión PHP bajo un espacio de nombres usando la directiva `//export_php:namespace`. Esto ayuda a evitar conflictos de nombres y proporciona una mejor organización para la API de tu extensión.\n\n#### Declarando un Espacio de Nombres\n\nUsa la directiva `//export_php:namespace` al inicio de tu archivo Go para colocar todos los símbolos exportados bajo un espacio de nombres específico:\n\n```go\n//export_php:namespace Mi\\Extensión\npackage ejemplo\n\nimport (\n    \"unsafe\"\n\n    \"github.com/dunglas/frankenphp\"\n)\n\n//export_php:function hello(): string\nfunc hello() string {\n    return \"Hola desde el espacio de nombres Mi\\\\Extensión!\"\n}\n\n//export_php:class User\ntype UserStruct struct {\n    // campos internos\n}\n\n//export_php:method User::getName(): string\nfunc (u *UserStruct) GetName() unsafe.Pointer {\n    return frankenphp.PHPString(\"John Doe\", false)\n}\n\n//export_php:const\nconst STATUS_ACTIVE = 1\n```\n\n#### Usando la Extensión con Espacio de Nombres en PHP\n\nCuando se declara un espacio de nombres, todas las funciones, clases y constantes se colocan bajo ese espacio de nombres en PHP:\n\n```php\n<?php\n\necho Mi\\Extensión\\hello(); // \"Hola desde el espacio de nombres Mi\\Extensión!\"\n\n$user = new Mi\\Extensión\\User();\necho $user->getName(); // \"John Doe\"\n\necho Mi\\Extensión\\STATUS_ACTIVE; // 1\n```\n\n#### Notas Importantes\n\n- Solo se permite **una** directiva de espacio de nombres por archivo. Si se encuentran múltiples directivas de espacio de nombres, el generador devolverá un error.\n- El espacio de nombres se aplica a **todos** los símbolos exportados en el archivo: funciones, clases, métodos y constantes.\n- Los nombres de espacios de nombres siguen las convenciones de espacios de nombres de PHP usando barras invertidas (`\\`) como separadores.\n- Si no se declara un espacio de nombres, los símbolos se exportan al espacio de nombres global como de costumbre.\n\n### Generando la Extensión\n\nAquí es donde ocurre la magia, y tu extensión ahora puede ser generada. Puedes ejecutar el generador con el siguiente comando:\n\n```console\nGEN_STUB_SCRIPT=php-src/build/gen_stub.php frankenphp extension-init mi_extensión.go\n```\n\n> [!NOTE]\n> No olvides establecer la variable de entorno `GEN_STUB_SCRIPT` a la ruta del archivo `gen_stub.php` en las fuentes de PHP que descargaste anteriormente. Este es el mismo script `gen_stub.php` mencionado en la sección de implementación manual.\n\nSi todo salió bien, se debería haber creado un nuevo directorio llamado `build`. Este directorio contiene los archivos generados para tu extensión, incluyendo el archivo `mi_extensión.go` con los stubs de funciones PHP generadas.\n\n### Integrando la Extensión Generada en FrankenPHP\n\nNuestra extensión ahora está lista para ser compilada e integrada en FrankenPHP. Para hacerlo, consulta la documentación de [compilación de FrankenPHP](compile.md) para aprender cómo compilar FrankenPHP. Agrega el módulo usando la bandera `--with`, apuntando a la ruta de tu módulo:\n\n```console\nCGO_ENABLED=1 \\\nXCADDY_GO_BUILD_FLAGS=\"-ldflags='-w -s' -tags=nobadger,nomysql,nopgx\" \\\nCGO_CFLAGS=$(php-config --includes) \\\nCGO_LDFLAGS=\"$(php-config --ldflags) $(php-config --libs)\" \\\nxcaddy build \\\n    --output frankenphp \\\n    --with github.com/mi-cuenta/mi-módulo/build\n```\n\nTen en cuenta que apuntas al subdirectorio `/build` que se creó durante el paso de generación. Sin embargo, esto no es obligatorio: también puedes copiar los archivos generados a tu directorio de módulo y apuntar a él directamente.\n\n### Probando tu Extensión Generada\n\nPuedes crear un archivo PHP para probar las funciones y clases que has creado. Por ejemplo, crea un archivo `index.php` con el siguiente contenido:\n\n```php\n<?php\n\n// Usando constantes globales\nvar_dump(repeat_this('Hola Mundo', 5, STR_REVERSE));\n\n// Usando constantes de clase\n$processor = new StringProcessor();\necho $processor->process('Hola Mundo', StringProcessor::MODE_LOWERCASE);  // \"hola mundo\"\necho $processor->process('Hola Mundo', StringProcessor::MODE_UPPERCASE);  // \"HOLA MUNDO\"\n```\n\nUna vez que hayas integrado tu extensión en FrankenPHP como se demostró en la sección anterior, puedes ejecutar este archivo de prueba usando `./frankenphp php-server`, y deberías ver tu extensión funcionando.\n\n## Implementación Manual\n\nSi quieres entender cómo funcionan las extensiones o necesitas un control total sobre tu extensión, puedes escribirlas manualmente. Este enfoque te da control completo pero requiere más código repetitivo.\n\n### Función Básica\n\nVeremos cómo escribir una extensión PHP simple en Go que define una nueva función nativa. Esta función será llamada desde PHP y desencadenará una goroutine que registra un mensaje en los logs de Caddy. Esta función no toma ningún parámetro y no devuelve nada.\n\n#### Definir la Función Go\n\nEn tu módulo, necesitas definir una nueva función nativa que será llamada desde PHP. Para esto, crea un archivo con el nombre que desees, por ejemplo, `extension.go`, y agrega el siguiente código:\n\n```go\npackage ejemplo\n\n// #include \"extension.h\"\nimport \"C\"\nimport (\n\t\"log/slog\"\n\t\"unsafe\"\n\n\t\"github.com/dunglas/frankenphp\"\n)\n\nfunc init() {\n\tfrankenphp.RegisterExtension(unsafe.Pointer(&C.ext_module_entry))\n}\n\n//export go_print_something\nfunc go_print_something() {\n\tgo func() {\n\t\tslog.Info(\"¡Hola desde una goroutine!\")\n\t}()\n}\n```\n\nLa función `frankenphp.RegisterExtension()` simplifica el proceso de registro de la extensión manejando la lógica interna de registro de PHP. La función `go_print_something` usa la directiva `//export` para indicar que será accesible en el código C que escribiremos, gracias a CGO.\n\nEn este ejemplo, nuestra nueva función desencadenará una goroutine que registra un mensaje en los logs de Caddy.\n\n#### Definir la Función PHP\n\nPara permitir que PHP llame a nuestra función, necesitamos definir una función PHP correspondiente. Para esto, crearemos un archivo stub, por ejemplo, `extension.stub.php`, que contendrá el siguiente código:\n\n```php\n<?php\n\n/** @generate-class-entries */\n\nfunction go_print(): void {}\n```\n\nEste archivo define la firma de la función `go_print()`, que será llamada desde PHP. La directiva `@generate-class-entries` permite a PHP generar automáticamente entradas de funciones para nuestra extensión.\n\nEsto no se hace manualmente, sino usando un script proporcionado en las fuentes de PHP (asegúrate de ajustar la ruta al script `gen_stub.php` según dónde se encuentren tus fuentes de PHP):\n\n```bash\nphp ../php-src/build/gen_stub.php extension.stub.php\n```\n\nEste script generará un archivo llamado `extension_arginfo.h` que contiene la información necesaria para que PHP sepa cómo definir y llamar a nuestra función.\n\n#### Escribir el Puente entre Go y C\n\nAhora, necesitamos escribir el puente entre Go y C. Crea un archivo llamado `extension.h` en el directorio de tu módulo con el siguiente contenido:\n\n```c\n#ifndef _EXTENSION_H\n#define _EXTENSION_H\n\n#include <php.h>\n\nextern zend_module_entry ext_module_entry;\n\n#endif\n```\n\nA continuación, crea un archivo llamado `extension.c` que realizará los siguientes pasos:\n\n- Incluir los encabezados de PHP;\n- Declarar nuestra nueva función nativa de PHP `go_print()`;\n- Declarar los metadatos de la extensión.\n\nComencemos incluyendo los encabezados requeridos:\n\n```c\n#include <php.h>\n#include \"extension.h\"\n#include \"extension_arginfo.h\"\n\n// Contiene símbolos exportados por Go\n#include \"_cgo_export.h\"\n```\n\nLuego definimos nuestra función PHP como una función de lenguaje nativo:\n\n```c\nPHP_FUNCTION(go_print)\n{\n    ZEND_PARSE_PARAMETERS_NONE();\n\n    go_print_something();\n}\n\nzend_module_entry ext_module_entry = {\n    STANDARD_MODULE_HEADER,\n    \"ext_go\",\n    ext_functions, /* Funciones */\n    NULL,          /* MINIT */\n    NULL,          /* MSHUTDOWN */\n    NULL,          /* RINIT */\n    NULL,          /* RSHUTDOWN */\n    NULL,          /* MINFO */\n    \"0.1.1\",\n    STANDARD_MODULE_PROPERTIES\n};\n```\n\nEn este caso, nuestra función no toma parámetros y no devuelve nada. Simplemente llama a la función Go que definimos anteriormente, exportada usando la directiva `//export`.\n\nFinalmente, definimos los metadatos de la extensión en una estructura `zend_module_entry`, como su nombre, versión y propiedades. Esta información es necesaria para que PHP reconozca y cargue nuestra extensión. Ten en cuenta que `ext_functions` es un array de punteros a las funciones PHP que definimos, y fue generado automáticamente por el script `gen_stub.php` en el archivo `extension_arginfo.h`.\n\nEl registro de la extensión es manejado automáticamente por la función `RegisterExtension()` de FrankenPHP que llamamos en nuestro código Go.\n\n### Uso Avanzado\n\nAhora que sabemos cómo crear una extensión PHP básica en Go, compliquemos nuestro ejemplo. Ahora crearemos una función PHP que tome una cadena como parámetro y devuelva su versión en mayúsculas.\n\n#### Definir el Stub de la Función PHP\n\nPara definir la nueva función PHP, modificaremos nuestro archivo `extension.stub.php` para incluir la nueva firma de la función:\n\n```php\n<?php\n\n/** @generate-class-entries */\n\n/**\n * Convierte una cadena a mayúsculas.\n *\n * @param string $string La cadena a convertir.\n * @return string La versión en mayúsculas de la cadena.\n */\nfunction go_upper(string $string): string {}\n```\n\n> [!TIP]\n> ¡No descuides la documentación de tus funciones! Es probable que compartas tus stubs de extensión con otros desarrolladores para documentar cómo usar tu extensión y qué características están disponibles.\n\nAl regenerar el archivo stub con el script `gen_stub.php`, el archivo `extension_arginfo.h` debería verse así:\n\n```c\nZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_go_upper, 0, 1, IS_STRING, 0)\n    ZEND_ARG_TYPE_INFO(0, string, IS_STRING, 0)\nZEND_END_ARG_INFO()\n\nZEND_FUNCTION(go_upper);\n\nstatic const zend_function_entry ext_functions[] = {\n    ZEND_FE(go_upper, arginfo_go_upper)\n    ZEND_FE_END\n};\n```\n\nPodemos ver que la función `go_upper` está definida con un parámetro de tipo `string` y un tipo de retorno `string`.\n\n#### Manipulación de Tipos entre Go y PHP/C\n\nTu función Go no puede aceptar directamente una cadena PHP como parámetro. Necesitas convertirla a una cadena Go. Afortunadamente, FrankenPHP proporciona funciones helper para manejar la conversión entre cadenas PHP y cadenas Go, similar a lo que vimos en el enfoque del generador.\n\nEl archivo de encabezado sigue siendo simple:\n\n```c\n#ifndef _EXTENSION_H\n#define _EXTENSION_H\n\n#include <php.h>\n\nextern zend_module_entry ext_module_entry;\n\n#endif\n```\n\nAhora podemos escribir el puente entre Go y C en nuestro archivo `extension.c`. Pasaremos la cadena PHP directamente a nuestra función Go:\n\n```c\nPHP_FUNCTION(go_upper)\n{\n    zend_string *str;\n\n    ZEND_PARSE_PARAMETERS_START(1, 1)\n        Z_PARAM_STR(str)\n    ZEND_PARSE_PARAMETERS_END();\n\n    zend_string *result = go_upper(str);\n    RETVAL_STR(result);\n}\n```\n\nPuedes aprender más sobre `ZEND_PARSE_PARAMETERS_START` y el análisis de parámetros en la página dedicada del [Libro de Internals de PHP](https://www.phpinternalsbook.com/php7/extensions_design/php_functions.html#parsing-parameters-zend-parse-parameters). Aquí, le decimos a PHP que nuestra función toma un parámetro obligatorio de tipo `string` como `zend_string`. Luego pasamos esta cadena directamente a nuestra función Go y devolvemos el resultado usando `RETVAL_STR`.\n\nSolo queda una cosa por hacer: implementar la función `go_upper` en Go.\n\n#### Implementar la Función Go\n\nNuestra función Go tomará un `*C.zend_string` como parámetro, lo convertirá a una cadena Go usando la función helper de FrankenPHP, lo procesará y devolverá el resultado como un nuevo `*C.zend_string`. Las funciones helper manejan toda la complejidad de gestión de memoria y conversión por nosotros.\n\n```go\npackage ejemplo\n\n// #include <Zend/zend_types.h>\nimport \"C\"\nimport (\n    \"unsafe\"\n    \"strings\"\n\n    \"github.com/dunglas/frankenphp\"\n)\n\n//export go_upper\nfunc go_upper(s *C.zend_string) *C.zend_string {\n    str := frankenphp.GoString(unsafe.Pointer(s))\n\n    upper := strings.ToUpper(str)\n\n    return (*C.zend_string)(frankenphp.PHPString(upper, false))\n}\n```\n\nEste enfoque es mucho más limpio y seguro que la gestión manual de memoria.\nLas funciones helper de FrankenPHP manejan la conversión entre el formato `zend_string` de PHP y las cadenas Go automáticamente.\nEl parámetro `false` en `PHPString()` indica que queremos crear una nueva cadena no persistente (liberada al final de la solicitud).\n\n> [!TIP]\n>\n> En este ejemplo, no realizamos ningún manejo de errores, pero siempre debes verificar que los punteros no sean `nil` y que los datos sean válidos antes de usarlos en tus funciones Go.\n\n### Integrando la Extensión en FrankenPHP\n\nNuestra extensión ahora está lista para ser compilada e integrada en FrankenPHP. Para hacerlo, consulta la documentación de [compilación de FrankenPHP](compile.md) para aprender cómo compilar FrankenPHP. Agrega el módulo usando la bandera `--with`, apuntando a la ruta de tu módulo:\n\n```console\nCGO_ENABLED=1 \\\nXCADDY_GO_BUILD_FLAGS=\"-ldflags='-w -s' -tags=nobadger,nomysql,nopgx\" \\\nCGO_CFLAGS=$(php-config --includes) \\\nCGO_LDFLAGS=\"$(php-config --ldflags) $(php-config --libs)\" \\\nxcaddy build \\\n    --output frankenphp \\\n    --with github.com/mi-cuenta/mi-módulo\n```\n\n¡Eso es todo! Tu extensión ahora está integrada en FrankenPHP y puede ser usada en tu código PHP.\n\n### Probando tu Extensión\n\nDespués de integrar tu extensión en FrankenPHP, puedes crear un archivo `index.php` con ejemplos para las funciones que has implementado:\n\n```php\n<?php\n\n// Probar función básica\ngo_print();\n\n// Probar función avanzada\necho go_upper(\"hola mundo\") . \"\\n\";\n```\n\nAhora puedes ejecutar FrankenPHP con este archivo usando `./frankenphp php-server`, y deberías ver tu extensión funcionando.\n"
  },
  {
    "path": "docs/es/github-actions.md",
    "content": "# Usando GitHub Actions\n\nEste repositorio construye y despliega la imagen Docker en [Docker Hub](https://hub.docker.com/r/dunglas/frankenphp) en\ncada pull request aprobado o en tu propio fork una vez configurado.\n\n## Configurando GitHub Actions\n\nEn la configuración del repositorio, bajo secrets, agrega los siguientes secretos:\n\n- `REGISTRY_LOGIN_SERVER`: El registro Docker a usar (ej. `docker.io`).\n- `REGISTRY_USERNAME`: El nombre de usuario para iniciar sesión en el registro (ej. `dunglas`).\n- `REGISTRY_PASSWORD`: La contraseña para iniciar sesión en el registro (ej. una clave de acceso).\n- `IMAGE_NAME`: El nombre de la imagen (ej. `dunglas/frankenphp`).\n\n## Construyendo y Subiendo la Imagen\n\n1. Crea un Pull Request o haz push a tu fork.\n2. GitHub Actions construirá la imagen y ejecutará cualquier prueba.\n3. Si la construcción es exitosa, la imagen será subida al registro usando la etiqueta `pr-x`, donde `x` es el número del PR.\n\n## Desplegando la Imagen\n\n1. Una vez que el Pull Request sea fusionado, GitHub Actions ejecutará nuevamente las pruebas y construirá una nueva imagen.\n2. Si la construcción es exitosa, la etiqueta `main` será actualizada en el registro Docker.\n\n## Lanzamientos (Releases)\n\n1. Crea una nueva etiqueta (tag) en el repositorio.\n2. GitHub Actions construirá la imagen y ejecutará cualquier prueba.\n3. Si la construcción es exitosa, la imagen será subida al registro usando el nombre de la etiqueta como etiqueta (ej. se crearán `v1.2.3` y `v1.2`).\n4. La etiqueta `latest` también será actualizada.\n"
  },
  {
    "path": "docs/es/hot-reload.md",
    "content": "# Hot reload\n\nFrankenPHP incluye una función de **hot reload** integrada diseñada para mejorar significativamente la experiencia del desarrollador.\n\n![Mercure](../hot-reload.png)\n\nEsta función proporciona un flujo de trabajo similar a **Hot Module Replacement (HMR)** encontrado en herramientas modernas de JavaScript (como Vite o webpack).\nEn lugar de actualizar manualmente el navegador después de cada cambio de archivo (código PHP, plantillas, archivos JavaScript y CSS...),\nFrankenPHP actualiza el contenido en tiempo real.\n\nLa Hot Reload funciona de forma nativa con WordPress, Laravel, Symfony y cualquier otra aplicación o framework PHP.\n\nCuando está activada, FrankenPHP vigila el directorio de trabajo actual en busca de cambios en el sistema de archivos.\nCuando se modifica un archivo, envía una actualización [Mercure](mercure.md) al navegador.\n\nDependiendo de la configuración, el navegador:\n\n- **Transformará el DOM** (preservando la posición de desplazamiento y el estado de los inputs) si [Idiomorph](https://github.com/bigskysoftware/idiomorph) está cargado.\n- **Recargará la página** (recarga en vivo estándar) si Idiomorph no está presente.\n\n## Configuración\n\nPara habilitar la Hot Reload, active Mercure y luego agregue la subdirectiva `hot_reload` a la directiva `php_server` en su `Caddyfile`.\n\n> [!WARNING]\n> Esta función está destinada **únicamente a entornos de desarrollo**.\n> No active `hot_reload` en producción, ya que vigilar el sistema de archivos implica una sobrecarga de rendimiento y expone endpoints internos.\n\n```caddyfile\nlocalhost\n\nmercure {\n    anonymous\n}\n\nroot public/\nphp_server {\n    hot_reload\n}\n```\n\nPor omisión, FrankenPHP vigilará todos los archivos en el directorio de trabajo actual que coincidan con este patrón glob: `./**/*.{css,env,gif,htm,html,jpg,jpeg,js,mjs,php,png,svg,twig,webp,xml,yaml,yml}`\n\nEs posible establecer explícitamente los archivos a vigilar usando la sintaxis glob:\n\n```caddyfile\nlocalhost\n\nmercure {\n    anonymous\n}\n\nroot public/\nphp_server {\n    hot_reload src/**/*{.php,.js} config/**/*.yaml\n}\n```\n\nUse la forma larga para especificar el tema de Mercure a utilizar, así como qué directorios o archivos vigilar, proporcionando rutas a la opción `hot_reload`:\n\n```caddyfile\nlocalhost\n\nmercure {\n    anonymous\n}\n\nroot public/\nphp_server {\n    hot_reload {\n        topic hot-reload-topic\n        watch src/**/*.php\n        watch assets/**/*.{ts,json}\n        watch templates/\n        watch public/css/\n    }\n}\n```\n\n## Integración Lado-Cliente\n\nMientras el servidor detecta los cambios, el navegador necesita suscribirse a estos eventos para actualizar la página.\nFrankenPHP expone la URL del Mercure Hub a utilizar para suscribirse a los cambios de archivos a través de la variable de entorno `$_SERVER['FRANKENPHP_HOT_RELOAD']`.\n\nUna biblioteca JavaScript de conveniencia, [frankenphp-hot-reload](https://www.npmjs.com/package/frankenphp-hot-reload), también está disponible para manejar la lógica lado-cliente.\nPara usarla, agregue lo siguiente a su diseño principal:\n\n```php\n<!DOCTYPE html>\n<title>FrankenPHP Hot Reload</title>\n<?php if (isset($_SERVER['FRANKENPHP_HOT_RELOAD'])): ?>\n<meta name=\"frankenphp-hot-reload:url\" content=\"<?=$_SERVER['FRANKENPHP_HOT_RELOAD']?>\">\n<script src=\"https://cdn.jsdelivr.net/npm/idiomorph\"></script>\n<script src=\"https://cdn.jsdelivr.net/npm/frankenphp-hot-reload/+esm\" type=\"module\"></script>\n<?php endif ?>\n```\n\nLa biblioteca se suscribirá automáticamente al hub de Mercure, obtendrá la URL actual en segundo plano cuando se detecte un cambio en un archivo y transformará el DOM.\nEstá disponible como un paquete [npm](https://www.npmjs.com/package/frankenphp-hot-reload) y en [GitHub](https://github.com/dunglas/frankenphp-hot-reload).\n\nAlternativamente, puede implementar su propia lógica lado-cliente suscribiéndose directamente al hub de Mercure usando la clase nativa de JavaScript `EventSource`.\n\n### Modo Worker\n\nSi está ejecutando su aplicación en [Modo Worker](https://frankenphp.dev/docs/worker/), el script de su aplicación permanece en memoria.\nEsto significa que los cambios en su código PHP no se reflejarán inmediatamente, incluso si el navegador se recarga.\n\nPara la mejor experiencia de desarrollador, debe combinar `hot_reload` con [la subdirectiva `watch` en la directiva `worker`](config.md#watching-for-file-changes).\n\n- `hot_reload`: actualiza el **navegador** cuando los archivos cambian\n- `worker.watch`: reinicia el worker cuando los archivos cambian\n\n```caddy\nlocalhost\n\nmercure {\n    anonymous\n}\n\nroot public/\nphp_server {\n    hot_reload\n    worker {\n        file /path/to/my_worker.php\n        watch\n    }\n}\n```\n\n### Funcionamiento\n\n1. **Vigilancia**: FrankenPHP monitorea el sistema de archivos en busca de modificaciones usando [la biblioteca `e-dant/watcher`](https://github.com/e-dant/watcher) internamente (contribuimos con el binding de Go).\n2. **Reinicio (Modo Worker)**: si `watch` está habilitado en la configuración del worker, el worker de PHP se reinicia para cargar el nuevo código.\n3. **Envío**: se envía una carga útil JSON que contiene la lista de archivos modificados al [hub de Mercure](https://mercure.rocks) integrado.\n4. **Recepción**: El navegador, escuchando a través de la biblioteca JavaScript, recibe el evento de Mercure.\n5. **Actualización**:\n\n- Si se detecta **Idiomorph**, obtiene el contenido actualizado y transforma el HTML actual para que coincida con el nuevo estado, aplicando los cambios al instante sin perder el estado.\n- De lo contrario, se llama a `window.location.reload()` para recargar la página.\n"
  },
  {
    "path": "docs/es/known-issues.md",
    "content": "# Problemas Conocidos\n\n## Extensiones PHP no Soportadas\n\nLas siguientes extensiones se sabe que no son compatibles con FrankenPHP:\n\n| Nombre                                                                                                        | Razón               | Alternativas                                                                                                         |\n| ----------------------------------------------------------------------------------------------------------- | ------------------- | -------------------------------------------------------------------------------------------------------------------- |\n| [imap](https://www.php.net/manual/es/imap.installation.php)                                                 | No es thread-safe   | [javanile/php-imap2](https://github.com/javanile/php-imap2), [webklex/php-imap](https://github.com/Webklex/php-imap) |\n| [newrelic](https://docs.newrelic.com/docs/apm/agents/php-agent/getting-started/introduction-new-relic-php/) | No es thread-safe   | -                                                                                                                    |\n\n## Extensiones PHP con Errores\n\nLas siguientes extensiones tienen errores conocidos y comportamientos inesperados cuando se usan con FrankenPHP:\n\n| Nombre                                                          | Problema                                                                                                                                                                                                                   |\n| ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [ext-openssl](https://www.php.net/manual/es/book.openssl.php) | Cuando se usa musl libc, la extensión OpenSSL puede fallar bajo cargas pesadas. El problema no ocurre cuando se usa la más popular GNU libc. Este error está [siendo rastreado por PHP](https://github.com/php/php-src/issues/13648). |\n\n## get_browser\n\nLa función [get_browser()](https://www.php.net/manual/es/function.get-browser.php) parece funcionar mal después de un tiempo. Una solución es almacenar en caché (por ejemplo, con [APCu](https://www.php.net/manual/es/book.apcu.php)) los resultados por User Agent, ya que son estáticos.\n\n## Binario Autónomo e Imágenes Docker Basadas en Alpine\n\nLos binarios completamente estáticos y las imágenes Docker basadas en Alpine (`dunglas/frankenphp:*-alpine`) usan [musl libc](https://musl.libc.org/) en lugar de [glibc](https://www.etalabs.net/compare_libcs.html), para mantener un tamaño de binario más pequeño.\nEsto puede llevar a algunos problemas de compatibilidad.\nEn particular, la bandera glob `GLOB_BRACE` [no está disponible](https://www.php.net/manual/es/function.glob.php).\n\nSe recomienda usar la variante GNU del binario estático y las imágenes Docker basadas en Debian si encuentras problemas.\n\n## Usar `https://127.0.0.1` con Docker\n\nPor defecto, FrankenPHP genera un certificado TLS para `localhost`.\nEs la opción más fácil y recomendada para el desarrollo local.\n\nSi realmente deseas usar `127.0.0.1` como host en su lugar, es posible configurarlo para generar un certificado para él estableciendo el nombre del servidor en `127.0.0.1`.\n\nDesafortunadamente, esto no es suficiente al usar Docker debido a [su sistema de red](https://docs.docker.com/network/).\nObtendrás un error TLS similar a `curl: (35) LibreSSL/3.3.6: error:1404B438:SSL routines:ST_CONNECT:tlsv1 alert internal error`.\n\nSi estás usando Linux, una solución es usar [el controlador de red host](https://docs.docker.com/network/network-tutorial-host/):\n\n```console\ndocker run \\\n    -e SERVER_NAME=\"127.0.0.1\" \\\n    -v $PWD:/app/public \\\n    --network host \\\n    dunglas/frankenphp\n```\n\nEl controlador de red host no está soportado en Mac y Windows. En estas plataformas, tendrás que adivinar la dirección IP del contenedor e incluirla en los nombres del servidor.\n\nEjecuta `docker network inspect bridge` y busca la clave `Containers` para identificar la última dirección IP actualmente asignada bajo la clave `IPv4Address`, y incrementa en uno. Si no hay contenedores en ejecución, la primera dirección IP asignada suele ser `172.17.0.2`.\n\nLuego, incluye esto en la variable de entorno `SERVER_NAME`:\n\n```console\ndocker run \\\n    -e SERVER_NAME=\"127.0.0.1, 172.17.0.3\" \\\n    -v $PWD:/app/public \\\n    -p 80:80 -p 443:443 -p 443:443/udp \\\n    dunglas/frankenphp\n```\n\n> [!CAUTION]\n>\n> Asegúrate de reemplazar `172.17.0.3` con la IP que se asignará a tu contenedor.\n\nAhora deberías poder acceder a `https://127.0.0.1` desde la máquina host.\n\nSi no es así, inicia FrankenPHP en modo depuración para intentar identificar el problema:\n\n```console\ndocker run \\\n    -e CADDY_GLOBAL_OPTIONS=\"debug\" \\\n    -e SERVER_NAME=\"127.0.0.1\" \\\n    -v $PWD:/app/public \\\n    -p 80:80 -p 443:443 -p 443:443/udp \\\n    dunglas/frankenphp\n```\n\n## Scripts de Composer que Referencian `@php`\n\nLos [scripts de Composer](https://getcomposer.org/doc/articles/scripts.md) pueden querer ejecutar un binario PHP para algunas tareas, por ejemplo, en [un proyecto Laravel](laravel.md) para ejecutar `@php artisan package:discover --ansi`. Esto [actualmente falla](https://github.com/php/frankenphp/issues/483#issuecomment-1899890915) por dos razones:\n\n- Composer no sabe cómo llamar al binario de FrankenPHP;\n- Composer puede agregar configuraciones de PHP usando la bandera `-d` en el comando, que FrankenPHP aún no soporta.\n\nComo solución alternativa, podemos crear un script de shell en `/usr/local/bin/php` que elimine los parámetros no soportados y luego llame a FrankenPHP:\n\n```bash\n#!/usr/bin/env bash\nargs=(\"$@\")\nindex=0\nfor i in \"$@\"\ndo\n    if [ \"$i\" == \"-d\" ]; then\n        unset 'args[$index]'\n        unset 'args[$index+1]'\n    fi\n    index=$((index+1))\ndone\n\n/usr/local/bin/frankenphp php-cli ${args[@]}\n```\n\nLuego, establece la variable de entorno `PHP_BINARY` a la ruta de nuestro script `php` y ejecuta Composer:\n\n```console\nexport PHP_BINARY=/usr/local/bin/php\ncomposer install\n```\n\n## Solución de Problemas de TLS/SSL con Binarios Estáticos\n\nAl usar los binarios estáticos, puedes encontrar los siguientes errores relacionados con TLS, por ejemplo, al enviar correos electrónicos usando STARTTLS:\n\n```text\nNo se puede conectar con STARTTLS: stream_socket_enable_crypto(): La operación SSL falló con el código 5. Mensajes de error de OpenSSL:\nerror:80000002:librería del sistema::No existe el archivo o el directorio\nerror:80000002:librería del sistema::No existe el archivo o el directorio\nerror:80000002:librería del sistema::No existe el archivo o el directorio\nerror:0A000086:rutinas de SSL::falló la verificación del certificado\n```\n\nDado que el binario estático no incluye certificados TLS, necesitas indicar a OpenSSL la ubicación de tu instalación local de certificados CA.\n\nInspecciona la salida de [`openssl_get_cert_locations()`](https://www.php.net/manual/es/function.openssl-get-cert-locations.php),\npara encontrar dónde deben instalarse los certificados CA y guárdalos en esa ubicación.\n\n> [!CAUTION]\n>\n> Los contextos web y CLI pueden tener configuraciones diferentes.\n> Asegúrate de ejecutar `openssl_get_cert_locations()` en el contexto adecuado.\n\n[Los certificados CA extraídos de Mozilla pueden descargarse del sitio de cURL](https://curl.se/docs/caextract.html).\n\nAlternativamente, muchas distribuciones, incluyendo Debian, Ubuntu y Alpine, proporcionan paquetes llamados `ca-certificates` que contienen estos certificados.\n\nTambién es posible usar `SSL_CERT_FILE` y `SSL_CERT_DIR` para indicar a OpenSSL dónde buscar los certificados CA:\n\n```console\n# Establecer variables de entorno de certificados TLS\nexport SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt\nexport SSL_CERT_DIR=/etc/ssl/certs\n```\n"
  },
  {
    "path": "docs/es/laravel.md",
    "content": "# Laravel\n\n## Docker\n\nServir una aplicación web [Laravel](https://laravel.com) con FrankenPHP es tan fácil como montar el proyecto en el directorio `/app` de la imagen Docker oficial.\n\nEjecuta este comando desde el directorio principal de tu aplicación Laravel:\n\n```console\ndocker run -p 80:80 -p 443:443 -p 443:443/udp -v $PWD:/app dunglas/frankenphp\n```\n\n¡Y listo!\n\n## Instalación Local\n\nAlternativamente, puedes ejecutar tus proyectos Laravel con FrankenPHP desde tu máquina local:\n\n1. [Descarga el binario correspondiente a tu sistema](../#binario-autónomo)\n2. Agrega la siguiente configuración a un archivo llamado `Caddyfile` en el directorio raíz de tu proyecto Laravel:\n\n   ```caddyfile\n   {\n   \tfrankenphp\n   }\n\n   # El nombre de dominio de tu servidor\n   localhost {\n   \t# Establece el directorio web raíz en public/\n   \troot public/\n   \t# Habilita la compresión (opcional)\n   \tencode zstd br gzip\n   \t# Ejecuta archivos PHP desde el directorio public/ y sirve los assets\n   \tphp_server {\n   \t\ttry_files {path} index.php\n   \t}\n   }\n   ```\n\n3. Inicia FrankenPHP desde el directorio raíz de tu proyecto Laravel: `frankenphp run`\n\n## Laravel Octane\n\nOctane se puede instalar a través del gestor de paquetes Composer:\n\n```console\ncomposer require laravel/octane\n```\n\nDespués de instalar Octane, puedes ejecutar el comando Artisan `octane:install`, que instalará el archivo de configuración de Octane en tu aplicación:\n\n```console\nphp artisan octane:install --server=frankenphp\n```\n\nEl servidor Octane se puede iniciar mediante el comando Artisan `octane:frankenphp`.\n\n```console\nphp artisan octane:frankenphp\n```\n\nEl comando `octane:frankenphp` puede tomar las siguientes opciones:\n\n- `--host`: La dirección IP a la que el servidor debe enlazarse (por defecto: `127.0.0.1`)\n- `--port`: El puerto en el que el servidor debe estar disponible (por defecto: `8000`)\n- `--admin-port`: El puerto en el que el servidor de administración debe estar disponible (por defecto: `2019`)\n- `--workers`: El número de workers que deben estar disponibles para manejar solicitudes (por defecto: `auto`)\n- `--max-requests`: El número de solicitudes a procesar antes de recargar el servidor (por defecto: `500`)\n- `--caddyfile`: La ruta al archivo `Caddyfile` de FrankenPHP (por defecto: [Caddyfile de plantilla en Laravel Octane](https://github.com/laravel/octane/blob/2.x/src/Commands/stubs/Caddyfile))\n- `--https`: Habilita HTTPS, HTTP/2 y HTTP/3, y genera y renueva certificados automáticamente\n- `--http-redirect`: Habilita la redirección de HTTP a HTTPS (solo se habilita si se pasa --https)\n- `--watch`: Recarga automáticamente el servidor cuando se modifica la aplicación\n- `--poll`: Usa la sondea del sistema de archivos mientras se observa para vigilar archivos a través de una red\n- `--log-level`: Registra mensajes en o por encima del nivel de registro especificado, usando el registrador nativo de Caddy\n\n> [!TIP]\n> Para obtener registros JSON estructurados (útil al usar soluciones de análisis de registros), pasa explícitamente la opción `--log-level`.\n\nConsulta también [cómo usar Mercure con Octane](#soporte-para-mercure).\n\nAprende más sobre [Laravel Octane en su documentación oficial](https://laravel.com/docs/octane).\n\n## Aplicaciones Laravel como Binarios Autónomos\n\nUsando [la característica de incrustación de aplicaciones de FrankenPHP](embed.md), es posible distribuir aplicaciones Laravel\ncomo binarios autónomos.\n\nSigue estos pasos para empaquetar tu aplicación Laravel como un binario autónomo para Linux:\n\n1. Crea un archivo llamado `static-build.Dockerfile` en el repositorio de tu aplicación:\n\n   ```dockerfile\n   FROM --platform=linux/amd64 dunglas/frankenphp:static-builder-gnu\n   # Si tienes intención de ejecutar el binario en sistemas musl-libc, usa static-builder-musl en su lugar\n\n   # Copia tu aplicación\n   WORKDIR /go/src/app/dist/app\n   COPY . .\n\n   # Elimina las pruebas y otros archivos innecesarios para ahorrar espacio\n   # Alternativamente, agrega estos archivos a un archivo .dockerignore\n   RUN rm -Rf tests/\n\n   # Copia el archivo .env\n   RUN cp .env.example .env\n   # Cambia APP_ENV y APP_DEBUG para que estén listos para producción\n   RUN sed -i'' -e 's/^APP_ENV=.*/APP_ENV=production/' -e 's/^APP_DEBUG=.*/APP_DEBUG=false/' .env\n\n   # Realiza otros cambios en tu archivo .env si es necesario\n\n   # Instala las dependencias\n   RUN composer install --ignore-platform-reqs --no-dev -a\n\n   # Compila el binario estático\n   WORKDIR /go/src/app/\n   RUN EMBED=dist/app/ ./build-static.sh\n   ```\n\n   > [!CAUTION]\n   >\n   > Algunos archivos `.dockerignore`\n   > ignorarán el directorio `vendor/` y los archivos `.env`. Asegúrate de ajustar o eliminar el archivo `.dockerignore` antes de la compilación.\n\n2. Compila:\n\n   ```console\n   docker build -t static-laravel-app -f static-build.Dockerfile .\n   ```\n\n3. Extrae el binario:\n\n   ```console\n   docker cp $(docker create --name static-laravel-app-tmp static-laravel-app):/go/src/app/dist/frankenphp-linux-x86_64 frankenphp ; docker rm static-laravel-app-tmp\n   ```\n\n4. Rellena las cachés:\n\n   ```console\n   frankenphp php-cli artisan optimize\n   ```\n\n5. Ejecuta las migraciones de la base de datos (si las hay):\n\n   ```console\n   frankenphp php-cli artisan migrate\n   ```\n\n6. Genera la clave secreta de la aplicación:\n\n   ```console\n   frankenphp php-cli artisan key:generate\n   ```\n\n7. Inicia el servidor:\n\n   ```console\n   frankenphp php-server\n   ```\n\n¡Tu aplicación ya está lista!\n\nAprende más sobre las opciones disponibles y cómo compilar binarios para otros sistemas operativos en la documentación de [incrustación de aplicaciones](embed.md).\n\n### Cambiar la Ruta de Almacenamiento\n\nPor defecto, Laravel almacena los archivos subidos, cachés, registros, etc. en el directorio `storage/` de la aplicación.\nEsto no es adecuado para aplicaciones incrustadas, ya que cada nueva versión se extraerá en un directorio temporal diferente.\n\nEstablece la variable de entorno `LARAVEL_STORAGE_PATH` (por ejemplo, en tu archivo `.env`) o llama al método `Illuminate\\Foundation\\Application::useStoragePath()` para usar un directorio fuera del directorio temporal.\n\n### Soporte para Mercure\n\n[Mercure](https://mercure.rocks) es una excelente manera de agregar capacidades en tiempo real a tus aplicaciones Laravel.\nFrankenPHP incluye [soporte para Mercure integrado](mercure.md).\n\nSi no estás usando [Octane](#laravel-octane), consulta [la entrada de documentación de Mercure](mercure.md).\n\nSi estás usando Octane, puedes habilitar el soporte para Mercure agregando las siguientes líneas a tu archivo `config/octane.php`:\n\n```php\n// ...\n\nreturn [\n    // ...\n\n    'mercure' => [\n        'anonymous' => true,\n        'publisher_jwt' => '!CambiaEstaClaveSecretaJWTDelHubMercure!',\n        'subscriber_jwt' => '!CambiaEstaClaveSecretaJWTDelHubMercure!',\n    ],\n];\n```\n\nPuedes usar [todas las directivas soportadas por Mercure](https://mercure.rocks/docs/hub/config#directives) en este array.\n\nPara publicar y suscribirte a actualizaciones, recomendamos usar la biblioteca [Laravel Mercure Broadcaster](https://github.com/mvanduijker/laravel-mercure-broadcaster).\nAlternativamente, consulta [la documentación de Mercure](mercure.md) para hacerlo en PHP y JavaScript puros.\n\n### Ejecutar Octane con Binarios Autónomos\n\n¡Incluso es posible empaquetar aplicaciones Laravel Octane como binarios autónomos!\n\nPara hacerlo, [instala Octane correctamente](#laravel-octane) y sigue los pasos descritos en [la sección anterior](#aplicaciones-laravel-como-binarios-autónomos).\n\nLuego, para iniciar FrankenPHP en modo worker a través de Octane, ejecuta:\n\n```console\nPATH=\"$PWD:$PATH\" frankenphp php-cli artisan octane:frankenphp\n```\n\n> [!CAUTION]\n>\n> Para que el comando funcione, el binario autónomo **debe** llamarse `frankenphp`\n> porque Octane necesita un programa llamado `frankenphp` disponible en la ruta.\n"
  },
  {
    "path": "docs/es/logging.md",
    "content": "# Registro de actividad\n\nFrankenPHP se integra perfectamente con [el sistema de registro de Caddy](https://caddyserver.com/docs/logging).\nPuede registrar mensajes usando funciones estándar de PHP o aprovechar la función dedicada `frankenphp_log()` para capacidades avanzadas de registro estructurado.\n\n## `frankenphp_log()`\n\nLa función `frankenphp_log()` le permite emitir registros estructurados directamente desde su aplicación PHP,\nfacilitando la ingesta en plataformas como Datadog, Grafana Loki o Elastic, así como el soporte para OpenTelemetry.\n\nInternamente, `frankenphp_log()` envuelve [el paquete `log/slog` de Go](https://pkg.go.dev/log/slog) para proporcionar funciones avanzadas de registro.\n\nEstos registros incluyen el nivel de gravedad y datos de contexto opcionales.\n\n```php\nfunction frankenphp_log(string $message, int $level = FRANKENPHP_LOG_LEVEL_INFO, array $context = []): void\n```\n\n### Parámetros\n\n- **`message`**: El string del mensaje de registro.\n- **`level`**: El nivel de gravedad del registro. Puede ser cualquier entero arbitrario. Se proporcionan constantes de conveniencia para niveles comunes: `FRANKENPHP_LOG_LEVEL_DEBUG` (`-4`), `FRANKENPHP_LOG_LEVEL_INFO` (`0`), `FRANKENPHP_LOG_LEVEL_WARN` (`4`) y `FRANKENPHP_LOG_LEVEL_ERROR` (`8`)). Por omisión es `FRANKENPHP_LOG_LEVEL_INFO`.\n- **`context`**: Un array asociativo de datos adicionales para incluir en la entrada del registro.\n\n### Ejemplo\n\n```php\n<?php\n\n// Registrar un mensaje informativo simple\nfrankenphp_log(\"¡Hola desde FrankenPHP!\");\n\n// Registrar una advertencia con datos de contexto\nfrankenphp_log(\n    \"Uso de memoria alto\",\n    FRANKENPHP_LOG_LEVEL_WARN,\n    [\n        'uso_actual' => memory_get_usage(),\n        'uso_pico' => memory_get_peak_usage(),\n    ],\n);\n\n```\n\nAl ver los registros (por ejemplo, mediante `docker compose logs`), la salida aparecerá como JSON estructurado:\n\n```json\n{\"level\":\"info\",\"ts\":1704067200,\"logger\":\"frankenphp\",\"msg\":\"¡Hola desde FrankenPHP!\"}\n{\"level\":\"warn\",\"ts\":1704067200,\"logger\":\"frankenphp\",\"msg\":\"Uso de memoria alto\",\"uso_actual\":10485760,\"uso_pico\":12582912}\n```\n\n## `error_log()`\n\nFrankenPHP también permite el registro mediante la función estándar `error_log()`. Si el parámetro `$message_type` es `4` (SAPI),\nestos mensajes se redirigen al registrador de Caddy.\n\nPor omisión, los mensajes enviados a través de `error_log()` se tratan como texto no estructurado.\nSon útiles para la compatibilidad con aplicaciones o bibliotecas existentes que dependen de la biblioteca estándar de PHP.\n\n### Uso\n\n```php\nerror_log(\"Fallo en la conexión a la base de datos\", 4);\n```\n\nEsto aparecerá en los registros de Caddy, a menudo con un prefijo que indica que se originó desde PHP.\n\n> [!TIP]\n> Para una mejor observabilidad en entornos de producción, prefiera `frankenphp_log()`\n> ya que permite filtrar registros por nivel (Depuración, Error, etc.)\n> y consultar campos específicos en su infraestructura de registro.\n"
  },
  {
    "path": "docs/es/mercure.md",
    "content": "# Tiempo Real\n\n¡FrankenPHP incluye un hub [Mercure](https://mercure.rocks) integrado!\nMercure te permite enviar eventos en tiempo real a todos los dispositivos conectados: recibirán un evento JavaScript al instante.\n\n¡Es una alternativa conveniente a WebSockets que es simple de usar y es soportada nativamente por todos los navegadores web modernos!\n\n![Mercure](../mercure-hub.png)\n\n## Habilitando Mercure\n\nEl soporte para Mercure está deshabilitado por defecto.\nAquí tienes un ejemplo mínimo de un `Caddyfile` que habilita tanto FrankenPHP como el hub Mercure:\n\n```caddyfile\n# El nombre de host al que responder\nlocalhost\n\nmercure {\n    # La clave secreta usada para firmar los tokens JWT para los publicadores\n    publisher_jwt !CambiaEstaClaveSecretaJWTDelHubMercure!\n    # Cuando se establece publisher_jwt, ¡también debes establecer subscriber_jwt!\n    subscriber_jwt !CambiaEstaClaveSecretaJWTDelHubMercure!\n    # Permite suscriptores anónimos (sin JWT)\n    anonymous\n}\n\nroot public/\nphp_server\n```\n\n> [!TIP]\n>\n> El [`Caddyfile` de ejemplo](https://github.com/php/frankenphp/blob/main/caddy/frankenphp/Caddyfile)\n> proporcionado por [las imágenes Docker](docker.md) ya incluye una configuración comentada de Mercure\n> con variables de entorno convenientes para configurarlo.\n>\n> Descomenta la sección Mercure en `/etc/frankenphp/Caddyfile` para habilitarla.\n\n## Suscribiéndose a Actualizaciones\n\nPor defecto, el hub Mercure está disponible en la ruta `/.well-known/mercure` de tu servidor FrankenPHP.\nPara suscribirte a actualizaciones, usa la clase nativa [`EventSource`](https://developer.mozilla.org/es/docs/Web/API/EventSource) de JavaScript:\n\n```html\n<!-- public/index.html -->\n<!doctype html>\n<title>Ejemplo Mercure</title>\n<script>\n  const eventSource = new EventSource(\"/.well-known/mercure?topic=mi-tema\");\n  eventSource.onmessage = function (event) {\n    console.log(\"Nuevo mensaje:\", event.data);\n  };\n</script>\n```\n\n## Publicando Actualizaciones\n\n### Usando `mercure_publish()`\n\nFrankenPHP proporciona una función conveniente `mercure_publish()` para publicar actualizaciones en el hub Mercure integrado:\n\n```php\n<?php\n// public/publish.php\n\n$updateID = mercure_publish('mi-tema', json_encode(['clave' => 'valor']));\n\n// Escribir en los registros de FrankenPHP\nerror_log(\"actualización $updateID publicada\", 4);\n```\n\nLa firma completa de la función es:\n\n```php\n/**\n * @param string|string[] $topics\n */\nfunction mercure_publish(string|array $topics, string $data = '', bool $private = false, ?string $id = null, ?string $type = null, ?int $retry = null): string {}\n```\n\n### Usando `file_get_contents()`\n\nPara enviar una actualización a los suscriptores conectados, envía una solicitud POST autenticada al hub Mercure con los parámetros `topic` y `data`:\n\n```php\n<?php\n// public/publish.php\n\nconst JWT_SECRET = '!ChangeThisMercureHubJWTSecretKey!'; // Debe ser la misma que mercure.publisher_jwt en Caddyfile\n\n$updateID = file_get_contents('https://localhost/.well-known/mercure', context: stream_context_create(['http' => [\n    'method'  => 'POST',\n    'header'  => \"Content-type: application/x-www-form-urlencoded\\r\\nAuthorization: Bearer \" . JWT,\n    'content' => http_build_query([\n        'topic' => 'mi-tema',\n        'data' => json_encode(['clave' => 'valor']),\n    ]),\n]]));\n\n// Escribir en los registros de FrankenPHP\nerror_log(\"actualización $updateID publicada\", 4);\n```\n\nLa clave pasada como parámetro de la opción `mercure.publisher_jwt` en el `Caddyfile` debe usarse para firmar el token JWT usado en el encabezado `Authorization`.\n\nEl JWT debe incluir un reclamo `mercure` con un permiso `publish` para los temas a los que deseas publicar.\nConsulta [la documentación de Mercure](https://mercure.rocks/spec#publishers) sobre autorización.\n\nPara generar tus propios tokens, puedes usar [este enlace de jwt.io](https://www.jwt.io/#token=eyJhbGciOiJIUzI1NiJ9.eyJtZXJjdXJlIjp7InB1Ymxpc2giOlsiKiJdfX0.PXwpfIGng6KObfZlcOXvcnWCJOWTFLtswGI5DZuWSK4),\npero para aplicaciones en producción, se recomienda usar tokens de corta duración generados dinámicamente usando una biblioteca [JWT](https://www.jwt.io/libraries?programming_language=php) confiable.\n\n### Usando Symfony Mercure\n\nAlternativamente, puedes usar el [Componente Symfony Mercure](https://symfony.com/components/Mercure), una biblioteca PHP independiente.\n\nEsta biblioteca maneja la generación de JWT, la publicación de actualizaciones así como la autorización basada en cookies para los suscriptores.\n\nPrimero, instala la biblioteca usando Composer:\n\n```console\ncomposer require symfony/mercure lcobucci/jwt\n```\n\nLuego, puedes usarla de la siguiente manera:\n\n```php\n<?php\n// public/publish.php\n\nrequire __DIR__ . '/../vendor/autoload.php';\n\nconst JWT_SECRET = '!CambiaEstaClaveSecretaJWTDelHubMercure!'; // Debe ser la misma que mercure.publisher_jwt en Caddyfile\n\n// Configurar el proveedor de tokens JWT\n$jwFactory = new \\Symfony\\Component\\Mercure\\Jwt\\LcobucciFactory(JWT_SECRET);\n$provider = new \\Symfony\\Component\\Mercure\\Jwt\\FactoryTokenProvider($jwFactory, publish: ['*']);\n\n$hub = new \\Symfony\\Component\\Mercure\\Hub('https://localhost/.well-known/mercure', $provider);\n// Serializar la actualización y enviarla al hub, que la transmitirá a los clientes\n$updateID = $hub->publish(new \\Symfony\\Component\\Mercure\\Update('mi-tema', json_encode(['clave' => 'valor'])));\n\n// Escribir en los registros de FrankenPHP\nerror_log(\"actualización $updateID publicada\", 4);\n```\n\nMercure también es soportado nativamente por:\n\n- [Laravel](laravel.md#soporte-para-mercure)\n- [Symfony](https://symfony.com/doc/current/mercure.html)\n- [API Platform](https://api-platform.com/docs/core/mercure/)\n"
  },
  {
    "path": "docs/es/metrics.md",
    "content": "# Métricas\n\nCuando las [métricas de Caddy](https://caddyserver.com/docs/metrics) están habilitadas, FrankenPHP expone las siguientes métricas:\n\n- `frankenphp_total_threads`: El número total de hilos PHP.\n- `frankenphp_busy_threads`: El número de hilos PHP procesando actualmente una solicitud (los workers en ejecución siempre consumen un hilo).\n- `frankenphp_queue_depth`: El número de solicitudes regulares en cola\n- `frankenphp_total_workers{worker=\"[nombre_worker]\"}`: El número total de workers.\n- `frankenphp_busy_workers{worker=\"[nombre_worker]\"}`: El número de workers procesando actualmente una solicitud.\n- `frankenphp_worker_request_time{worker=\"[nombre_worker]\"}`: El tiempo dedicado al procesamiento de solicitudes por todos los workers.\n- `frankenphp_worker_request_count{worker=\"[nombre_worker]\"}`: El número de solicitudes procesadas por todos los workers.\n- `frankenphp_ready_workers{worker=\"[nombre_worker]\"}`: El número de workers que han llamado a `frankenphp_handle_request` al menos una vez.\n- `frankenphp_worker_crashes{worker=\"[nombre_worker]\"}`: El número de veces que un worker ha terminado inesperadamente.\n- `frankenphp_worker_restarts{worker=\"[nombre_worker]\"}`: El número de veces que un worker ha sido reiniciado deliberadamente.\n- `frankenphp_worker_queue_depth{worker=\"[nombre_worker]\"}`: El número de solicitudes en cola.\n\nPara las métricas de los workers, el marcador de posición `[nombre_worker]` es reemplazado por el nombre del worker en el Caddyfile; de lo contrario, se usará la ruta absoluta del archivo del worker.\n"
  },
  {
    "path": "docs/es/performance.md",
    "content": "# Rendimiento\n\nPor defecto, FrankenPHP intenta ofrecer un buen compromiso entre rendimiento y facilidad de uso.\nSin embargo, es posible mejorar sustancialmente el rendimiento usando una configuración adecuada.\n\n## Número de Hilos y Workers\n\nPor defecto, FrankenPHP inicia 2 veces más hilos y workers (en modo worker) que el número de CPUs disponibles.\n\nLos valores apropiados dependen en gran medida de cómo está escrita tu aplicación, qué hace y tu hardware.\nRecomendamos encarecidamente cambiar estos valores. Para una mejor estabilidad del sistema, se recomienda que `num_threads` x `memory_limit` < `memoria_disponible`.\n\nPara encontrar los valores correctos, es mejor ejecutar pruebas de carga que simulen tráfico real.\n[k6](https://k6.io) y [Gatling](https://gatling.io) son buenas herramientas para esto.\n\nPara configurar el número de hilos, usa la opción `num_threads` de las directivas `php_server` y `php`.\nPara cambiar el número de workers, usa la opción `num` de la sección `worker` de la directiva `frankenphp`.\n\n### `max_threads`\n\nAunque siempre es mejor saber exactamente cómo será tu tráfico, las aplicaciones reales tienden a ser más impredecibles.\nLa configuración `max_threads` [configuración](config.md#caddyfile-config) permite a FrankenPHP generar automáticamente hilos adicionales en tiempo de ejecución hasta el límite especificado.\n`max_threads` puede ayudarte a determinar cuántos hilos necesitas para manejar tu tráfico y puede hacer que el servidor sea más resiliente a picos de latencia.\nSi se establece en `auto`, el límite se estimará en función del `memory_limit` en tu `php.ini`. Si no puede hacerlo,\n`auto` se establecerá por defecto en 2x `num_threads`. Ten en cuenta que `auto` puede subestimar fuertemente el número de hilos necesarios.\n`max_threads` es similar a [pm.max_children](https://www.php.net/manual/es/install.fpm.configuration.php#pm.max-children) de PHP FPM. La principal diferencia es que FrankenPHP usa hilos en lugar de procesos y los delega automáticamente entre diferentes scripts de worker y el 'modo clásico' según sea necesario.\n\n## Modo Worker\n\nHabilitar [el modo worker](worker.md) mejora drásticamente el rendimiento,\npero tu aplicación debe adaptarse para ser compatible con este modo:\ndebes crear un script de worker y asegurarte de que la aplicación no tenga fugas de memoria.\n\n## No Usar musl\n\nLa variante Alpine Linux de las imágenes Docker oficiales y los binarios predeterminados que proporcionamos usan [la libc musl](https://musl.libc.org).\n\nSe sabe que PHP es [más lento](https://gitlab.alpinelinux.org/alpine/aports/-/issues/14381) cuando usa esta biblioteca C alternativa en lugar de la biblioteca GNU tradicional,\nespecialmente cuando se compila en modo ZTS (thread-safe), que es requerido para FrankenPHP. La diferencia puede ser significativa en un entorno con muchos hilos.\n\nAdemás, [algunos errores solo ocurren cuando se usa musl](https://github.com/php/php-src/issues?q=sort%3Aupdated-desc+is%3Aissue+is%3Aopen+label%3ABug+musl).\n\nEn entornos de producción, recomendamos usar FrankenPHP vinculado a glibc, compilado con un nivel de optimización adecuado.\n\nEsto se puede lograr usando las imágenes Docker de Debian, usando los paquetes de nuestros mantenedores [.deb](https://debs.henderkes.com) o [.rpm](https://rpms.henderkes.com), o [compilando FrankenPHP desde las fuentes](compile.md).\n\n## Configuración del Runtime de Go\n\nFrankenPHP está escrito en Go.\n\nEn general, el runtime de Go no requiere ninguna configuración especial, pero en ciertas circunstancias,\nuna configuración específica mejora el rendimiento.\n\nProbablemente quieras establecer la variable de entorno `GODEBUG` en `cgocheck=0` (el valor predeterminado en las imágenes Docker de FrankenPHP).\n\nSi ejecutas FrankenPHP en contenedores (Docker, Kubernetes, LXC...) y limitas la memoria disponible para los contenedores,\nestablece la variable de entorno `GOMEMLIMIT` en la cantidad de memoria disponible.\n\nPara más detalles, [la página de documentación de Go dedicada a este tema](https://pkg.go.dev/runtime#hdr-Environment_Variables) es una lectura obligada para aprovechar al máximo el runtime.\n\n## `file_server`\n\nPor defecto, la directiva `php_server` configura automáticamente un servidor de archivos para\nservir archivos estáticos (assets) almacenados en el directorio raíz.\n\nEsta característica es conveniente, pero tiene un costo.\nPara deshabilitarla, usa la siguiente configuración:\n\n```caddyfile\nphp_server {\n    file_server off\n}\n```\n\n## `try_files`\n\nAdemás de los archivos estáticos y los archivos PHP, `php_server` también intentará servir los archivos de índice de tu aplicación\ny los índices de directorio (`/ruta/` -> `/ruta/index.php`). Si no necesitas índices de directorio,\npuedes deshabilitarlos definiendo explícitamente `try_files` de esta manera:\n\n```caddyfile\nphp_server {\n    try_files {path} index.php\n    root /ruta/a/tu/app # agregar explícitamente la raíz aquí permite un mejor almacenamiento en caché\n}\n```\n\nEsto puede reducir significativamente el número de operaciones de archivo innecesarias.\n\nUn enfoque alternativo con 0 operaciones innecesarias de sistema de archivos sería usar en su lugar la directiva `php` y separar\nlos archivos de PHP por ruta. Este enfoque funciona bien si toda tu aplicación es servida por un solo archivo de entrada.\nUn ejemplo de [configuración](config.md#caddyfile-config) que sirve archivos estáticos detrás de una carpeta `/assets` podría verse así:\n\n```caddyfile\nroute {\n    @assets {\n        path /assets/*\n    }\n\n    # todo lo que está detrás de /assets es manejado por el servidor de archivos\n    file_server @assets {\n        root /ruta/a/tu/app\n    }\n\n    # todo lo que no está en /assets es manejado por tu archivo index o worker PHP\n    rewrite index.php\n    php {\n        root /ruta/a/tu/app # agregar explícitamente la raíz aquí permite un mejor almacenamiento en caché\n    }\n}\n```\n\n## Marcadores de Posición (Placeholders)\n\nPuedes usar [marcadores de posición](https://caddyserver.com/docs/conventions#placeholders) en las directivas `root` y `env`.\nSin embargo, esto evita el almacenamiento en caché de estos valores y conlleva un costo significativo de rendimiento.\n\nSi es posible, evita los marcadores de posición en estas directivas.\n\n## `resolve_root_symlink`\n\nPor defecto, si la raíz del documento es un enlace simbólico, se resuelve automáticamente por FrankenPHP (esto es necesario para que PHP funcione correctamente).\nSi la raíz del documento no es un enlace simbólico, puedes deshabilitar esta característica.\n\n```caddyfile\nphp_server {\n    resolve_root_symlink false\n}\n```\n\nEsto mejorará el rendimiento si la directiva `root` contiene [marcadores de posición](https://caddyserver.com/docs/conventions#placeholders).\nLa ganancia será negligible en otros casos.\n\n## Registros (Logs)\n\nEl registro es obviamente muy útil, pero, por definición,\nrequiere operaciones de E/S y asignaciones de memoria, lo que reduce considerablemente el rendimiento.\nAsegúrate de [establecer el nivel de registro](https://caddyserver.com/docs/caddyfile/options#log) correctamente,\ny registra solo lo necesario.\n\n## Rendimiento de PHP\n\nFrankenPHP usa el intérprete oficial de PHP.\nTodas las optimizaciones de rendimiento habituales relacionadas con PHP se aplican con FrankenPHP.\n\nEn particular:\n\n- verifica que [OPcache](https://www.php.net/manual/es/book.opcache.php) esté instalado, habilitado y correctamente configurado\n- habilita [optimizaciones del autoload de Composer](https://getcomposer.org/doc/articles/autoloader-optimization.md)\n- asegúrate de que la caché `realpath` sea lo suficientemente grande para las necesidades de tu aplicación\n- usa [preloading](https://www.php.net/manual/es/opcache.preloading.php)\n\nPara más detalles, lee [la entrada de documentación dedicada de Symfony](https://symfony.com/doc/current/performance.html)\n(la mayoría de los consejos son útiles incluso si no usas Symfony).\n\n## Dividiendo el Pool de Hilos\n\nEs común que las aplicaciones interactúen con servicios externos lentos, como una\nAPI que tiende a ser poco confiable bajo alta carga o que consistentemente tarda 10+ segundos en responder.\nEn tales casos, puede ser beneficioso dividir el pool de hilos para tener pools \"lentos\" dedicados.\nEsto evita que los endpoints lentos consuman todos los recursos/hilos del servidor y\nlimita la concurrencia de solicitudes hacia el endpoint lento, similar a un\npool de conexiones.\n\n```caddyfile\n{\n    frankenphp {\n        max_threads 100 # máximo 100 hilos compartidos por todos los workers\n    }\n}\n\nejemplo.com {\n    php_server {\n        root /app/public # la raíz de tu aplicación\n        worker index.php {\n            match /endpoint-lento/* # todas las solicitudes con la ruta /endpoint-lento/* son manejadas por este pool de hilos\n            num 10 # mínimo 10 hilos para solicitudes que coincidan con /endpoint-lento/*\n        }\n        worker index.php {\n            match * # todas las demás solicitudes son manejadas por separado\n            num 20 # mínimo 20 hilos para otras solicitudes, incluso si los endpoints lentos comienzan a colgarse\n        }\n    }\n}\n```\n\nEn general, también es aconsejable manejar endpoints muy lentos de manera asíncrona, utilizando mecanismos relevantes como colas de mensajes.\n"
  },
  {
    "path": "docs/es/production.md",
    "content": "# Despliegue en Producción\n\nEn este tutorial, aprenderemos cómo desplegar una aplicación PHP en un único servidor usando Docker Compose.\n\nSi estás usando Symfony, consulta la documentación \"[Despliegue en producción](https://github.com/dunglas/symfony-docker/blob/main/docs/production.md)\" del proyecto Symfony Docker (que usa FrankenPHP).\n\nSi estás usando API Platform (que también usa FrankenPHP), consulta [la documentación de despliegue del framework](https://api-platform.com/docs/deployment/).\n\n## Preparando tu Aplicación\n\nPrimero, crea un archivo `Dockerfile` en el directorio raíz de tu proyecto PHP:\n\n```dockerfile\nFROM dunglas/frankenphp\n\n# Asegúrate de reemplazar \"tu-dominio.ejemplo.com\" por tu nombre de dominio\nENV SERVER_NAME=tu-dominio.ejemplo.com\n# Si quieres deshabilitar HTTPS, usa este valor en su lugar:\n#ENV SERVER_NAME=:80\n\n# Si tu proyecto no usa el directorio \"public\" como raíz web, puedes establecerlo aquí:\n# ENV SERVER_ROOT=web/\n\n# Habilitar configuración de producción de PHP\nRUN mv \"$PHP_INI_DIR/php.ini-production\" \"$PHP_INI_DIR/php.ini\"\n\n# Copiar los archivos PHP de tu proyecto en el directorio público\nCOPY . /app/public\n# Si usas Symfony o Laravel, necesitas copiar todo el proyecto en su lugar:\n#COPY . /app\n```\n\nConsulta \"[Construyendo una Imagen Docker Personalizada](docker.md)\" para más detalles y opciones,\ny para aprender cómo personalizar la configuración, instalar extensiones PHP y módulos de Caddy.\n\nSi tu proyecto usa Composer,\nasegúrate de incluirlo en la imagen Docker e instalar tus dependencias.\n\nLuego, agrega un archivo `compose.yaml`:\n\n```yaml\nservices:\n  php:\n    image: dunglas/frankenphp\n    restart: always\n    ports:\n      - \"80:80\" # HTTP\n      - \"443:443\" # HTTPS\n      - \"443:443/udp\" # HTTP/3\n    volumes:\n      - caddy_data:/data\n      - caddy_config:/config\n\n# Volúmenes necesarios para los certificados y configuración de Caddy\nvolumes:\n  caddy_data:\n  caddy_config:\n```\n\n> [!NOTE]\n>\n> Los ejemplos anteriores están destinados a uso en producción.\n> En desarrollo, es posible que desees usar un volumen, una configuración PHP diferente y un valor diferente para la variable de entorno `SERVER_NAME`.\n>\n> Consulta el proyecto [Symfony Docker](https://github.com/dunglas/symfony-docker)\n> (que usa FrankenPHP) para un ejemplo más avanzado usando imágenes multi-etapa,\n> Composer, extensiones PHP adicionales, etc.\n\nFinalmente, si usas Git, haz commit de estos archivos y haz push.\n\n## Preparando un Servidor\n\nPara desplegar tu aplicación en producción, necesitas un servidor.\nEn este tutorial, usaremos una máquina virtual proporcionada por DigitalOcean, pero cualquier servidor Linux puede funcionar.\nSi ya tienes un servidor Linux con Docker instalado, puedes saltar directamente a [la siguiente sección](#configurando-un-nombre-de-dominio).\n\nDe lo contrario, usa [este enlace de afiliado](https://m.do.co/c/5d8aabe3ab80) para obtener $200 de crédito gratuito, crea una cuenta y luego haz clic en \"Crear un Droplet\".\nLuego, haz clic en la pestaña \"Marketplace\" bajo la sección \"Elegir una imagen\" y busca la aplicación llamada \"Docker\".\nEsto aprovisionará un servidor Ubuntu con las últimas versiones de Docker y Docker Compose ya instaladas.\n\nPara fines de prueba, los planes más económicos serán suficientes.\nPara un uso real en producción, probablemente querrás elegir un plan en la sección \"uso general\" que se adapte a tus necesidades.\n\n![Desplegando FrankenPHP en DigitalOcean con Docker](digitalocean-droplet.png)\n\nPuedes mantener los valores predeterminados para otras configuraciones o ajustarlos según tus necesidades.\nNo olvides agregar tu clave SSH o crear una contraseña y luego presionar el botón \"Finalizar y crear\".\n\nLuego, espera unos segundos mientras se aprovisiona tu Droplet.\nCuando tu Droplet esté listo, usa SSH para conectarte:\n\n```console\nssh root@<ip-del-droplet>\n```\n\n## Configurando un Nombre de Dominio\n\nEn la mayoría de los casos, querrás asociar un nombre de dominio a tu sitio.\nSi aún no tienes un nombre de dominio, deberás comprar uno a través de un registrador.\n\nLuego, crea un registro DNS de tipo `A` para tu nombre de dominio que apunte a la dirección IP de tu servidor:\n\n```dns\ntu-dominio.ejemplo.com.  IN  A     207.154.233.113\n```\n\nEjemplo con el servicio de Dominios de DigitalOcean (\"Redes\" > \"Dominios\"):\n\n![Configurando DNS en DigitalOcean](../digitalocean-dns.png)\n\n> [!NOTE]\n>\n> Let's Encrypt, el servicio utilizado por defecto por FrankenPHP para generar automáticamente un certificado TLS, no soporta el uso de direcciones IP puras. Usar un nombre de dominio es obligatorio para usar Let's Encrypt.\n\n## Despliegue\n\nCopia tu proyecto en el servidor usando `git clone`, `scp` o cualquier otra herramienta que se ajuste a tus necesidades.\nSi usas GitHub, es posible que desees usar [una clave de despliegue](https://docs.github.com/es/free-pro-team@latest/developers/overview/managing-deploy-keys#deploy-keys).\nLas claves de despliegue también son [soportadas por GitLab](https://docs.gitlab.com/ee/user/project/deploy_keys/).\n\nEjemplo con Git:\n\n```console\ngit clone git@github.com:<usuario>/<nombre-proyecto>.git\n```\n\nVe al directorio que contiene tu proyecto (`<nombre-proyecto>`) e inicia la aplicación en modo producción:\n\n```console\ndocker compose up --wait\n```\n\nTu servidor está en funcionamiento y se ha generado automáticamente un certificado HTTPS para ti.\nVe a `https://tu-dominio.ejemplo.com` y ¡disfruta!\n\n> [!CAUTION]\n>\n> Docker puede tener una capa de caché, asegúrate de tener la compilación correcta para cada despliegue o vuelve a compilar tu proyecto con la opción `--no-cache` para evitar problemas de caché.\n\n## Despliegue en Múltiples Nodos\n\nSi deseas desplegar tu aplicación en un clúster de máquinas, puedes usar [Docker Swarm](https://docs.docker.com/engine/swarm/stack-deploy/),\nque es compatible con los archivos Compose proporcionados.\nPara desplegar en Kubernetes, consulta [el gráfico Helm proporcionado con API Platform](https://api-platform.com/docs/deployment/kubernetes/), que usa FrankenPHP.\n"
  },
  {
    "path": "docs/es/static.md",
    "content": "# Crear una Compilación Estática\n\nEn lugar de usar una instalación local de la biblioteca PHP,\nes posible crear una compilación estática o mayormente estática de FrankenPHP gracias al excelente [proyecto static-php-cli](https://github.com/crazywhalecc/static-php-cli) (a pesar de su nombre, este proyecto soporta todas las SAPI, no solo CLI).\n\nCon este método, un único binario portátil contendrá el intérprete de PHP, el servidor web Caddy y FrankenPHP.\n\nLos ejecutables nativos completamente estáticos no requieren dependencias y pueden ejecutarse incluso en la imagen Docker [`scratch`](https://docs.docker.com/build/building/base-images/#create-a-minimal-base-image-using-scratch).\nSin embargo, no pueden cargar extensiones PHP dinámicas (como Xdebug) y tienen algunas limitaciones porque usan la libc musl.\n\nLos binarios mayormente estáticos solo requieren `glibc` y pueden cargar extensiones dinámicas.\n\nCuando sea posible, recomendamos usar compilaciones mayormente estáticas basadas en glibc.\n\nFrankenPHP también soporta [incrustar la aplicación PHP en el binario estático](embed.md).\n\n## Linux\n\nProporcionamos imágenes Docker para compilar binarios Linux estáticos:\n\n### Compilación Completamente Estática Basada en musl\n\nPara un binario completamente estático que se ejecuta en cualquier distribución Linux sin dependencias pero que no soporta carga dinámica de extensiones:\n\n```console\ndocker buildx bake --load static-builder-musl\ndocker cp $(docker create --name static-builder-musl dunglas/frankenphp:static-builder-musl):/go/src/app/dist/frankenphp-linux-$(uname -m) frankenphp ; docker rm static-builder-musl\n```\n\nPara un mejor rendimiento en escenarios altamente concurrentes, considera usar el asignador [mimalloc](https://github.com/microsoft/mimalloc).\n\n```console\ndocker buildx bake --load --set static-builder-musl.args.MIMALLOC=1 static-builder-musl\n```\n\n### Compilación Mayormente Estática Basada en glibc (Con Soporte para Extensiones Dinámicas)\n\nPara un binario que soporta la carga dinámica de extensiones PHP mientras tiene las extensiones seleccionadas compiladas estáticamente:\n\n```console\ndocker buildx bake --load static-builder-gnu\ndocker cp $(docker create --name static-builder-gnu dunglas/frankenphp:static-builder-gnu):/go/src/app/dist/frankenphp-linux-$(uname -m) frankenphp ; docker rm static-builder-gnu\n```\n\nEste binario soporta todas las versiones de glibc 2.17 y superiores, pero no se ejecuta en sistemas basados en musl (como Alpine Linux).\n\nEl binario resultante (mayormente estático excepto por `glibc`) se llama `frankenphp` y está disponible en el directorio actual.\n\nSi deseas compilar el binario estático sin Docker, consulta las instrucciones para macOS, que también funcionan para Linux.\n\n### Extensiones Personalizadas\n\nPor omisión, se compilan las extensiones PHP más populares.\n\nPara reducir el tamaño del binario y disminuir la superficie de ataque, puedes elegir la lista de extensiones a compilar usando el ARG de Docker `PHP_EXTENSIONS`.\n\nPor ejemplo, ejecuta el siguiente comando para compilar solo la extensión `opcache`:\n\n```console\ndocker buildx bake --load --set static-builder-musl.args.PHP_EXTENSIONS=opcache,pdo_sqlite static-builder-musl\n# ...\n```\n\nPara agregar bibliotecas que habiliten funcionalidades adicionales a las extensiones que has habilitado, puedes pasar el ARG de Docker `PHP_EXTENSION_LIBS`:\n\n```console\ndocker buildx bake \\\n  --load \\\n  --set static-builder-musl.args.PHP_EXTENSIONS=gd \\\n  --set static-builder-musl.args.PHP_EXTENSION_LIBS=libjpeg,libwebp \\\n  static-builder-musl\n```\n\n### Módulos Adicionales de Caddy\n\nPara agregar módulos adicionales de Caddy o pasar otros argumentos a [xcaddy](https://github.com/caddyserver/xcaddy), usa el ARG de Docker `XCADDY_ARGS`:\n\n```console\ndocker buildx bake \\\n  --load \\\n  --set static-builder-musl.args.XCADDY_ARGS=\"--with github.com/darkweak/souin/plugins/caddy --with github.com/dunglas/caddy-cbrotli --with github.com/dunglas/mercure/caddy --with github.com/dunglas/vulcain/caddy\" \\\n  static-builder-musl\n```\n\nEn este ejemplo, agregamos el módulo de caché HTTP [Souin](https://souin.io) para Caddy, así como los módulos [cbrotli](https://github.com/dunglas/caddy-cbrotli), [Mercure](https://mercure.rocks) y [Vulcain](https://vulcain.rocks).\n\n> [!TIP]\n>\n> Los módulos cbrotli, Mercure y Vulcain están incluidos por omisión si `XCADDY_ARGS` está vacío o no está configurado.\n> Si personalizas el valor de `XCADDY_ARGS`, debes incluirlos explícitamente si deseas que estén incluidos.\n\nConsulta también cómo [personalizar la compilación](#personalizando-la-compilación).\n\n### Token de GitHub\n\nSi alcanzas el límite de tasa de la API de GitHub, establece un Token de Acceso Personal de GitHub en una variable de entorno llamada `GITHUB_TOKEN`:\n\n```console\nGITHUB_TOKEN=\"xxx\" docker --load buildx bake static-builder-musl\n# ...\n```\n\n## macOS\n\nEjecuta el siguiente script para crear un binario estático para macOS (debes tener [Homebrew](https://brew.sh/) instalado):\n\n```console\ngit clone https://github.com/php/frankenphp\ncd frankenphp\n./build-static.sh\n```\n\nNota: este script también funciona en Linux (y probablemente en otros Unix) y es usado internamente por las imágenes Docker que proporcionamos.\n\n## Personalizando la Compilación\n\nLas siguientes variables de entorno pueden pasarse a `docker build` y al script `build-static.sh` para personalizar la compilación estática:\n\n- `FRANKENPHP_VERSION`: la versión de FrankenPHP a usar\n- `PHP_VERSION`: la versión de PHP a usar\n- `PHP_EXTENSIONS`: las extensiones PHP a compilar ([lista de extensiones soportadas](https://static-php.dev/en/guide/extensions.html))\n- `PHP_EXTENSION_LIBS`: bibliotecas adicionales a compilar que añaden funcionalidades a las extensiones\n- `XCADDY_ARGS`: argumentos a pasar a [xcaddy](https://github.com/caddyserver/xcaddy), por ejemplo para agregar módulos adicionales de Caddy\n- `EMBED`: ruta de la aplicación PHP a incrustar en el binario\n- `CLEAN`: cuando está establecido, libphp y todas sus dependencias se compilan desde cero (sin caché)\n- `NO_COMPRESS`: no comprimir el binario resultante usando UPX\n- `DEBUG_SYMBOLS`: cuando está establecido, los símbolos de depuración no se eliminarán y se añadirán al binario\n- `MIMALLOC`: (experimental, solo Linux) reemplaza mallocng de musl por [mimalloc](https://github.com/microsoft/mimalloc) para mejorar el rendimiento. Solo recomendamos usar esto para compilaciones orientadas a musl; para glibc, preferimos deshabilitar esta opción y usar [`LD_PRELOAD`](https://microsoft.github.io/mimalloc/overrides.html) cuando ejecutes tu binario.\n- `RELEASE`: (solo para mantenedores) cuando está establecido, el binario resultante se subirá a GitHub\n\n## Extensiones\n\nCon los binarios basados en glibc o macOS, puedes cargar extensiones PHP dinámicamente. Sin embargo, estas extensiones deberán ser compiladas con soporte ZTS.\nDado que la mayoría de los gestores de paquetes no ofrecen actualmente versiones ZTS de sus extensiones, tendrás que compilarlas tú mismo.\n\nPara esto, puedes compilar y ejecutar el contenedor Docker `static-builder-gnu`, acceder a él y compilar las extensiones con `./configure --with-php-config=/go/src/app/dist/static-php-cli/buildroot/bin/php-config`.\n\nPasos de ejemplo para [la extensión Xdebug](https://xdebug.org):\n\n```console\ndocker build -t gnu-ext -f static-builder-gnu.Dockerfile --build-arg FRANKENPHP_VERSION=1.0 .\ndocker create --name static-builder-gnu -it gnu-ext /bin/sh\ndocker start static-builder-gnu\ndocker exec -it static-builder-gnu /bin/sh\ncd /go/src/app/dist/static-php-cli/buildroot/bin\ngit clone https://github.com/xdebug/xdebug.git && cd xdebug\nsource scl_source enable devtoolset-10\n../phpize\n./configure --with-php-config=/go/src/app/dist/static-php-cli/buildroot/bin/php-config\nmake\nexit\ndocker cp static-builder-gnu:/go/src/app/dist/static-php-cli/buildroot/bin/xdebug/modules/xdebug.so xdebug-zts.so\ndocker cp static-builder-gnu:/go/src/app/dist/frankenphp-linux-$(uname -m) ./frankenphp\ndocker stop static-builder-gnu\ndocker rm static-builder-gnu\ndocker rmi gnu-ext\n```\n\nEsto creará `frankenphp` y `xdebug-zts.so` en el directorio actual.\nSi mueves `xdebug-zts.so` a tu directorio de extensiones, agrega `zend_extension=xdebug-zts.so` a tu php.ini y ejecuta FrankenPHP, cargará Xdebug.\n"
  },
  {
    "path": "docs/es/wordpress.md",
    "content": "# WordPress\n\nEjecute [WordPress](https://wordpress.org/) con FrankenPHP para disfrutar de una pila moderna y de alto rendimiento con HTTPS automático, HTTP/3 y compresión Zstandard.\n\n## Instalación Mínima\n\n1. [Descargue WordPress](https://wordpress.org/download/)\n2. Extraiga el archivo ZIP y abra una terminal en el directorio extraído\n3. Ejecute:\n\n   ```console\n   frankenphp php-server\n   ```\n\n4. Vaya a `http://localhost/wp-admin/` y siga las instrucciones de instalación\n5. ¡Listo!\n\nPara una configuración lista para producción, prefiera usar `frankenphp run` con un `Caddyfile` como este:\n\n```caddyfile\nexample.com\n\nphp_server\nencode zstd br gzip\nlog\n```\n\n## Hot Reload\n\nPara usar la función de [Hot reload](hot-reload.md) con WordPress, active [Mercure](mercure.md) y agregue la subdirectiva `hot_reload` a la directiva `php_server` en su `Caddyfile`:\n\n```caddyfile\nlocalhost\n\nmercure {\n    anonymous\n}\n\nphp_server {\n    hot_reload\n}\n```\n\nLuego, agregue el código necesario para cargar las bibliotecas JavaScript en el archivo `functions.php` de su tema de WordPress:\n\n```php\nfunction hot_reload() {\n    ?>\n    <?php if (isset($_SERVER['FRANKENPHP_HOT_RELOAD'])): ?>\n        <meta name=\"frankenphp-hot-reload:url\" content=\"<?=$_SERVER['FRANKENPHP_HOT_RELOAD']?>\">\n        <script src=\"https://cdn.jsdelivr.net/npm/idiomorph\"></script>\n        <script src=\"https://cdn.jsdelivr.net/npm/frankenphp-hot-reload/+esm\" type=\"module\"></script>\n    <?php endif ?>\n    <?php\n}\nadd_action('wp_head', 'hot_reload');\n```\n\nFinalmente, ejecute `frankenphp run` desde el directorio raíz de WordPress.\n"
  },
  {
    "path": "docs/es/worker.md",
    "content": "# Usando los Workers de FrankenPHP\n\nInicia tu aplicación una vez y manténla en memoria.\nFrankenPHP gestionará las peticiones entrantes en unos pocos milisegundos.\n\n## Iniciando Scripts de Worker\n\n### Docker\n\nEstablece el valor de la variable de entorno `FRANKENPHP_CONFIG` a `worker /ruta/a/tu/script/worker.php`:\n\n```console\ndocker run \\\n    -e FRANKENPHP_CONFIG=\"worker /app/ruta/a/tu/script/worker.php\" \\\n    -v $PWD:/app \\\n    -p 80:80 -p 443:443 -p 443:443/udp \\\n    dunglas/frankenphp\n```\n\n### Binario Autónomo\n\nUsa la opción `--worker` del comando `php-server` para servir el contenido del directorio actual usando un worker:\n\n```console\nfrankenphp php-server --worker /ruta/a/tu/script/worker.php\n```\n\nSi tu aplicación PHP está [incrustada en el binario](embed.md), puedes agregar un `Caddyfile` personalizado en el directorio raíz de la aplicación.\nSerá usado automáticamente.\n\nTambién es posible [reiniciar el worker al detectar cambios en archivos](config.md#watching-for-file-changes) con la opción `--watch`.\nEl siguiente comando activará un reinicio si algún archivo que termine en `.php` en el directorio `/ruta/a/tu/app/` o sus subdirectorios es modificado:\n\n```console\nfrankenphp php-server --worker /ruta/a/tu/script/worker.php --watch=\"/ruta/a/tu/app/**/*.php\"\n```\n\nEsta función se utiliza frecuentemente en combinación con [hot reloading](hot-reload.md).\n\n## Symfony Runtime\n\n> [!TIP]\n> La siguiente sección es necesaria solo para versiones anteriores a Symfony 7.4, donde se introdujo soporte nativo para el modo worker de FrankenPHP.\n\nEl modo worker de FrankenPHP es soportado por el [Componente Runtime de Symfony](https://symfony.com/doc/current/components/runtime.html).\nPara iniciar cualquier aplicación Symfony en un worker, instala el paquete FrankenPHP de [PHP Runtime](https://github.com/php-runtime/runtime):\n\n```console\ncomposer require runtime/frankenphp-symfony\n```\n\nInicia tu servidor de aplicación definiendo la variable de entorno `APP_RUNTIME` para usar el Runtime de FrankenPHP Symfony:\n\n```console\ndocker run \\\n    -e FRANKENPHP_CONFIG=\"worker ./public/index.php\" \\\n    -e APP_RUNTIME=Runtime\\\\FrankenPhpSymfony\\\\Runtime \\\n    -v $PWD:/app \\\n    -p 80:80 -p 443:443 -p 443:443/udp \\\n    dunglas/frankenphp\n```\n\n## Laravel Octane\n\nConsulta [la documentación dedicada](laravel.md#laravel-octane).\n\n## Aplicaciones Personalizadas\n\nEl siguiente ejemplo muestra cómo crear tu propio script de worker sin depender de una biblioteca de terceros:\n\n```php\n<?php\n// public/index.php\n\n// Prevenir la terminación del script de worker cuando una conexión de cliente se interrumpe\nignore_user_abort(true);\n\n// Iniciar tu aplicación\nrequire __DIR__.'/vendor/autoload.php';\n\n$myApp = new \\App\\Kernel();\n$myApp->boot();\n\n// Manejador fuera del bucle para mejor rendimiento (menos trabajo)\n$handler = static function () use ($myApp) {\n    try {\n        // Llamado cuando se recibe una petición,\n        // las superglobales, php://input y similares se reinician\n        echo $myApp->handle($_GET, $_POST, $_COOKIE, $_FILES, $_SERVER);\n    } catch (\\Throwable $exception) {\n        // `set_exception_handler` se llama solo cuando el script de worker termina,\n        // lo cual puede no ser lo que esperas, así que captura y maneja excepciones aquí\n        (new \\MyCustomExceptionHandler)->handleException($exception);\n    }\n};\n\n$maxRequests = (int)($_SERVER['MAX_REQUESTS'] ?? 0);\nfor ($nbRequests = 0; !$maxRequests || $nbRequests < $maxRequests; ++$nbRequests) {\n    $keepRunning = \\frankenphp_handle_request($handler);\n\n    // Haz algo después de enviar la respuesta HTTP\n    $myApp->terminate();\n\n    // Llama al recolector de basura para reducir las posibilidades de que se active en medio de la generación de una página\n    gc_collect_cycles();\n\n    if (!$keepRunning) break;\n}\n\n// Limpieza\n$myApp->shutdown();\n```\n\nLuego, inicia tu aplicación y usa la variable de entorno `FRANKENPHP_CONFIG` para configurar tu worker:\n\n```console\ndocker run \\\n    -e FRANKENPHP_CONFIG=\"worker ./public/index.php\" \\\n    -v $PWD:/app \\\n    -p 80:80 -p 443:443 -p 443:443/udp \\\n    dunglas/frankenphp\n```\n\nPor omisión, se inician 2 workers por CPU.\nTambién puedes configurar el número de workers a iniciar:\n\n```console\ndocker run \\\n    -e FRANKENPHP_CONFIG=\"worker ./public/index.php 42\" \\\n    -v $PWD:/app \\\n    -p 80:80 -p 443:443 -p 443:443/udp \\\n    dunglas/frankenphp\n```\n\n### Reiniciar el Worker Después de un Número Determinado de Peticiones\n\nComo PHP no fue diseñado originalmente para procesos de larga duración, aún hay muchas bibliotecas y códigos heredados que generan fugas de memoria.\nUna solución para usar este tipo de código en modo worker es reiniciar el script de worker después de procesar un cierto número de peticiones:\n\nEl fragmento de worker anterior permite configurar un número máximo de peticiones a manejar estableciendo una variable de entorno llamada `MAX_REQUESTS`.\n\n### Reiniciar Workers Manualmente\n\nAunque es posible reiniciar workers [al detectar cambios en archivos](config.md#watching-for-file-changes), también es posible reiniciar todos los workers\nde manera controlada a través de la [API de administración de Caddy](https://caddyserver.com/docs/api). Si el admin está habilitado en tu\n[Caddyfile](config.md#caddyfile-config), puedes activar el endpoint de reinicio con una simple petición POST como esta:\n\n```console\ncurl -X POST http://localhost:2019/frankenphp/workers/restart\n```\n\n### Fallos en Workers\n\nSi un script de worker falla con un código de salida distinto de cero, FrankenPHP lo reiniciará con una estrategia de retroceso exponencial.\nSi el script de worker permanece activo más tiempo que el último retroceso * 2,\nno penalizará al script de worker y lo reiniciará nuevamente.\nSin embargo, si el script de worker continúa fallando con un código de salida distinto de cero en un corto período de tiempo\n(por ejemplo, tener un error tipográfico en un script), FrankenPHP fallará con el error: `too many consecutive failures`.\n\nEl número de fallos consecutivos puede configurarse en tu [Caddyfile](config.md#caddyfile-config) con la opción `max_consecutive_failures`:\n\n```caddyfile\nfrankenphp {\n    worker {\n        # ...\n        max_consecutive_failures 10\n    }\n}\n```\n\n## Comportamiento de las Superglobales\n\nLas [superglobales de PHP](https://www.php.net/manual/es/language.variables.superglobals.php) (`$_SERVER`, `$_ENV`, `$_GET`...)\nse comportan de la siguiente manera:\n\n- antes de la primera llamada a `frankenphp_handle_request()`, las superglobales contienen valores vinculados al script de worker en sí\n- durante y después de la llamada a `frankenphp_handle_request()`, las superglobales contienen valores generados a partir de la petición HTTP procesada; cada llamada a `frankenphp_handle_request()` cambia los valores de las superglobales\n\nPara acceder a las superglobales del script de worker dentro de la retrollamada, debes copiarlas e importar la copia en el ámbito de la retrollamada:\n\n```php\n<?php\n// Copia la superglobal $_SERVER del worker antes de la primera llamada a frankenphp_handle_request()\n$workerServer = $_SERVER;\n\n$handler = static function () use ($workerServer) {\n    var_dump($_SERVER); // $_SERVER vinculado a la petición\n    var_dump($workerServer); // $_SERVER del script de worker\n};\n\n// ...\n```\n"
  },
  {
    "path": "docs/es/x-sendfile.md",
    "content": "# Sirviendo archivos estáticos grandes de manera eficiente (`X-Sendfile`/`X-Accel-Redirect`)\n\nNormalmente, los archivos estáticos pueden ser servidos directamente por el servidor web,\npero a veces es necesario ejecutar código PHP antes de enviarlos:\ncontrol de acceso, estadísticas, encabezados HTTP personalizados...\n\nDesafortunadamente, usar PHP para servir archivos estáticos grandes es ineficiente en comparación con\nel uso directo del servidor web (sobrecarga de memoria, rendimiento reducido...).\n\nFrankenPHP permite delegar el envío de archivos estáticos al servidor web\n**después** de ejecutar código PHP personalizado.\n\nPara hacerlo, tu aplicación PHP simplemente necesita definir un encabezado HTTP personalizado\nque contenga la ruta del archivo a servir. FrankenPHP se encarga del resto.\n\nEsta funcionalidad es conocida como **`X-Sendfile`** para Apache y **`X-Accel-Redirect`** para NGINX.\n\nEn los siguientes ejemplos, asumimos que el directorio raíz del proyecto es `public/`\ny que queremos usar PHP para servir archivos almacenados fuera del directorio `public/`,\ndesde un directorio llamado `private-files/`.\n\n## Configuración\n\nPrimero, agrega la siguiente configuración a tu `Caddyfile` para habilitar esta funcionalidad:\n\n```patch\n\troot public/\n\t# ...\n\n+\t# Necesario para Symfony, Laravel y otros proyectos que usan el componente Symfony HttpFoundation\n+\trequest_header X-Sendfile-Type x-accel-redirect\n+\trequest_header X-Accel-Mapping ../private-files=/private-files\n+\n+\tintercept {\n+\t\t@accel header X-Accel-Redirect *\n+\t\thandle_response @accel {\n+\t\t\troot private-files/\n+\t\t\trewrite * {resp.header.X-Accel-Redirect}\n+\t\t\tmethod * GET\n+\n+\t\t\t# Elimina el encabezado X-Accel-Redirect establecido por PHP para mayor seguridad\n+\t\t\theader -X-Accel-Redirect\n+\n+\t\t\tfile_server\n+\t\t}\n+\t}\n\n\tphp_server\n```\n\n## PHP puro\n\nEstablece la ruta relativa del archivo (desde `private-files/`) como valor del encabezado `X-Accel-Redirect`:\n\n```php\nheader('X-Accel-Redirect: file.txt');\n```\n\n## Proyectos que usan el componente Symfony HttpFoundation (Symfony, Laravel, Drupal...)\n\nSymfony HttpFoundation [soporta nativamente esta funcionalidad](https://symfony.com/doc/current/components/http_foundation.html#serving-files).\nDeterminará automáticamente el valor correcto para el encabezado `X-Accel-Redirect` y lo agregará a la respuesta.\n\n```php\nuse Symfony\\Component\\HttpFoundation\\BinaryFileResponse;\n\nBinaryFileResponse::trustXSendfileTypeHeader();\n$response = new BinaryFileResponse(__DIR__.'/../private-files/file.txt');\n\n// ...\n```\n"
  },
  {
    "path": "docs/extension-workers.md",
    "content": "# Extension Workers\n\nExtension Workers enable your [FrankenPHP extension](https://frankenphp.dev/docs/extensions/) to manage a dedicated pool of PHP threads for executing background tasks, handling asynchronous events, or implementing custom protocols. Useful for queue systems, event listeners, schedulers, etc.\n\n## Registering the Worker\n\n### Static Registration\n\nIf you don't need to make the worker configurable by the user (fixed script path, fixed number of threads), you can simply register the worker in the `init()` function.\n\n```go\npackage myextension\n\nimport (\n\t\"github.com/dunglas/frankenphp\"\n\t\"github.com/dunglas/frankenphp/caddy\"\n)\n\n// Global handle to communicate with the worker pool\nvar worker frankenphp.Workers\n\nfunc init() {\n\t// Register the worker when the module is loaded.\n\tworker = caddy.RegisterWorkers(\n\t\t\"my-internal-worker\", // Unique name\n\t\t\"worker.php\",         // Script path (relative to execution or absolute)\n\t\t2,                    // Fixed Thread count\n\t\t// Optional Lifecycle Hooks\n\t\tfrankenphp.WithWorkerOnServerStartup(func() {\n\t\t\t// Global setup logic...\n\t\t}),\n\t)\n}\n```\n\n### In a Caddy Module (Configurable by the user)\n\nIf you plan to share your extension (like a generic queue or event listener), you should wrap it in a Caddy module. This allows users to configure the script path and thread count via their `Caddyfile`. This requires implementing the `caddy.Provisioner` interface and parsing the Caddyfile ([see an example](https://github.com/dunglas/frankenphp-queue/blob/989120d394d66dd6c8e2101cac73dd622fade334/caddy.go)).\n\n### In a Pure Go Application (Embedding)\n\nIf you are [embedding FrankenPHP in a standard Go application without caddy](https://pkg.go.dev/github.com/dunglas/frankenphp#example-ServeHTTP), you can register extension workers using `frankenphp.WithExtensionWorkers` when initializing options.\n\n## Interacting with Workers\n\nOnce the worker pool is active, you can dispatch tasks to it. This can be done inside [native functions exported to PHP](https://frankenphp.dev/docs/extensions/#writing-the-extension), or from any Go logic such as a cron scheduler, an event listener (MQTT, Kafka), or a any other goroutine.\n\n### Headless Mode : `SendMessage`\n\nUse `SendMessage` to pass raw data directly to your worker script. This is ideal for queues or simple commands.\n\n#### Example: An Async Queue Extension\n\n```go\n// #include <Zend/zend_types.h>\nimport \"C\"\nimport (\n\t\"context\"\n\t\"unsafe\"\n\t\"github.com/dunglas/frankenphp\"\n)\n\n//export_php:function my_queue_push(mixed $data): bool\nfunc my_queue_push(data *C.zval) bool {\n\t// 1. Ensure worker is ready\n\tif worker == nil {\n\t\treturn false\n\t}\n\n\t// 2. Dispatch to the background worker\n\t_, err := worker.SendMessage(\n\t\tcontext.Background(), // Standard Go context\n\t\tunsafe.Pointer(data), // Data to pass to the worker\n\t\tnil, // Optional http.ResponseWriter\n\t)\n\n\treturn err == nil\n}\n```\n\n### HTTP Emulation :`SendRequest`\n\nUse `SendRequest` if your extension needs to invoke a PHP script that expects a standard web environment (populating `$_SERVER`, `$_GET`, etc.).\n\n```go\n// #include <Zend/zend_types.h>\nimport \"C\"\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"unsafe\"\n\t\"github.com/dunglas/frankenphp\"\n)\n\n//export_php:function my_worker_http_request(string $path): string\nfunc my_worker_http_request(path *C.zend_string) unsafe.Pointer {\n\t// 1. Prepare the request and recorder\n\turl := frankenphp.GoString(unsafe.Pointer(path))\n\treq, _ := http.NewRequest(\"GET\", url, http.NoBody)\n\trr := httptest.NewRecorder()\n\n\t// 2. Dispatch to the worker\n\tif err := worker.SendRequest(rr, req); err != nil {\n\t\treturn nil\n\t}\n\n\t// 3. Return the captured response\n\treturn frankenphp.PHPString(rr.Body.String(), false)\n}\n```\n\n## Worker Script\n\nThe PHP worker script runs in a loop and can handle both raw messages and HTTP requests.\n\n```php\n<?php\n// Handle both raw messages and HTTP requests in the same loop\n$handler = function ($payload = null) {\n    // Case 1: Message Mode\n    if ($payload !== null) {\n        return \"Received payload: \" . $payload;\n    }\n\n    // Case 2: HTTP Mode (standard PHP superglobals are populated)\n    echo \"Hello from page: \" . $_SERVER['REQUEST_URI'];\n};\n\nwhile (frankenphp_handle_request($handler)) {\n    gc_collect_cycles();\n}\n```\n\n## Lifecycle Hooks\n\nFrankenPHP provides hooks to execute Go code at specific points in the lifecycle.\n\n| Hook Type  | Option Name                  | Signature            | Context & Use Case                                                     |\n| :--------- | :--------------------------- | :------------------- | :--------------------------------------------------------------------- |\n| **Server** | `WithWorkerOnServerStartup`  | `func()`             | Global setup. Run **Once**. Example: Connect to NATS/Redis.            |\n| **Server** | `WithWorkerOnServerShutdown` | `func()`             | Global cleanup. Run **Once**. Example: Close shared connections.       |\n| **Thread** | `WithWorkerOnReady`          | `func(threadID int)` | Per-thread setup. Called when a thread starts. Receives the Thread ID. |\n| **Thread** | `WithWorkerOnShutdown`       | `func(threadID int)` | Per-thread cleanup. Receives the Thread ID.                            |\n\n### Example\n\n```go\npackage myextension\n\nimport (\n    \"fmt\"\n    \"github.com/dunglas/frankenphp\"\n    frankenphpCaddy \"github.com/dunglas/frankenphp/caddy\"\n)\n\nfunc init() {\n    workerHandle = frankenphpCaddy.RegisterWorkers(\n        \"my-worker\", \"worker.php\", 2,\n\n        // Server Startup (Global)\n        frankenphp.WithWorkerOnServerStartup(func() {\n            fmt.Println(\"Extension: Server starting up...\")\n        }),\n\n        // Thread Ready (Per Thread)\n        // Note: The function accepts an integer representing the Thread ID\n        frankenphp.WithWorkerOnReady(func(id int) {\n            fmt.Printf(\"Extension: Worker thread #%d is ready.\\n\", id)\n        }),\n    )\n}\n```\n"
  },
  {
    "path": "docs/extensions.md",
    "content": "# Writing PHP Extensions in Go\n\nWith FrankenPHP, you can **write PHP extensions in Go**, which allows you to create **high-performance native functions** that can be called directly from PHP. Your applications can leverage any existing or new Go library, as well as the infamous concurrency model of **goroutines right from your PHP code**.\n\nWriting PHP extensions is typically done in C, but it's also possible to write them in other languages with a bit of extra work. PHP extensions allow you to leverage the power of low-level languages to extend PHP's functionalities, for example, by adding native functions or optimizing specific operations.\n\nThanks to Caddy modules, you can write PHP extensions in Go and integrate them very quickly into FrankenPHP.\n\n## Two Approaches\n\nFrankenPHP provides two ways to create PHP extensions in Go:\n\n1. **Using the Extension Generator** - The recommended approach that generates all necessary boilerplate for most use cases, allowing you to focus on writing your Go code\n2. **Manual Implementation** - Full control over the extension structure for advanced use cases\n\nWe'll start with the generator approach as it's the easiest way to get started, then show the manual implementation for those who need complete control.\n\n## Using the Extension Generator\n\nFrankenPHP is bundled with a tool that allows you **to create a PHP extension** only using Go. **No need to write C code** or use CGO directly: FrankenPHP also includes a **public types API** to help you write your extensions in Go without having to worry about **the type juggling between PHP/C and Go**.\n\n> [!TIP]\n> If you want to understand how extensions can be written in Go from scratch, you can read the manual implementation section below demonstrating how to write a PHP extension in Go without using the generator.\n\nKeep in mind that this tool is **not a full-fledged extension generator**. It is meant to help you write simple extensions in Go, but it does not provide the most advanced features of PHP extensions. If you need to write a more **complex and optimized** extension, you may need to write some C code or use CGO directly.\n\n### Prerequisites\n\nAs covered in the manual implementation section below as well, you need to [get the PHP sources](https://www.php.net/downloads.php) and create a new Go module.\n\n#### Create a New Module and Get PHP Sources\n\nThe first step to writing a PHP extension in Go is to create a new Go module. You can use the following command for this:\n\n```console\ngo mod init example.com/example\n```\n\nThe second step is to [get the PHP sources](https://www.php.net/downloads.php) for the next steps. Once you have them, decompress them into the directory of your choice, not inside your Go module:\n\n```console\ntar xf php-*\n```\n\n### Writing the Extension\n\nEverything is now setup to write your native function in Go. Create a new file named `stringext.go`. Our first function will take a string as an argument, the number of times to repeat it, a boolean to indicate whether to reverse the string, and return the resulting string. This should look like this:\n\n```go\npackage example\n\n// #include <Zend/zend_types.h>\nimport \"C\"\nimport (\n    \"strings\"\n\t\"unsafe\"\n\n\t\"github.com/dunglas/frankenphp\"\n)\n\n//export_php:function repeat_this(string $str, int $count, bool $reverse): string\nfunc repeat_this(s *C.zend_string, count int64, reverse bool) unsafe.Pointer {\n    str := frankenphp.GoString(unsafe.Pointer(s))\n\n    result := strings.Repeat(str, int(count))\n    if reverse {\n        runes := []rune(result)\n        for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {\n            runes[i], runes[j] = runes[j], runes[i]\n        }\n        result = string(runes)\n    }\n\n    return frankenphp.PHPString(result, false)\n}\n```\n\nThere are two important things to note here:\n\n- A directive comment `//export_php:function` defines the function signature in PHP. This is how the generator knows how to generate the PHP function with the right parameters and return type;\n- The function must return an `unsafe.Pointer`. FrankenPHP provides an API to help you with type juggling between C and Go.\n\nWhile the first point speaks for itself, the second may be harder to apprehend. Let's take a deeper dive to type juggling in the next section.\n\n### Type Juggling\n\nWhile some variable types have the same memory representation between C/PHP and Go, some types require more logic to be directly used. This is maybe the hardest part when it comes to writing extensions because it requires understanding internals of the Zend Engine and how variables are stored internally in PHP.\nThis table summarizes what you need to know:\n\n| PHP type           | Go type                       | Direct conversion | C to Go helper                    | Go to C helper                     | Class Methods Support |\n| ------------------ | ----------------------------- | ----------------- | --------------------------------- | ---------------------------------- | --------------------- |\n| `int`              | `int64`                       | ✅                | -                                 | -                                  | ✅                    |\n| `?int`             | `*int64`                      | ✅                | -                                 | -                                  | ✅                    |\n| `float`            | `float64`                     | ✅                | -                                 | -                                  | ✅                    |\n| `?float`           | `*float64`                    | ✅                | -                                 | -                                  | ✅                    |\n| `bool`             | `bool`                        | ✅                | -                                 | -                                  | ✅                    |\n| `?bool`            | `*bool`                       | ✅                | -                                 | -                                  | ✅                    |\n| `string`/`?string` | `*C.zend_string`              | ❌                | `frankenphp.GoString()`           | `frankenphp.PHPString()`           | ✅                    |\n| `array`            | `frankenphp.AssociativeArray` | ❌                | `frankenphp.GoAssociativeArray()` | `frankenphp.PHPAssociativeArray()` | ✅                    |\n| `array`            | `map[string]any`              | ❌                | `frankenphp.GoMap()`              | `frankenphp.PHPMap()`              | ✅                    |\n| `array`            | `[]any`                       | ❌                | `frankenphp.GoPackedArray()`      | `frankenphp.PHPPackedArray()`      | ✅                    |\n| `mixed`            | `any`                         | ❌                | `GoValue()`                       | `PHPValue()`                       | ❌                    |\n| `callable`         | `*C.zval`                     | ❌                | -                                 | frankenphp.CallPHPCallable()       | ❌                    |\n| `object`           | `struct`                      | ❌                | _Not yet implemented_             | _Not yet implemented_              | ❌                    |\n\n> [!NOTE]\n>\n> This table is not exhaustive yet and will be completed as the FrankenPHP types API gets more complete.\n>\n> For class methods specifically, primitive types and arrays are currently supported. Objects cannot be used as method parameters or return types yet.\n\nIf you refer to the code snippet of the previous section, you can see that helpers are used to convert the first parameter and the return value. The second and third parameter of our `repeat_this()` function don't need to be converted as memory representation of the underlying types are the same for both C and Go.\n\n#### Working with Arrays\n\nFrankenPHP provides native support for PHP arrays through `frankenphp.AssociativeArray` or direct conversion to a map or slice.\n\n`AssociativeArray` represents a [hash map](https://en.wikipedia.org/wiki/Hash_table) composed of a `Map: map[string]any`field and an optional `Order: []string` field (unlike PHP \"associative arrays\", Go maps aren't ordered).\n\nIf order or association are not needed, it's also possible to directly convert to a slice `[]any` or unordered map `map[string]any`.\n\n**Creating and manipulating arrays in Go:**\n\n```go\npackage example\n\n// #include <Zend/zend_types.h>\nimport \"C\"\nimport (\n    \"unsafe\"\n\n    \"github.com/dunglas/frankenphp\"\n)\n\n// export_php:function process_data_ordered(array $input): array\nfunc process_data_ordered_map(arr *C.zend_array) unsafe.Pointer {\n\t// Convert PHP associative array to Go while keeping the order\n\tassociativeArray, err := frankenphp.GoAssociativeArray[any](unsafe.Pointer(arr))\n    if err != nil {\n        // handle error\n    }\n\n\t// loop over the entries in order\n\tfor _, key := range associativeArray.Order {\n\t\tvalue, _ = associativeArray.Map[key]\n\t\t// do something with key and value\n\t}\n\n\t// return an ordered array\n\t// if 'Order' is not empty, only the key-value pairs in 'Order' will be respected\n\treturn frankenphp.PHPAssociativeArray[string](frankenphp.AssociativeArray[string]{\n\t\tMap: map[string]string{\n\t\t\t\"key1\": \"value1\",\n\t\t\t\"key2\": \"value2\",\n\t\t},\n\t\tOrder: []string{\"key1\", \"key2\"},\n\t})\n}\n\n// export_php:function process_data_unordered(array $input): array\nfunc process_data_unordered_map(arr *C.zend_array) unsafe.Pointer {\n\t// Convert PHP associative array to a Go map without keeping the order\n\t// ignoring the order will be more performant\n\tgoMap, err := frankenphp.GoMap[any](unsafe.Pointer(arr))\n    if err != nil {\n        // handle error\n    }\n\n\t// loop over the entries in no specific order\n\tfor key, value := range goMap {\n\t\t// do something with key and value\n\t}\n\n\t// return an unordered array\n\treturn frankenphp.PHPMap(map[string]string {\n\t\t\"key1\": \"value1\",\n\t\t\"key2\": \"value2\",\n\t})\n}\n\n// export_php:function process_data_packed(array $input): array\nfunc process_data_packed(arr *C.zend_array) unsafe.Pointer {\n\t// Convert PHP packed array to Go\n\tgoSlice, err := frankenphp.GoPackedArray(unsafe.Pointer(arr))\n    if err != nil {\n        // handle error\n    }\n\n\t// loop over the slice in order\n\tfor index, value := range goSlice {\n\t\t// do something with index and value\n\t}\n\n\t// return a packed array\n\treturn frankenphp.PHPPackedArray([]string{\"value1\", \"value2\", \"value3\"})\n}\n```\n\n**Key features of array conversion:**\n\n- **Ordered key-value pairs** - Option to keep the order of the associative array\n- **Optimized for multiple cases** - Option to ditch the order for better performance or convert straight to a slice\n- **Automatic list detection** - When converting to PHP, automatically detects if array should be a packed list or hashmap\n- **Nested Arrays** - Arrays can be nested and will convert all support types automatically (`int64`,`float64`,`string`,`bool`,`nil`,`AssociativeArray`,`map[string]any`,`[]any`)\n- **Objects are not supported** - Currently, only scalar types and arrays can be used as values. Providing an object will result in a `null` value in the PHP array.\n\n##### Available methods: Packed and Associative\n\n- `frankenphp.PHPAssociativeArray(arr frankenphp.AssociativeArray) unsafe.Pointer` - Convert to an ordered PHP array with key-value pairs\n- `frankenphp.PHPMap(arr map[string]any) unsafe.Pointer` - Convert a map to an unordered PHP array with key-value pairs\n- `frankenphp.PHPPackedArray(slice []any) unsafe.Pointer` - Convert a slice to a PHP packed array with indexed values only\n- `frankenphp.GoAssociativeArray(arr unsafe.Pointer, ordered bool) frankenphp.AssociativeArray` - Convert a PHP array to an ordered Go `AssociativeArray` (map with order)\n- `frankenphp.GoMap(arr unsafe.Pointer) map[string]any` - Convert a PHP array to an unordered Go map\n- `frankenphp.GoPackedArray(arr unsafe.Pointer) []any` - Convert a PHP array to a Go slice\n- `frankenphp.IsPacked(zval *C.zend_array) bool` - Check if a PHP array is packed (indexed only) or associative (key-value pairs)\n\n### Working with Callables\n\nFrankenPHP provides a way to work with PHP callables using the `frankenphp.CallPHPCallable` helper. This allows you to call PHP functions or methods from Go code.\n\nTo showcase this, let's create our own `array_map()` function that takes a callable and an array, applies the callable to each element of the array, and returns a new array with the results:\n\n```go\n// export_php:function my_array_map(array $data, callable $callback): array\nfunc my_array_map(arr *C.zend_array, callback *C.zval) unsafe.Pointer {\n\tgoSlice, err := frankenphp.GoPackedArray[any](unsafe.Pointer(arr))\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tresult := make([]any, len(goSlice))\n\n\tfor index, value := range goSlice {\n\t\tresult[index] = frankenphp.CallPHPCallable(unsafe.Pointer(callback), []interface{}{value})\n\t}\n\n\treturn frankenphp.PHPPackedArray(result)\n}\n```\n\nNotice how we use `frankenphp.CallPHPCallable()` to call the PHP callable passed as a parameter. This function takes a pointer to the callable and an array of arguments, and it returns the result of the callable execution. You can use the callable syntax you're used to:\n\n```php\n<?php\n\n$result = my_array_map([1, 2, 3], function($x) { return $x * 2; });\n// $result will be [2, 4, 6]\n\n$result = my_array_map(['hello', 'world'], 'strtoupper');\n// $result will be ['HELLO', 'WORLD']\n```\n\n### Declaring a Native PHP Class\n\nThe generator supports declaring **opaque classes** as Go structs, which can be used to create PHP objects. You can use the `//export_php:class` directive comment to define a PHP class. For example:\n\n```go\npackage example\n\n//export_php:class User\ntype UserStruct struct {\n    Name string\n    Age  int\n}\n```\n\n#### What are Opaque Classes?\n\n**Opaque classes** are classes where the internal structure (properties) is hidden from PHP code. This means:\n\n- **No direct property access**: You cannot read or write properties directly from PHP (`$user->name` won't work)\n- **Method-only interface** - All interactions must go through methods you define\n- **Better encapsulation** - Internal data structure is completely controlled by Go code\n- **Type safety** - No risk of PHP code corrupting internal state with wrong types\n- **Cleaner API** - Forces to design a proper public interface\n\nThis approach provides better encapsulation and prevents PHP code from accidentally corrupting the internal state of your Go objects. All interactions with the object must go through the methods you explicitly define.\n\n#### Adding Methods to Classes\n\nSince properties are not directly accessible, you **must define methods** to interact with your opaque classes. Use the `//export_php:method` directive to define behavior:\n\n```go\npackage example\n\n// #include <Zend/zend_types.h>\nimport \"C\"\nimport (\n    \"unsafe\"\n\n    \"github.com/dunglas/frankenphp\"\n)\n\n//export_php:class User\ntype UserStruct struct {\n    Name string\n    Age  int\n}\n\n//export_php:method User::getName(): string\nfunc (us *UserStruct) GetUserName() unsafe.Pointer {\n    return frankenphp.PHPString(us.Name, false)\n}\n\n//export_php:method User::setAge(int $age): void\nfunc (us *UserStruct) SetUserAge(age int64) {\n    us.Age = int(age)\n}\n\n//export_php:method User::getAge(): int\nfunc (us *UserStruct) GetUserAge() int64 {\n    return int64(us.Age)\n}\n\n//export_php:method User::setNamePrefix(string $prefix = \"User\"): void\nfunc (us *UserStruct) SetNamePrefix(prefix *C.zend_string) {\n    us.Name = frankenphp.GoString(unsafe.Pointer(prefix)) + \": \" + us.Name\n}\n```\n\n#### Nullable Parameters\n\nThe generator supports nullable parameters using the `?` prefix in PHP signatures. When a parameter is nullable, it becomes a pointer in your Go function, allowing you to check if the value was `null` in PHP:\n\n```go\npackage example\n\n// #include <Zend/zend_types.h>\nimport \"C\"\nimport (\n\t\"unsafe\"\n\n\t\"github.com/dunglas/frankenphp\"\n)\n\n//export_php:method User::updateInfo(?string $name, ?int $age, ?bool $active): void\nfunc (us *UserStruct) UpdateInfo(name *C.zend_string, age *int64, active *bool) {\n    // Check if name was provided (not null)\n    if name != nil {\n        us.Name = frankenphp.GoString(unsafe.Pointer(name))\n    }\n\n    // Check if age was provided (not null)\n    if age != nil {\n        us.Age = int(*age)\n    }\n\n    // Check if active was provided (not null)\n    if active != nil {\n        us.Active = *active\n    }\n}\n```\n\n**Key points about nullable parameters:**\n\n- **Nullable primitive types** (`?int`, `?float`, `?bool`) become pointers (`*int64`, `*float64`, `*bool`) in Go\n- **Nullable strings** (`?string`) remain as `*C.zend_string` but can be `nil`\n- **Check for `nil`** before dereferencing pointer values\n- **PHP `null` becomes Go `nil`** - when PHP passes `null`, your Go function receives a `nil` pointer\n\n> [!WARNING]\n>\n> Currently, class methods have the following limitations. **Objects are not supported** as parameter types or return types. **Arrays are fully supported** for both parameters and return types. Supported types: `string`, `int`, `float`, `bool`, `array`, and `void` (for return type). **Nullable parameter types are fully supported** for all scalar types (`?string`, `?int`, `?float`, `?bool`).\n\nAfter generating the extension, you will be allowed to use the class and its methods in PHP. Note that you **cannot access properties directly**:\n\n```php\n<?php\n\n$user = new User();\n\n// ✅ This works - using methods\n$user->setAge(25);\necho $user->getName();           // Output: (empty, default value)\necho $user->getAge();            // Output: 25\n$user->setNamePrefix(\"Employee\");\n\n// ✅ This also works - nullable parameters\n$user->updateInfo(\"John\", 30, true);        // All parameters provided\n$user->updateInfo(\"Jane\", null, false);     // Age is null\n$user->updateInfo(null, 25, null);          // Name and active are null\n\n// ❌ This will NOT work - direct property access\n// echo $user->name;             // Error: Cannot access private property\n// $user->age = 30;              // Error: Cannot access private property\n```\n\nThis design ensures that your Go code has complete control over how the object's state is accessed and modified, providing better encapsulation and type safety.\n\n### Declaring Constants\n\nThe generator supports exporting Go constants to PHP using two directives: `//export_php:const` for global constants and `//export_php:classconst` for class constants. This allows you to share configuration values, status codes, and other constants between Go and PHP code.\n\n#### Global Constants\n\nUse the `//export_php:const` directive to create global PHP constants:\n\n```go\npackage example\n\n//export_php:const\nconst MAX_CONNECTIONS = 100\n\n//export_php:const\nconst API_VERSION = \"1.2.3\"\n\n//export_php:const\nconst (\n\tSTATUS_OK = iota\n\tSTATUS_ERROR\n)\n```\n\n> [!NOTE]\n> PHP constants will take the name of the Go constant, thus using upper case letters is recommended.\n\n#### Class Constants\n\nUse the `//export_php:classconst ClassName` directive to create constants that belong to a specific PHP class:\n\n```go\npackage example\n\n//export_php:classconst User\nconst STATUS_ACTIVE = 1\n\n//export_php:classconst User\nconst STATUS_INACTIVE = 0\n\n//export_php:classconst User\nconst ROLE_ADMIN = \"admin\"\n\n//export_php:classconst Order\nconst (\n\tSTATE_PENDING = iota\n\tSTATE_PROCESSING\n\tSTATE_COMPLETED\n)\n```\n\n> [!NOTE]\n> Just like global constants, the class constants will take the name of the Go constant.\n\nClass constants are accessible using the class name scope in PHP:\n\n```php\n<?php\n\n// Global constants\necho MAX_CONNECTIONS;    // 100\necho API_VERSION;        // \"1.2.3\"\n\n// Class constants\necho User::STATUS_ACTIVE;    // 1\necho User::ROLE_ADMIN;       // \"admin\"\necho Order::STATE_PENDING;   // 0\n```\n\nThe directive supports various value types including strings, integers, booleans, floats, and iota constants. When using `iota`, the generator automatically assigns sequential values (0, 1, 2, etc.). Global constants become available in your PHP code as global constants, while class constants are scoped to their respective classes using the public visibility. When using integers, different possible notation (binary, hex, octal) are supported and dumped as is in the PHP stub file.\n\nYou can use constants just like you are used to in the Go code. For example, let's take the `repeat_this()` function we declared earlier and change the last argument to an integer:\n\n```go\npackage example\n\n// #include <Zend/zend_types.h>\nimport \"C\"\nimport (\n\t\"strings\"\n\t\"unsafe\"\n\n\t\"github.com/dunglas/frankenphp\"\n)\n\n//export_php:const\nconst STR_REVERSE = iota\n\n//export_php:const\nconst STR_NORMAL = iota\n\n//export_php:classconst StringProcessor\nconst MODE_LOWERCASE = 1\n\n//export_php:classconst StringProcessor\nconst MODE_UPPERCASE = 2\n\n//export_php:function repeat_this(string $str, int $count, int $mode): string\nfunc repeat_this(s *C.zend_string, count int64, mode int) unsafe.Pointer {\n\tstr := frankenphp.GoString(unsafe.Pointer(s))\n\n\tresult := strings.Repeat(str, int(count))\n\tif mode == STR_REVERSE {\n\t\t// reverse the string\n\t}\n\n\tif mode == STR_NORMAL {\n\t\t// no-op, just to showcase the constant\n\t}\n\n\treturn frankenphp.PHPString(result, false)\n}\n\n//export_php:class StringProcessor\ntype StringProcessorStruct struct {\n\t// internal fields\n}\n\n//export_php:method StringProcessor::process(string $input, int $mode): string\nfunc (sp *StringProcessorStruct) Process(input *C.zend_string, mode int64) unsafe.Pointer {\n\tstr := frankenphp.GoString(unsafe.Pointer(input))\n\n\tswitch mode {\n\tcase MODE_LOWERCASE:\n\t\tstr = strings.ToLower(str)\n\tcase MODE_UPPERCASE:\n\t\tstr = strings.ToUpper(str)\n\t}\n\n\treturn frankenphp.PHPString(str, false)\n}\n```\n\n### Using Namespaces\n\nThe generator supports organizing your PHP extension's functions, classes, and constants under a namespace using the `//export_php:namespace` directive. This helps avoid naming conflicts and provides better organization for your extension's API.\n\n#### Declaring a Namespace\n\nUse the `//export_php:namespace` directive at the top of your Go file to place all exported symbols under a specific namespace:\n\n```go\n//export_php:namespace My\\Extension\npackage example\n\nimport (\n    \"unsafe\"\n\n    \"github.com/dunglas/frankenphp\"\n)\n\n//export_php:function hello(): string\nfunc hello() string {\n    return \"Hello from My\\\\Extension namespace!\"\n}\n\n//export_php:class User\ntype UserStruct struct {\n    // internal fields\n}\n\n//export_php:method User::getName(): string\nfunc (u *UserStruct) GetName() unsafe.Pointer {\n    return frankenphp.PHPString(\"John Doe\", false)\n}\n\n//export_php:const\nconst STATUS_ACTIVE = 1\n```\n\n#### Using Namespaced Extension in PHP\n\nWhen a namespace is declared, all functions, classes, and constants are placed under that namespace in PHP:\n\n```php\n<?php\n\necho My\\Extension\\hello(); // \"Hello from My\\Extension namespace!\"\n\n$user = new My\\Extension\\User();\necho $user->getName(); // \"John Doe\"\n\necho My\\Extension\\STATUS_ACTIVE; // 1\n```\n\n#### Important Notes\n\n- Only **one** namespace directive is allowed per file. If multiple namespace directives are found, the generator will return an error.\n- The namespace applies to **all** exported symbols in the file: functions, classes, methods, and constants.\n- Namespace names follow PHP namespace conventions using backslashes (`\\`) as separators.\n- If no namespace is declared, symbols are exported to the global namespace as usual.\n\n### Generating the Extension\n\nThis is where the magic happens, and your extension can now be generated. You can run the generator with the following command:\n\n```console\nGEN_STUB_SCRIPT=php-src/build/gen_stub.php frankenphp extension-init my_extension.go\n```\n\n> [!NOTE]\n> Don't forget to set the `GEN_STUB_SCRIPT` environment variable to the path of the `gen_stub.php` file in the PHP sources you downloaded earlier. This is the same `gen_stub.php` script mentioned in the manual implementation section.\n\nIf everything went well, your project directory should contain the following files for your extension:\n\n- **`my_extension.go`** - Your original source file (remains unchanged)\n- **`my_extension_generated.go`** - Generated file with CGO wrappers that call your functions\n- **`my_extension.stub.php`** - PHP stub file for IDE autocompletion\n- **`my_extension_arginfo.h`** - PHP argument information\n- **`my_extension.h`** - C header file\n- **`my_extension.c`** - C implementation file\n- **`README.md`** - Documentation\n\n> [!IMPORTANT]\n> **Your source file (`my_extension.go`) is never modified.** The generator creates a separate `_generated.go` file containing CGO wrappers that call your original functions. This means you can safely version control your source file without worrying about generated code polluting it.\n\n### Integrating the Generated Extension into FrankenPHP\n\nOur extension is now ready to be compiled and integrated into FrankenPHP. To do this, refer to the FrankenPHP [compilation documentation](compile.md) to learn how to compile FrankenPHP. Add the module using the `--with` flag, pointing to the path of your module:\n\n```console\nCGO_ENABLED=1 \\\nXCADDY_GO_BUILD_FLAGS=\"-ldflags='-w -s' -tags=nobadger,nomysql,nopgx\" \\\nCGO_CFLAGS=$(php-config --includes) \\\nCGO_LDFLAGS=\"$(php-config --ldflags) $(php-config --libs)\" \\\nxcaddy build \\\n    --output frankenphp \\\n    --with github.com/my-account/my-module/build\n```\n\nNote that you point to the `/build` subdirectory that was created during the generation step. However, this is not mandatory: you can also copy the generated files to your module directory and point to it directly.\n\n### Testing Your Generated Extension\n\nYou can create a PHP file to test the functions and classes you've created. For example, create an `index.php` file with the following content:\n\n```php\n<?php\n\n// Using global constants\nvar_dump(repeat_this('Hello World', 5, STR_REVERSE));\n\n// Using class constants\n$processor = new StringProcessor();\necho $processor->process('Hello World', StringProcessor::MODE_LOWERCASE);  // \"hello world\"\necho $processor->process('Hello World', StringProcessor::MODE_UPPERCASE);  // \"HELLO WORLD\"\n```\n\nOnce you've integrated your extension into FrankenPHP as demonstrated in the previous section, you can run this test file using `./frankenphp php-server`, and you should see your extension working.\n\n## Manual Implementation\n\nIf you want to understand how extensions work or need full control over your extension, you can write them manually. This approach gives you complete control but requires more boilerplate code.\n\n### Basic Function\n\nWe'll see how to write a simple PHP extension in Go that defines a new native function. This function will be called from PHP and will trigger a goroutine that logs a message in Caddy's logs. This function doesn't take any parameters and returns nothing.\n\n#### Define the Go Function\n\nIn your module, you need to define a new native function that will be called from PHP. To do this, create a file with the name you want, for example, `extension.go`, and add the following code:\n\n```go\npackage example\n\n// #include \"extension.h\"\nimport \"C\"\nimport (\n\t\"log/slog\"\n\t\"unsafe\"\n\n\t\"github.com/dunglas/frankenphp\"\n)\n\nfunc init() {\n\tfrankenphp.RegisterExtension(unsafe.Pointer(&C.ext_module_entry))\n}\n\n//export go_print_something\nfunc go_print_something() {\n\tgo func() {\n\t\tslog.Info(\"Hello from a goroutine!\")\n\t}()\n}\n```\n\nThe `frankenphp.RegisterExtension()` function simplifies the extension registration process by handling the internal PHP registration logic. The `go_print_something` function uses the `//export` directive to indicate that it will be accessible in the C code we will write, thanks to CGO.\n\nIn this example, our new function will trigger a goroutine that logs a message in Caddy's logs.\n\n#### Define the PHP Function\n\nTo allow PHP to call our function, we need to define a corresponding PHP function. For this, we will create a stub file, for example, `extension.stub.php`, which will contain the following code:\n\n```php\n<?php\n\n/** @generate-class-entries */\n\nfunction go_print(): void {}\n```\n\nThis file defines the signature of the `go_print()` function, which will be called from PHP. The `@generate-class-entries` directive allows PHP to automatically generate function entries for our extension.\n\nThis is not done manually but using a script provided in the PHP sources (make sure to adjust the path to the `gen_stub.php` script based on where your PHP sources are located):\n\n```bash\nphp ../php-src/build/gen_stub.php extension.stub.php\n```\n\nThis script will generate a file named `extension_arginfo.h` that contains the necessary information for PHP to know how to define and call our function.\n\n#### Write the Bridge Between Go and C\n\nNow, we need to write the bridge between Go and C. Create a file named `extension.h` in your module directory with the following content:\n\n```c\n#ifndef _EXTENSION_H\n#define _EXTENSION_H\n\n#include <php.h>\n\nextern zend_module_entry ext_module_entry;\n\n#endif\n```\n\nNext, create a file named `extension.c` that will perform the following steps:\n\n- Include PHP headers;\n- Declare our new native PHP function `go_print()`;\n- Declare the extension metadata.\n\nLet's start by including the required headers:\n\n```c\n#include <php.h>\n#include \"extension.h\"\n#include \"extension_arginfo.h\"\n\n// Contains symbols exported by Go\n#include \"_cgo_export.h\"\n```\n\nWe then define our PHP function as a native language function:\n\n```c\nPHP_FUNCTION(go_print)\n{\n    ZEND_PARSE_PARAMETERS_NONE();\n\n    go_print_something();\n}\n\nzend_module_entry ext_module_entry = {\n    STANDARD_MODULE_HEADER,\n    \"ext_go\",\n    ext_functions, /* Functions */\n    NULL,          /* MINIT */\n    NULL,          /* MSHUTDOWN */\n    NULL,          /* RINIT */\n    NULL,          /* RSHUTDOWN */\n    NULL,          /* MINFO */\n    \"0.1.1\",\n    STANDARD_MODULE_PROPERTIES\n};\n```\n\nIn this case, our function takes no parameters and returns nothing. It simply calls the Go function we defined earlier, exported using the `//export` directive.\n\nFinally, we define the extension's metadata in a `zend_module_entry` structure, such as its name, version, and properties. This information is necessary for PHP to recognize and load our extension. Note that `ext_functions` is an array of pointers to the PHP functions we defined, and it was automatically generated by the `gen_stub.php` script in the `extension_arginfo.h` file.\n\nThe extension registration is automatically handled by FrankenPHP's `RegisterExtension()` function that we call in our Go code.\n\n### Advanced Usage\n\nNow that we know how to create a basic PHP extension in Go, let's complexify our example. We will now create a PHP function that takes a string as a parameter and returns its uppercase version.\n\n#### Define the PHP Function Stub\n\nTo define the new PHP function, we will modify our `extension.stub.php` file to include the new function signature:\n\n```php\n<?php\n\n/** @generate-class-entries */\n\n/**\n * Converts a string to uppercase.\n *\n * @param string $string The string to convert.\n * @return string The uppercase version of the string.\n */\nfunction go_upper(string $string): string {}\n```\n\n> [!TIP]\n> Don't neglect the documentation of your functions! You are likely to share your extension stubs with other developers to document how to use your extension and which features are available.\n\nBy regenerating the stub file with the `gen_stub.php` script, the `extension_arginfo.h` file should look like this:\n\n```c\nZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_go_upper, 0, 1, IS_STRING, 0)\n    ZEND_ARG_TYPE_INFO(0, string, IS_STRING, 0)\nZEND_END_ARG_INFO()\n\nZEND_FUNCTION(go_upper);\n\nstatic const zend_function_entry ext_functions[] = {\n    ZEND_FE(go_upper, arginfo_go_upper)\n    ZEND_FE_END\n};\n```\n\nWe can see that the `go_upper` function is defined with a parameter of type `string` and a return type of `string`.\n\n#### Type Juggling Between Go and PHP/C\n\nYour Go function cannot directly accept a PHP string as a parameter. You need to convert it to a Go string. Fortunately, FrankenPHP provides helper functions to handle the conversion between PHP strings and Go strings, similar to what we saw in the generator approach.\n\nThe header file remains simple:\n\n```c\n#ifndef _EXTENSION_H\n#define _EXTENSION_H\n\n#include <php.h>\n\nextern zend_module_entry ext_module_entry;\n\n#endif\n```\n\nWe can now write the bridge between Go and C in our `extension.c` file. We will pass the PHP string directly to our Go function:\n\n```c\nPHP_FUNCTION(go_upper)\n{\n    zend_string *str;\n\n    ZEND_PARSE_PARAMETERS_START(1, 1)\n        Z_PARAM_STR(str)\n    ZEND_PARSE_PARAMETERS_END();\n\n    zend_string *result = go_upper(str);\n    RETVAL_STR(result);\n}\n```\n\nYou can learn more about the `ZEND_PARSE_PARAMETERS_START` and parameters parsing in the dedicated page of [the PHP Internals Book](https://www.phpinternalsbook.com/php7/extensions_design/php_functions.html#parsing-parameters-zend-parse-parameters). Here, we tell PHP that our function takes one mandatory parameter of type `string` as a `zend_string`. We then pass this string directly to our Go function and return the result using `RETVAL_STR`.\n\nThere's only one thing left to do: implement the `go_upper` function in Go.\n\n#### Implement the Go Function\n\nOur Go function will take a `*C.zend_string` as a parameter, convert it to a Go string using FrankenPHP's helper function, process it, and return the result as a new `*C.zend_string`. The helper functions handle all the memory management and conversion complexity for us.\n\n```go\npackage example\n\n// #include <Zend/zend_types.h>\nimport \"C\"\nimport (\n    \"unsafe\"\n    \"strings\"\n\n    \"github.com/dunglas/frankenphp\"\n)\n\n//export go_upper\nfunc go_upper(s *C.zend_string) *C.zend_string {\n    str := frankenphp.GoString(unsafe.Pointer(s))\n\n    upper := strings.ToUpper(str)\n\n    return (*C.zend_string)(frankenphp.PHPString(upper, false))\n}\n```\n\nThis approach is much cleaner and safer than manual memory management.\nFrankenPHP's helper functions handle the conversion between PHP's `zend_string` format and Go strings automatically.\nThe `false` parameter in `PHPString()` indicates that we want to create a new non-persistent string (freed at the end of the request).\n\n> [!TIP]\n>\n> In this example, we don't perform any error handling, but you should always check that pointers are not `nil` and that the data is valid before using it in your Go functions.\n\n### Integrating the Extension into FrankenPHP\n\nOur extension is now ready to be compiled and integrated into FrankenPHP. To do this, refer to the FrankenPHP [compilation documentation](compile.md) to learn how to compile FrankenPHP. Add the module using the `--with` flag, pointing to the path of your module:\n\n```console\nCGO_ENABLED=1 \\\nXCADDY_GO_BUILD_FLAGS=\"-ldflags='-w -s' -tags=nobadger,nomysql,nopgx\" \\\nCGO_CFLAGS=$(php-config --includes) \\\nCGO_LDFLAGS=\"$(php-config --ldflags) $(php-config --libs)\" \\\nxcaddy build \\\n    --output frankenphp \\\n    --with github.com/my-account/my-module\n```\n\nThat's it! Your extension is now integrated into FrankenPHP and can be used in your PHP code.\n\n### Testing Your Extension\n\nAfter integrating your extension into FrankenPHP, you can create an `index.php` file with examples for the functions you've implemented:\n\n```php\n<?php\n\n// Test basic function\ngo_print();\n\n// Test advanced function\necho go_upper(\"hello world\") . \"\\n\";\n```\n\nYou can now run FrankenPHP with this file using `./frankenphp php-server`, and you should see your extension working.\n"
  },
  {
    "path": "docs/fr/CONTRIBUTING.md",
    "content": "# Contribuer\n\n## Compiler PHP\n\n### Avec Docker (Linux)\n\nConstruisez l'image Docker de développement :\n\n```console\ndocker build -t frankenphp-dev -f dev.Dockerfile .\ndocker run --cap-add=SYS_PTRACE --security-opt seccomp=unconfined -p 8080:8080 -p 443:443 -p 443:443/udp -v $PWD:/go/src/app -it frankenphp-dev\n```\n\nL'image contient les outils de développement habituels (Go, GDB, Valgrind, Neovim...) et utilise les emplacements de configuration PHP suivants\n\n- php.ini: `/etc/frankenphp/php.ini` Un fichier php.ini avec des préréglages de développement est fourni par défaut.\n- fichiers de configuration supplémentaires: `/etc/frankenphp/php.d/*.ini`\n- extensions php: `/usr/lib/frankenphp/modules/`\n\nSi votre version de Docker est inférieure à 23.0, la construction échouera à cause d'un [problème de pattern](https://github.com/moby/moby/pull/42676) dans `.dockerignore`. Ajoutez les répertoires à `.dockerignore`.\n\n```patch\n !testdata/*.php\n !testdata/*.txt\n+!caddy\n+!internal\n```\n\n### Sans Docker (Linux et macOS)\n\n[Suivez les instructions pour compiler à partir des sources](compile.md) et passez l'indicateur de configuration `--debug`.\n\n## Exécution de la suite de tests\n\n```console\ngo test -race -v ./...\n```\n\n## Module Caddy\n\nConstruire Caddy avec le module FrankenPHP :\n\n```console\ncd caddy/frankenphp/\ngo build -tags nobadger,nomysql,nopgx\ncd ../../\n```\n\nExécuter Caddy avec le module FrankenPHP :\n\n```console\ncd testdata/\n../caddy/frankenphp/frankenphp run\n```\n\nLe serveur est configuré pour écouter à l'adresse `127.0.0.1:80`:\n\n> [!NOTE]\n>\n> Si vous utilisez Docker, vous devrez soit lier le port 80 du conteneur, soit exécuter depuis l'intérieur du conteneur.\n\n```console\ncurl -vk http://127.0.0.1/phpinfo.php\n```\n\n## Serveur de test minimal\n\nConstruire le serveur de test minimal :\n\n```console\ncd internal/testserver/\ngo build\ncd ../../\n```\n\nLancer le test serveur :\n\n```console\ncd testdata/\n../internal/testserver/testserver\n```\n\nLe serveur est configuré pour écouter à l'adresse `127.0.0.1:8080`:\n\n```console\ncurl -v http://127.0.0.1:8080/phpinfo.php\n```\n\n## Construire localement les images Docker\n\nAfficher le plan de compilation :\n\n```console\ndocker buildx bake -f docker-bake.hcl --print\n```\n\nConstruire localement les images FrankenPHP pour amd64 :\n\n```console\ndocker buildx bake -f docker-bake.hcl --pull --load --set \"*.platform=linux/amd64\"\n```\n\nConstruire localement les images FrankenPHP pour arm64 :\n\n```console\ndocker buildx bake -f docker-bake.hcl --pull --load --set \"*.platform=linux/arm64\"\n```\n\nConstruire à partir de zéro les images FrankenPHP pour arm64 & amd64 et les pousser sur Docker Hub :\n\n```console\ndocker buildx bake -f docker-bake.hcl --pull --no-cache --push\n```\n\n## Déboguer les erreurs de segmentation avec les builds statiques\n\n1. Téléchargez la version de débogage du binaire FrankenPHP depuis GitHub ou créez votre propre build statique incluant des symboles de débogage :\n\n   ```console\n   docker buildx bake \\\n       --load \\\n       --set static-builder.args.DEBUG_SYMBOLS=1 \\\n       --set \"static-builder.platform=linux/amd64\" \\\n       static-builder\n   docker cp $(docker create --name static-builder-musl dunglas/frankenphp:static-builder-musl):/go/src/app/dist/frankenphp-linux-$(uname -m) frankenphp\n   ```\n\n2. Remplacez votre version actuelle de `frankenphp` par l'exécutable de débogage de FrankenPHP.\n3. Démarrez FrankenPHP comme d'habitude (alternativement, vous pouvez directement démarrer FrankenPHP avec GDB : `gdb --args frankenphp run`).\n4. Attachez-vous au processus avec GDB :\n\n   ```console\n   gdb -p `pidof frankenphp`\n   ```\n\n5. Si nécessaire, tapez `continue` dans le shell GDB\n6. Faites planter FrankenPHP.\n7. Tapez `bt` dans le shell GDB\n8. Copiez la sortie\n\n## Déboguer les erreurs de segmentation dans GitHub Actions\n\n1. Ouvrir `.github/workflows/tests.yml`\n2. Activer les symboles de débogage de la bibliothèque PHP\n\n   ```patch\n       - uses: shivammathur/setup-php@v2\n         # ...\n         env:\n           phpts: ts\n   +       debug: true\n   ```\n\n3. Activer `tmate` pour se connecter au conteneur\n\n   ```patch\n       - name: Set CGO flags\n         run: echo \"CGO_CFLAGS=$(php-config --includes)\" >> \"$GITHUB_ENV\"\n   +   - run: |\n   +       sudo apt install gdb\n   +       mkdir -p /home/runner/.config/gdb/\n   +       printf \"set auto-load safe-path /\\nhandle SIG34 nostop noprint pass\" > /home/runner/.config/gdb/gdbinit\n   +   - uses: mxschmitt/action-tmate@v3\n   ```\n\n4. Se connecter au conteneur\n5. Ouvrir `frankenphp.go`\n6. Activer `cgosymbolizer`\n\n   ```patch\n   - //_ \"github.com/ianlancetaylor/cgosymbolizer\"\n   + _ \"github.com/ianlancetaylor/cgosymbolizer\"\n   ```\n\n7. Télécharger le module : `go get`\n8. Dans le conteneur, vous pouvez utiliser GDB et similaires :\n\n   ```console\n   go test -c -ldflags=-w\n   gdb --args frankenphp.test -test.run ^MyTest$\n   ```\n\n9. Quand le bug est corrigé, annulez tous les changements.\n\n## Ressources Diverses pour le Développement\n\n- [Intégration de PHP dans uWSGI](https://github.com/unbit/uwsgi/blob/master/plugins/php/php_plugin.c)\n- [Intégration de PHP dans NGINX Unit](https://github.com/nginx/unit/blob/master/src/nxt_php_sapi.c)\n- [Intégration de PHP dans Go (go-php)](https://github.com/deuill/go-php)\n- [Intégration de PHP dans Go (GoEmPHP)](https://github.com/mikespook/goemphp)\n- [Intégration de PHP dans C++](https://gist.github.com/paresy/3cbd4c6a469511ac7479aa0e7c42fea7)\n- [Extending and Embedding PHP par Sara Golemon](https://books.google.fr/books?id=zMbGvK17_tYC&pg=PA254&lpg=PA254#v=onepage&q&f=false)\n- [Qu'est-ce que TSRMLS_CC, au juste ?](http://blog.golemon.com/2006/06/what-heck-is-tsrmlscc-anyway.html)\n- [Intégration de PHP sur Mac](https://gist.github.com/jonnywang/61427ffc0e8dde74fff40f479d147db4)\n- [Bindings SDL](https://pkg.go.dev/github.com/veandco/go-sdl2@v0.4.21/sdl#Main)\n\n## Ressources Liées à Docker\n\n- [Définition du fichier Bake](https://docs.docker.com/build/customize/bake/file-definition/)\n- [`docker buildx build`](https://docs.docker.com/engine/reference/commandline/buildx_build/)\n\n## Commande utile\n\n```console\napk add strace util-linux gdb\nstrace -e 'trace=!futex,epoll_ctl,epoll_pwait,tgkill,rt_sigreturn' -p 1\n```\n\n## Traduire la documentation\n\nPour traduire la documentation et le site dans une nouvelle langue, procédez comme suit :\n\n1. Créez un nouveau répertoire nommé avec le code ISO à 2 caractères de la langue dans le répertoire `docs/` de ce dépôt\n2. Copiez tous les fichiers `.md` à la racine du répertoire `docs/` dans le nouveau répertoire (utilisez toujours la version anglaise comme source de traduction, car elle est toujours à jour).\n3. Copiez les fichiers `README.md` et `CONTRIBUTING.md` du répertoire racine vers le nouveau répertoire.\n4. Traduisez le contenu des fichiers, mais ne changez pas les noms de fichiers, ne traduisez pas non plus les chaînes commençant par `> [!` (c'est un balisage spécial pour GitHub).\n5. Créez une Pull Request avec les traductions\n6. Dans le [référentiel du site](https://github.com/dunglas/frankenphp-website/tree/main), copiez et traduisez les fichiers de traduction dans les répertoires `content/`, `data/` et `i18n/`.\n7. Traduire les valeurs dans le fichier YAML créé.\n8. Ouvrir une Pull Request sur le dépôt du site.\n"
  },
  {
    "path": "docs/fr/README.md",
    "content": "# FrankenPHP : le serveur d'applications PHP moderne, écrit en Go\n\n<h1 align=\"center\"><a href=\"https://frankenphp.dev\"><img src=\"../../frankenphp.png\" alt=\"FrankenPHP\" width=\"600\"></a></h1>\n\nFrankenPHP est un serveur d'applications moderne pour PHP construit à partir du serveur web [Caddy](https://caddyserver.com/).\n\nFrankenPHP donne des super-pouvoirs à vos applications PHP grâce à ses fonctionnalités à la pointe : [_Early Hints_](early-hints.md), [mode worker](worker.md), [fonctionnalités en temps réel](mercure.md), HTTPS automatique, prise en charge de HTTP/2 et HTTP/3...\n\nFrankenPHP fonctionne avec n'importe quelle application PHP et rend vos projets Laravel et Symfony plus rapides que jamais grâce à leurs intégrations officielles avec le mode worker.\n\nFrankenPHP peut également être utilisé comme une bibliothèque Go autonome qui permet d'intégrer PHP dans n'importe quelle application en utilisant `net/http`.\n\nDécouvrez plus de détails sur ce serveur d’application dans le replay de cette conférence donnée au Forum PHP 2022 :\n\n<a href=\"https://dunglas.dev/2022/10/frankenphp-the-modern-php-app-server-written-in-go/\"><img src=\"https://dunglas.dev/wp-content/uploads/2022/10/frankenphp.png\" alt=\"Diapositives\" width=\"600\"></a>\n\n## Pour Commencer\n\nSur Windows, utilisez [WSL](https://learn.microsoft.com/windows/wsl/) pour exécuter FrankenPHP.\n\n### Script d'installation\n\nVous pouvez copier cette ligne dans votre terminal pour installer automatiquement\nune version adaptée à votre plateforme :\n\n```console\ncurl https://frankenphp.dev/install.sh | sh\n```\n\n### Binaire autonome\n\nNous fournissons des binaires statiques de FrankenPHP pour le développement, pour Linux et macOS,\ncontenant [PHP 8.4](https://www.php.net/releases/8.4/fr.php) et la plupart des extensions PHP populaires.\n\n[Télécharger FrankenPHP](https://github.com/php/frankenphp/releases)\n\n**Installation d'extensions :** Les extensions les plus courantes sont incluses. Il n'est pas possible d'en installer davantage.\n\n### Paquets rpm\n\nNos mainteneurs proposent des paquets rpm pour tous les systèmes utilisant `dnf`. Pour installer, exécutez :\n\n```console\nsudo dnf install https://rpm.henderkes.com/static-php-1-0.noarch.rpm\nsudo dnf module enable php-zts:static-8.4 # 8.2-8.5 disponibles\nsudo dnf install frankenphp\n```\n\n**Installation d'extensions :** `sudo dnf install php-zts-<extension>`\n\nPour les extensions non disponibles par défaut, utilisez [PIE](https://github.com/php/pie) :\n\n```console\nsudo dnf install pie-zts\nsudo pie-zts install asgrim/example-pie-extension\n```\n\n### Paquets deb\n\nNos mainteneurs proposent des paquets deb pour tous les systèmes utilisant `apt`. Pour installer, exécutez :\n\n```console\nsudo curl -fsSL https://key.henderkes.com/static-php.gpg -o /usr/share/keyrings/static-php.gpg && \\\necho \"deb [signed-by=/usr/share/keyrings/static-php.gpg] https://deb.henderkes.com/ stable main\" | sudo tee /etc/apt/sources.list.d/static-php.list && \\\nsudo apt update\nsudo apt install frankenphp\n```\n\n**Installation d'extensions :** `sudo apt install php-zts-<extension>`\n\nPour les extensions non disponibles par défaut, utilisez [PIE](https://github.com/php/pie) :\n\n```console\nsudo apt install pie-zts\nsudo pie-zts install asgrim/example-pie-extension\n```\n\n### Docker\n\nDes [images Docker](https://frankenphp.dev/docs/fr/docker/) sont également disponibles :\n\n```console\ndocker run -v .:/app/public \\\n    -p 80:80 -p 443:443 -p 443:443/udp \\\n    dunglas/frankenphp\n```\n\nRendez-vous sur `https://localhost`, c'est parti !\n\n> [!TIP]\n>\n> Ne tentez pas d'utiliser `https://127.0.0.1`. Utilisez `https://localhost` et acceptez le certificat auto-signé.\n> Utilisez [la variable d'environnement `SERVER_NAME`](config.md#variables-denvironnement) pour changer le domaine à utiliser.\n\n### Homebrew\n\nFrankenPHP est également disponible sous forme de paquet [Homebrew](https://brew.sh) pour macOS et Linux.\n\nPour l'installer :\n\n```console\nbrew install dunglas/frankenphp/frankenphp\n```\n\n**Installation d'extensions :** Utilisez [PIE](https://github.com/php/pie).\n\n### Utilisation\n\nPour servir le contenu du répertoire courant, exécutez :\n\n```console\nfrankenphp php-server\n```\n\nVous pouvez également exécuter des scripts en ligne de commande avec :\n\n```console\nfrankenphp php-cli /path/to/your/script.php\n```\n\nPour les paquets deb et rpm, vous pouvez aussi démarrer le service systemd :\n\n```console\nsudo systemctl start frankenphp\n```\n\n## Documentation\n\n- [Le mode classique](classic.md)\n- [Le mode worker](worker.md)\n- [Le support des Early Hints (code de statut HTTP 103)](early-hints.md)\n- [Temps réel](mercure.md)\n- [Servir efficacement les fichiers statiques volumineux](x-sendfile.md)\n- [Configuration](config.md)\n- [Écrire des extensions PHP en Go](extensions.md)\n- [Images Docker](docker.md)\n- [Déploiement en production](production.md)\n- [Optimisation des performances](performance.md)\n- [Créer des applications PHP **standalone**, auto-exécutables](embed.md)\n- [Créer un build statique](static.md)\n- [Compiler depuis les sources](compile.md)\n- [Surveillance de FrankenPHP](metrics.md)\n- [Intégration Laravel](laravel.md)\n- [Problèmes connus](known-issues.md)\n- [Application de démo (Symfony) et benchmarks](https://github.com/dunglas/frankenphp-demo)\n- [Documentation de la bibliothèque Go](https://pkg.go.dev/github.com/dunglas/frankenphp)\n- [Contribuer et débugger](CONTRIBUTING.md)\n\n## Exemples et squelettes\n\n- [Symfony](https://github.com/dunglas/symfony-docker)\n- [API Platform](https://api-platform.com/docs/distribution/)\n- [Laravel](laravel.md)\n- [Sulu](https://sulu.io/blog/running-sulu-with-frankenphp)\n- [WordPress](https://github.com/StephenMiracle/frankenwp)\n- [Drupal](https://github.com/dunglas/frankenphp-drupal)\n- [Joomla](https://github.com/alexandreelise/frankenphp-joomla)\n- [TYPO3](https://github.com/ochorocho/franken-typo3)\n- [Magento2](https://github.com/ekino/frankenphp-magento2)\n"
  },
  {
    "path": "docs/fr/classic.md",
    "content": "# Utilisation du mode classique\n\nSans aucune configuration additionnelle, FrankenPHP fonctionne en mode classique. Dans ce mode, FrankenPHP fonctionne comme un serveur PHP traditionnel, en servant directement les fichiers PHP. Cela en fait un remplaçant parfait à PHP-FPM ou Apache avec mod_php.\n\nComme Caddy, FrankenPHP accepte un nombre illimité de connexions et utilise un [nombre fixe de threads](config.md#configuration-du-caddyfile) pour les servir. Le nombre de connexions acceptées et en attente n'est limité que par les ressources système disponibles.\nLe pool de threads PHP fonctionne avec un nombre fixe de threads initialisés au démarrage, comparable au mode statique de PHP-FPM. Il est également possible de laisser les threads [s'adapter automatiquement à l'exécution](performance.md#max_threads), comme dans le mode dynamique de PHP-FPM.\n\nLes connexions en file d'attente attendront indéfiniment jusqu'à ce qu'un thread PHP soit disponible pour les servir. Pour éviter cela, vous pouvez utiliser la [configuration](config.md#configuration-du-caddyfile) `max_wait_time` dans la configuration globale de FrankenPHP pour limiter la durée pendant laquelle une requête peut attendre un thread PHP libre avant d'être rejetée.\nEn outre, vous pouvez définir un [délai d'écriture dans Caddy](https://caddyserver.com/docs/caddyfile/options#timeouts) raisonnable.\n\nChaque instance de Caddy n'utilisera qu'un seul pool de threads FrankenPHP, qui sera partagé par tous les blocs `php_server`.\n"
  },
  {
    "path": "docs/fr/compile.md",
    "content": "# Compiler depuis les sources\n\nCe document explique comment créer un build FrankenPHP qui chargera PHP en tant que bibliothèque dynamique.\nC'est la méthode recommandée.\n\nAlternativement, il est aussi possible de [créer des builds statiques](static.md).\n\n## Installer PHP\n\nFrankenPHP est compatible avec PHP 8.2 et versions ultérieures.\n\n### Avec Homebrew (Linux et Mac)\n\nLa manière la plus simple d'installer une version de libphp compatible avec FrankenPHP est d'utiliser les paquets ZTS fournis par [Homebrew PHP](https://github.com/shivammathur/homebrew-php).\n\nTout d'abord, si ce n'est déjà fait, installez [Homebrew](https://brew.sh).\n\nEnsuite, installez la variante ZTS de PHP, Brotli (facultatif, pour la prise en charge de la compression) et watcher (facultatif, pour la détection des modifications de fichiers) :\n\n```console\nbrew install shivammathur/php/php-zts brotli watcher\nbrew link --overwrite --force shivammathur/php/php-zts\n```\n\n### En compilant PHP\n\nVous pouvez également compiler PHP à partir des sources avec les options requises par FrankenPHP en suivant ces étapes.\n\nTout d'abord, [téléchargez les sources de PHP](https://www.php.net/downloads.php) et extrayez-les :\n\n```console\ntar xf php-*\ncd php-*/\n```\n\nEnsuite, configurez PHP pour votre système d'exploitation.\n\nLes options de configuration suivantes sont nécessaires pour la compilation, mais vous pouvez également inclure d'autres options selon vos besoins, par exemple pour ajouter des extensions et fonctionnalités supplémentaires.\n\n### Linux\n\n```console\n./configure \\\n    --enable-embed \\\n    --enable-zts \\\n    --disable-zend-signals \\\n    --enable-zend-max-execution-timers\n```\n\n### Mac\n\nUtilisez le gestionnaire de paquets [Homebrew](https://brew.sh/) pour installer les dépendances obligatoires et optionnelles :\n\n```console\nbrew install libiconv bison brotli re2c pkg-config watcher\necho 'export PATH=\"/opt/homebrew/opt/bison/bin:$PATH\"' >> ~/.zshrc\n```\n\nPuis exécutez le script de configuration :\n\n```console\n./configure \\\n    --enable-embed \\\n    --enable-zts \\\n    --disable-zend-signals \\\n    --disable-opcache-jit \\\n    --with-iconv=/opt/homebrew/opt/libiconv/\n```\n\n### Compilez PHP\n\nFinalement, compilez et installez PHP :\n\n```console\nmake -j\"$(getconf _NPROCESSORS_ONLN)\"\nsudo make install\n```\n\n## Installez les dépendances optionnelles\n\nCertaines fonctionnalités de FrankenPHP nécessitent des dépendances optionnelles qui doivent être installées.\nCes fonctionnalités peuvent également être désactivées en passant des tags de compilation au compilateur Go.\n\n| Fonctionnalité                                          | Dépendance                                                            | Tag de compilation pour la désactiver |\n| ------------------------------------------------------- | --------------------------------------------------------------------- | ------------------------------------- |\n| Compression Brotli                                      | [Brotli](https://github.com/google/brotli)                            | nobrotli                              |\n| Redémarrage des workers en cas de changement de fichier | [Watcher C](https://github.com/e-dant/watcher/tree/release/watcher-c) | nowatcher                             |\n\n## Compiler l'application Go\n\n### Utiliser xcaddy\n\nLa méthode recommandée consiste à utiliser [xcaddy](https://github.com/caddyserver/xcaddy) pour compiler FrankenPHP.\n`xcaddy` permet également d'ajouter facilement des [modules Caddy personnalisés](https://caddyserver.com/docs/modules/) et des extensions FrankenPHP :\n\n```console\nCGO_ENABLED=1 \\\nXCADDY_GO_BUILD_FLAGS=\"-ldflags='-w -s' -tags=nobadger,nomysql,nopgx\" \\\nCGO_CFLAGS=$(php-config --includes) \\\nCGO_LDFLAGS=\"$(php-config --ldflags) $(php-config --libs)\" \\\nxcaddy build \\\n    --output frankenphp \\\n    --with github.com/dunglas/frankenphp/caddy \\\n    --with github.com/dunglas/caddy-cbrotli \\\n    --with github.com/dunglas/mercure/caddy \\\n    --with github.com/dunglas/vulcain/caddy\n    # Ajoutez les modules Caddy supplémentaires et les extensions FrankenPHP ici\n```\n\n> [!TIP]\n>\n> Si vous utilisez musl libc (la bibliothèque par défaut sur Alpine Linux) et Symfony,\n> vous pourriez avoir besoin d'augmenter la taille par défaut de la pile.\n> Sinon, vous pourriez rencontrer des erreurs telles que `PHP Fatal error: Maximum call stack size of 83360 bytes reached during compilation. Try splitting expression`\n>\n> Pour ce faire, modifiez la variable d'environnement `XCADDY_GO_BUILD_FLAGS` en quelque chose comme\n> `XCADDY_GO_BUILD_FLAGS=$'-ldflags \"-w -s -extldflags \\'-Wl,-z,stack-size=0x80000\\'\"'`\n> (modifiez la valeur de la taille de la pile selon les besoins de votre application).\n\n### Sans xcaddy\n\nIl est également possible de compiler FrankenPHP sans `xcaddy` en utilisant directement la commande `go` :\n\n```console\ncurl -L https://github.com/php/frankenphp/archive/refs/heads/main.tar.gz | tar xz\ncd frankenphp-main/caddy/frankenphp\nCGO_CFLAGS=$(php-config --includes) CGO_LDFLAGS=\"$(php-config --ldflags) $(php-config --libs)\" go build -tags=nobadger,nomysql,nopgx\n```\n"
  },
  {
    "path": "docs/fr/config.md",
    "content": "# Configuration\n\nFrankenPHP, Caddy ainsi que les modules [Mercure](mercure.md) et [Vulcain](https://vulcain.rocks) peuvent être configurés à l'aide [des formats pris en charge par Caddy](https://caddyserver.com/docs/getting-started#your-first-config).\n\nLe format le plus courant est le `Caddyfile`, un format texte simple et facilement lisible par les humains.\nPar défaut, FrankenPHP recherchera un `Caddyfile` dans le répertoire courant.\nVous pouvez spécifier un chemin personnalisé avec l'option `-c` ou `--config`.\n\nUn `Caddyfile` minimal pour servir une application PHP est présenté ci-dessous :\n\n```caddyfile\n# Le nom d'hôte auquel répondre\nlocalhost\n\n# Optionnellement, le répertoire à partir duquel servir les fichiers, sinon le répertoire courant sera utilisé par défaut\n#root public/\nphp_server\n```\n\nUn `Caddyfile` plus avancé, activant davantage de fonctionnalités et fournissant des variables d'environnement pratiques, est disponible [dans le dépôt FrankenPHP](https://github.com/php/frankenphp/blob/main/caddy/frankenphp/Caddyfile) et avec les images Docker.\n\nPHP lui-même peut être configuré [en utilisant un fichier `php.ini`](https://www.php.net/manual/fr/configuration.file.php).\n\nSelon votre méthode d'installation, FrankenPHP et l'interpréteur PHP chercheront les fichiers de configuration aux emplacements décrits ci-dessous.\n\n## Docker\n\nFrankenPHP :\n\n- `/etc/frankenphp/Caddyfile` : le fichier de configuration principal\n- `/etc/frankenphp/Caddyfile.d/*.caddyfile` : fichiers de configuration additionnels qui sont chargés automatiquement\n\nPHP :\n\n- `php.ini` : `/usr/local/etc/php/php.ini` (aucun `php.ini` n'est fourni par défaut)\n- Fichiers de configuration supplémentaires : `/usr/local/etc/php/conf.d/*.ini`\n- Extensions PHP : `/usr/local/lib/php/extensions/no-debug-zts-<YYYYMMDD>/`\n- Vous devriez copier un modèle officiel fourni par le projet PHP :\n\n```dockerfile\nFROM dunglas/frankenphp\n\n# Production :\nRUN cp $PHP_INI_DIR/php.ini-production $PHP_INI_DIR/php.ini\n\n# Ou développement :\nRUN cp $PHP_INI_DIR/php.ini-development $PHP_INI_DIR/php.ini\n```\n\n## Packages RPM et Debian\n\nFrankenPHP :\n\n- `/etc/frankenphp/Caddyfile` : le fichier de configuration principal\n- `/etc/frankenphp/Caddyfile.d/*.caddyfile` : fichiers de configuration additionnels qui sont chargés automatiquement\n\nPHP :\n\n- `php.ini` : `/etc/php-zts/php.ini` (un fichier `php.ini` avec des préréglages de production est fourni par défaut)\n- fichiers de configuration supplémentaires : `/etc/php-zts/conf.d/*.ini`\n\n## Binaire statique\n\nFrankenPHP :\n\n- Dans le répertoire de travail actuel : `Caddyfile`\n\nPHP :\n\n- `php.ini` : Le répertoire dans lequel `frankenphp run` ou `frankenphp php-server` est exécuté, puis `/etc/frankenphp/php.ini`\n- fichiers de configuration supplémentaires : `/etc/frankenphp/php.d/*.ini`\n- Extensions PHP : ne peuvent pas être chargées, intégrez-les au binaire lui-même\n- copiez l'un des fichiers `php.ini-production` ou `php.ini-development` fournis [dans les sources de PHP](https://github.com/php/php-src/).\n\n## Configuration du Caddyfile\n\nLes [directives HTTP](https://caddyserver.com/docs/caddyfile/concepts#directives) `php_server` ou `php` peuvent être utilisées dans les blocs de site pour servir votre application PHP.\n\nExemple minimal :\n\n```caddyfile\nlocalhost {\n\t# Activer la compression (optionnel)\n\tencode zstd br gzip\n\t# Exécuter les fichiers PHP dans le répertoire courant et servir les assets\n\tphp_server\n}\n```\n\nVous pouvez également configurer explicitement FrankenPHP en utilisant l'[option globale](https://caddyserver.com/docs/caddyfile/concepts#global-options) `frankenphp` :\n\n```caddyfile\n{\n\tfrankenphp {\n\t\tnum_threads <num_threads> # Définit le nombre de threads PHP à démarrer. Par défaut : 2x le nombre de CPUs disponibles.\n\t\tmax_threads <num_threads> # Limite le nombre de threads PHP supplémentaires qui peuvent être démarrés au moment de l'exécution. Valeur par défaut : num_threads. Peut être mis à 'auto'.\n\t\tmax_wait_time <duration> # Définit le temps maximum pendant lequel une requête peut attendre un thread PHP libre avant d'être interrompue. Valeur par défaut : désactivé.\n\t\tmax_idle_time <duration> # Définit le temps maximum pendant lequel un thread auto-dimensionné peut être inactif avant d'être désactivé. Par défaut : 5s.\n\t\tphp_ini <key> <value> # Définit une directive php.ini. Peut être utilisé plusieurs fois pour définir plusieurs directives.\n\t\tworker {\n\t\t\tfile <path> # Définit le chemin vers le script worker.\n\t\t\tnum <num> # Définit le nombre de threads PHP à démarrer, par défaut 2x le nombre de CPUs disponibles.\n\t\t\tenv <key> <value> # Définit une variable d'environnement supplémentaire avec la valeur donnée. Peut être spécifié plusieurs fois pour régler plusieurs variables d'environnement.\n\t\t\twatch <path> # Définit le chemin d'accès à surveiller pour les modifications de fichiers. Peut être spécifié plusieurs fois pour plusieurs chemins.\n\t\t\tname <name> # Définit le nom du worker, utilisé dans les journaux et les métriques. Par défaut : chemin absolu du fichier du worker\n\t\t\tmax_consecutive_failures <num> # Définit le nombre maximum d'échecs consécutifs avant que le worker ne soit considéré comme défaillant, -1 signifie que le worker redémarre toujours. Par défaut : 6.\n\t\t}\n\t}\n}\n\n# ...\n```\n\nVous pouvez également utiliser la forme courte de l'option `worker` en une seule ligne :\n\n```caddyfile\n{\n\tfrankenphp {\n\t\tworker <file> <num>\n\t}\n}\n\n# ...\n```\n\nVous pouvez aussi définir plusieurs workers si vous servez plusieurs applications sur le même serveur :\n\n```caddyfile\napp.example.com {\n    root /path/to/app/public\n\tphp_server {\n\t\troot /path/to/app/public # permet une meilleure mise en cache\n\t\tworker index.php <num>\n\t}\n}\n\nother.example.com {\n    root /path/to/other/public\n\tphp_server {\n\t\troot /path/to/other/public\n\t\tworker index.php <num>\n\t}\n}\n\n# ...\n```\n\nL'utilisation de la directive `php_server` est généralement ce dont vous avez besoin,\nmais si vous avez besoin d'un contrôle total, vous pouvez utiliser la sous-directive `php`.\nLa directive `php` transmet toutes les entrées à PHP, au lieu de vérifier d'abord si\nc'est un fichier PHP ou pas. En savoir plus à ce sujet dans la [documentation liée aux performances](performance.md#try_files).\n\nUtiliser la directive `php_server` est équivalent à cette configuration :\n\n```caddyfile\nroute {\n\t# Ajoute un slash final pour les requêtes de répertoire\n\t@canonicalPath {\n\t\tfile {path}/index.php\n\t\tnot path */\n\t}\n\tredir @canonicalPath {path}/ 308\n\t# Si le fichier demandé n'existe pas, essayer les fichiers index\n\t@indexFiles file {\n\t\ttry_files {path} {path}/index.php index.php\n\t\tsplit_path .php\n\t}\n\trewrite @indexFiles {http.matchers.file.relative}\n\t# FrankenPHP!\n\t@phpFiles path *.php\n\tphp @phpFiles\n\tfile_server\n}\n```\n\nLes directives `php_server` et `php` disposent des options suivantes :\n\n```caddyfile\nphp_server [<matcher>] {\n\troot <directory> # Définit le dossier racine du site. Par défaut : la directive `root`.\n\tsplit_path <delim...> # Définit les sous-chaînes pour diviser l'URI en deux parties. La première sous-chaîne correspondante sera utilisée pour séparer le \"path info\" du chemin. La première partie est suffixée avec la sous-chaîne correspondante et sera considérée comme le nom réel de la ressource (script CGI). La seconde partie sera définie comme PATH_INFO pour utilisation par le script. Par défaut : `.php`\n\tresolve_root_symlink false # Désactive la résolution du répertoire `root` vers sa valeur réelle en évaluant un lien symbolique, s'il existe (activé par défaut).\n\tenv <key> <value> # Définit une variable d'environnement supplémentaire avec la valeur donnée. Peut être spécifié plusieurs fois pour plusieurs variables d'environnement.\n\tfile_server off # Désactive la directive file_server intégrée.\n\tworker { # Crée un worker spécifique à ce serveur. Peut être spécifié plusieurs fois pour plusieurs workers.\n\t\tfile <path> # Définit le chemin vers le script worker, peut être relatif à la racine du php_server\n\t\tnum <num> # Définit le nombre de threads PHP à démarrer, par défaut 2x le nombre de CPUs disponibles\n\t\tname <name> # Définit le nom du worker, utilisé dans les journaux et les métriques. Par défaut : chemin absolu du fichier du worker. Commence toujours par m# lorsqu'il est défini dans un bloc php_server.\n\t\twatch <path> # Définit le chemin d'accès à surveiller pour les modifications de fichiers. Peut être spécifié plusieurs fois pour plusieurs chemins.\n\t\tenv <key> <value> # Définit une variable d'environnement supplémentaire avec la valeur donnée. Peut être spécifié plusieurs fois pour plusieurs variables d'environnement. Les variables d'environnement pour ce worker sont également héritées du parent php_server, mais peuvent être écrasées ici.\n\t\tmatch <path> # fait correspondre le worker à un modèle de chemin. Écrase try_files et ne peut être utilisé que dans la directive php_server.\n\t}\n\tworker <other_file> <num> # Peut également utiliser la forme courte comme dans le bloc frankenphp global.\n}\n```\n\n### Surveillance des modifications de fichier\n\nVu que les workers ne démarrent votre application qu'une seule fois et la gardent en mémoire, toute modification\napportée à vos fichiers PHP ne sera pas répercutée immédiatement.\n\nLes workers peuvent être redémarrés en cas de changement de fichier via la directive `watch`.\nCeci est utile pour les environnements de développement.\n\n```caddyfile\n{\n\tfrankenphp {\n\t\tworker {\n\t\t\tfile  /path/to/app/public/worker.php\n\t\t\twatch\n\t\t}\n\t}\n}\n```\n\nCette fonctionnalité est souvent utilisée en combinaison avec [le rechargement à chaud](hot-reload.md).\n\nSi le répertoire `watch` n'est pas précisé, il se rabattra sur `./**/*.{env,php,twig,yaml,yml}`,\nqui surveille tous les fichiers `.env`, `.php`, `.twig`, `.yaml` et `.yml` dans le répertoire et les sous-répertoires\noù le processus FrankenPHP a été lancé. Vous pouvez également spécifier un ou plusieurs répertoires via un\n[motif de nom de fichier shell](https://pkg.go.dev/path/filepath#Match) :\n\n```caddyfile\n{\n\tfrankenphp {\n\t\tworker {\n\t\t\tfile  /path/to/app/public/worker.php\n\t\t\twatch /path/to/app # surveille tous les fichiers dans tous les sous-répertoires de /path/to/app\n\t\t\twatch /path/to/app/*.php # surveille les fichiers se terminant par .php dans /path/to/app\n\t\t\twatch /path/to/app/**/*.php # surveille les fichiers PHP dans /path/to/app et les sous-répertoires\n\t\t\twatch /path/to/app/**/*.{php,twig} # surveille les fichiers PHP et Twig dans /path/to/app et les sous-répertoires\n\t\t}\n\t}\n}\n```\n\n- Le motif `**` signifie une surveillance récursive.\n- Les répertoires peuvent également être relatifs (depuis l'endroit où le processus FrankenPHP est démarré).\n- Si vous avez défini plusieurs workers, ils seront tous redémarrés lorsqu'un fichier est modifié.\n- Méfiez-vous des fichiers créés au moment de l'exécution (comme les logs) car ils peuvent provoquer des redémarrages intempestifs du worker.\n\nLa surveillance des fichiers est basée sur [e-dant/watcher](https://github.com/e-dant/watcher).\n\n## Faire correspondre le Worker à un chemin\n\nDans les applications PHP traditionnelles, les scripts sont toujours placés dans le répertoire public. C'est également vrai pour les scripts worker, qui sont traités comme n'importe quel autre script PHP. Si vous souhaitez plutôt placer le script worker en dehors du répertoire public, vous pouvez le faire via la directive `match`.\n\nLa directive `match` est une alternative optimisée à `try_files` disponible uniquement à l'intérieur de `php_server` et `php`. L'exemple suivant servira toujours un fichier dans le répertoire public s'il est présent\net transmettra sinon la requête au worker correspondant au modèle de chemin.\n\n```caddyfile\n{\n\tfrankenphp {\n\t\tphp_server {\n\t\t\tworker {\n\t\t\t\tfile /path/to/worker.php # le fichier peut être en dehors du chemin public\n\t\t\t\tmatch /api/* # toutes les requêtes commençant par /api/ seront traitées par ce worker\n\t\t\t}\n\t\t}\n\t}\n}\n```\n\n## Variables d'environnement\n\nLes variables d'environnement suivantes peuvent être utilisées pour insérer des directives Caddy dans le `Caddyfile` sans le modifier :\n\n- `SERVER_NAME` : change [les adresses sur lesquelles écouter](https://caddyserver.com/docs/caddyfile/concepts#addresses), les noms d'hôte fournis seront également utilisés pour le certificat TLS généré\n- `SERVER_ROOT` : change le répertoire racine du site, par défaut `public/`\n- `CADDY_GLOBAL_OPTIONS` : injecte [des options globales](https://caddyserver.com/docs/caddyfile/options)\n- `FRANKENPHP_CONFIG` : insère la configuration sous la directive `frankenphp`\n\nComme pour les SAPI FPM et CLI, les variables d'environnement sont exposées par défaut dans la superglobale `$_SERVER`.\n\nLa valeur `S` de [la directive `variables_order` de PHP](https://www.php.net/manual/fr/ini.core.php#ini.variables-order) est toujours équivalente à `ES`, que `E` soit défini ailleurs dans cette directive ou non.\n\n## Configuration PHP\n\nPour charger [des fichiers de configuration PHP supplémentaires](https://www.php.net/manual/fr/configuration.file.php#configuration.file.scan),\nla variable d'environnement `PHP_INI_SCAN_DIR` peut être utilisée.\nLorsqu'elle est définie, PHP chargera tous les fichiers avec l'extension `.ini` présents dans les répertoires donnés.\n\nVous pouvez également modifier la configuration de PHP en utilisant la directive `php_ini` dans le fichier `Caddyfile` :\n\n```caddyfile\n{\n    frankenphp {\n        php_ini memory_limit 256M\n\n        # or\n\n        php_ini {\n            memory_limit 256M\n            max_execution_time 15\n        }\n    }\n}\n```\n\n### Désactiver HTTPS\n\nPar défaut, FrankenPHP activera automatiquement HTTPS pour tous les noms d'hôte, y compris `localhost`. Si vous souhaitez désactiver HTTPS (par exemple dans un environnement de développement), vous pouvez définir la variable d'environnement `SERVER_NAME` à `http://` ou `:80` :\n\nAlternativement, vous pouvez utiliser toutes les autres méthodes décrites dans la [documentation Caddy](https://caddyserver.com/docs/automatic-https#activation).\n\nSi vous souhaitez utiliser HTTPS avec l'adresse IP `127.0.0.1` au lieu du nom d'hôte `localhost`, veuillez lire la section [problèmes connus](known-issues.md#using-https127001-with-docker).\n\n### Full Duplex (HTTP/1)\n\nLors de l'utilisation de HTTP/1.x, il peut être souhaitable d'activer le mode full-duplex pour permettre l'écriture d'une réponse avant que le corps entier\nn'ait été lu. (par exemple : [Mercure](mercure.md), WebSocket, Server-Sent Events, etc.)\n\nIl s'agit d'une configuration facultative qui doit être ajoutée aux options globales dans le `Caddyfile` :\n\n```caddyfile\n{\n  servers {\n    enable_full_duplex\n  }\n}\n```\n\n> [!CAUTION]\n>\n> L'activation de cette option peut entraîner un blocage (deadlock) des anciens clients HTTP/1.x qui ne supportent pas le full-duplex.\n> Cela peut aussi être configuré en utilisant la variable d'environnement `CADDY_GLOBAL_OPTIONS` :\n\n```sh\nCADDY_GLOBAL_OPTIONS=\"servers {\n  enable_full_duplex\n}\"\n```\n\nVous trouverez plus d'informations sur ce paramètre dans la [documentation Caddy](https://caddyserver.com/docs/caddyfile/options#enable-full-duplex).\n\n## Activer le mode Debug\n\nLors de l'utilisation de l'image Docker, définissez la variable d'environnement `CADDY_GLOBAL_OPTIONS` sur `debug` pour activer le mode debug :\n\n```console\ndocker run -v $PWD:/app/public \\\n    -e CADDY_GLOBAL_OPTIONS=debug \\\n    -p 80:80 -p 443:443 -p 443:443/udp \\\n    dunglas/frankenphp\n```\n\n## Autocomplétion Shell\n\nFrankenPHP fournit un support d'autocomplétion intégré pour Bash, Zsh, Fish et PowerShell. Cela permet l'autocomplétion de toutes les commandes (y compris les commandes personnalisées comme `php-server`, `php-cli` et `extension-init`) ainsi que leurs options.\n\n### Bash\n\nPour charger l'autocomplétion dans votre session shell actuelle :\n\n```console\nsource <(frankenphp completion bash)\n```\n\nPour charger l'autocomplétion à chaque nouvelle session, exécutez :\n\n**Linux :**\n\n```console\nfrankenphp completion bash > /usr/share/bash-completion/completions/frankenphp\n```\n\n**macOS :**\n\n```console\nfrankenphp completion bash > $(brew --prefix)/share/bash-completion/completions/frankenphp\n```\n\n### Zsh\n\nSi l'autocomplétion shell n'est pas déjà activée dans votre environnement, vous devrez l'activer. Vous pouvez exécuter la commande suivante une fois :\n\n```console\necho \"autoload -U compinit; compinit\" >> ~/.zshrc\n```\n\nPour charger l'autocomplétion à chaque session, exécutez une fois :\n\n```console\nfrankenphp completion zsh > \"${fpath[1]}/_frankenphp\"\n```\n\nVous devrez démarrer un nouveau shell pour que cette configuration prenne effet.\n\n### Fish\n\nPour charger l'autocomplétion dans votre session shell actuelle :\n\n```console\nfrankenphp completion fish | source\n```\n\nPour charger l'autocomplétion à chaque nouvelle session, exécutez une fois :\n\n```console\nfrankenphp completion fish > ~/.config/fish/completions/frankenphp.fish\n```\n\n### PowerShell\n\nPour charger l'autocomplétion dans votre session shell actuelle :\n\n```powershell\nfrankenphp completion powershell | Out-String | Invoke-Expression\n```\n\nPour charger l'autocomplétion à chaque nouvelle session, exécutez une fois :\n\n```powershell\nfrankenphp completion powershell | Out-File -FilePath (Join-Path (Split-Path $PROFILE) \"frankenphp.ps1\")\nAdd-Content -Path $PROFILE -Value '. (Join-Path (Split-Path $PROFILE) \"frankenphp.ps1\")'\n```\n\nVous devrez démarrer un nouveau shell pour que cette configuration prenne effet.\n\nVous devrez ensuite démarrer un nouveau shell pour que cette configuration prenne effet.\n"
  },
  {
    "path": "docs/fr/docker.md",
    "content": "# Création d'une image Docker personnalisée\n\nLes images Docker de [FrankenPHP](https://hub.docker.com/r/dunglas/frankenphp) sont basées sur les [images PHP officielles](https://hub.docker.com/_/php/). Des variantes Debian et Alpine Linux sont fournies pour les architectures populaires. Les variantes Debian sont recommandées.\n\nDes variantes pour PHP 8.2, 8.3, 8.4 et 8.5 sont disponibles. [Parcourir les tags](https://hub.docker.com/r/dunglas/frankenphp/tags).\n\nLes tags suivent le pattern suivant : `dunglas/frankenphp:<frankenphp-version>-php<php-version>-<os>`\n\n- `<frankenphp-version>` et `<php-version>` sont respectivement les numéros de version de FrankenPHP et PHP, allant de majeur (e.g. `1`), mineur (e.g. `1.2`) à des versions correctives (e.g. `1.2.3`).\n- `<os>` est soit `trixie` (pour Debian Trixie), `bookworm` (pour Debian Bookworm), ou `alpine` (pour la dernière version stable d'Alpine).\n\n[Parcourir les tags](https://hub.docker.com/r/dunglas/frankenphp/tags).\n\n## Comment utiliser les images\n\nCréez un `Dockerfile` dans votre projet :\n\n```dockerfile\nFROM dunglas/frankenphp\n\nCOPY . /app/public\n```\n\nEnsuite, exécutez ces commandes pour construire et exécuter l'image Docker :\n\n```console\ndocker build -t my-php-app .\ndocker run -it --rm --name my-running-app my-php-app\n```\n\n## Comment ajuster la configuration\n\nPour une meilleure expérience initiale, un [`Caddyfile` par défaut](https://github.com/php/frankenphp/blob/main/caddy/frankenphp/Caddyfile) contenant des variables d'environnement communément utilisées est fourni dans l'image.\n\n## Comment installer plus d'extensions PHP\n\nLe script [`docker-php-extension-installer`](https://github.com/mlocati/docker-php-extension-installer) est fourni dans l'image de base. L'ajout d'extensions PHP supplémentaires se fait de cette manière :\n\n```dockerfile\nFROM dunglas/frankenphp\n\n# ajoutez des extensions supplémentaires ici :\nRUN install-php-extensions \\\n\tpdo_mysql \\\n\tgd \\\n\tintl \\\n\tzip \\\n\topcache\n```\n\n## Comment installer plus de modules Caddy\n\nFrankenPHP est construit sur Caddy, et tous les [modules Caddy](https://caddyserver.com/docs/modules/) peuvent être utilisés avec FrankenPHP.\n\nLa manière la plus simple d'installer des modules Caddy personnalisés est d'utiliser [xcaddy](https://github.com/caddyserver/xcaddy) :\n\n```dockerfile\nFROM dunglas/frankenphp:builder AS builder\n\n# Copier xcaddy dans l'image du constructeur\nCOPY --from=caddy:builder /usr/bin/xcaddy /usr/bin/xcaddy\n\n# CGO doit être activé pour construire FrankenPHP\nRUN CGO_ENABLED=1 \\\n    XCADDY_SETCAP=1 \\\n    XCADDY_GO_BUILD_FLAGS=\"-ldflags='-w -s' -tags=nobadger,nomysql,nopgx\" \\\n    CGO_CFLAGS=$(php-config --includes) \\\n    CGO_LDFLAGS=\"$(php-config --ldflags) $(php-config --libs)\" \\\n    xcaddy build \\\n        --output /usr/local/bin/frankenphp \\\n        --with github.com/dunglas/frankenphp=./ \\\n        --with github.com/dunglas/frankenphp/caddy=./caddy/ \\\n        --with github.com/dunglas/caddy-cbrotli \\\n        # Mercure et Vulcain sont inclus dans la construction officielle, mais n'hésitez pas à les retirer\n        --with github.com/dunglas/mercure/caddy \\\n        --with github.com/dunglas/vulcain/caddy\n        # Ajoutez des modules Caddy supplémentaires ici\n\nFROM dunglas/frankenphp AS runner\n\n# Remplacer le binaire officiel par celui contenant vos modules personnalisés\nCOPY --from=builder /usr/local/bin/frankenphp /usr/local/bin/frankenphp\n```\n\nL'image `builder` fournie par FrankenPHP contient une version compilée de `libphp`.\n[Les images builder](https://hub.docker.com/r/dunglas/frankenphp/tags?name=builder) sont fournies pour toutes les versions de FrankenPHP et PHP, à la fois pour Debian et Alpine.\n\n> [!TIP]\n>\n> Si vous utilisez Alpine Linux et Symfony,\n> vous devrez peut-être [augmenter la taille de pile par défaut](compile.md#utiliser-xcaddy).\n\n## Activer le mode Worker par défaut\n\nDéfinissez la variable d'environnement `FRANKENPHP_CONFIG` pour démarrer FrankenPHP avec un script worker :\n\n```dockerfile\nFROM dunglas/frankenphp\n\n# ...\n\nENV FRANKENPHP_CONFIG=\"worker ./public/index.php\"\n```\n\n## Utiliser un volume en développement\n\nPour développer facilement avec FrankenPHP, montez le répertoire de l'hôte contenant le code source de l'application comme un volume dans le conteneur Docker :\n\n```console\ndocker run -v $PWD:/app/public -p 80:80 -p 443:443 -p 443:443/udp --tty my-php-app\n```\n\n> [!TIP]\n>\n> L'option --tty permet d'avoir des logs lisibles par un humain au lieu de logs JSON.\n\nAvec Docker Compose :\n\n```yaml\n# compose.yaml\n\nservices:\n  php:\n    image: dunglas/frankenphp\n    # décommentez la ligne suivante si vous souhaitez utiliser un Dockerfile personnalisé\n    #build: .\n    # décommentez la ligne suivante si vous souhaitez exécuter ceci dans un environnement de production\n    # restart: always\n    ports:\n      - \"80:80\" # HTTP\n      - \"443:443\" # HTTPS\n      - \"443:443/udp\" # HTTP/3\n    volumes:\n      - ./:/app/public\n      - caddy_data:/data\n      - caddy_config:/config\n    # commentez la ligne suivante en production, elle permet d'avoir de beaux logs lisibles en dev\n    tty: true\n\n# Volumes nécessaires pour les certificats et la configuration de Caddy\nvolumes:\n  caddy_data:\n  caddy_config:\n```\n\n## Exécution en tant qu'utilisateur non-root\n\nFrankenPHP peut s'exécuter en tant qu'utilisateur non-root dans Docker.\n\nVoici un exemple de `Dockerfile` le permettant :\n\n```dockerfile\nFROM dunglas/frankenphp\n\nARG USER=appuser\n\nRUN \\\n\t# Utilisez \"adduser -D ${USER}\" pour les distributions basées sur Alpine\n\tuseradd ${USER}; \\\n\t# Ajouter la capacité supplémentaire de se lier aux ports 80 et 443\n\tsetcap CAP_NET_BIND_SERVICE=+eip /usr/local/bin/frankenphp; \\\n\t# Donner l'accès en écriture à /config/caddy et /data/caddy\n\tchown -R ${USER}:${USER} /config/caddy /data/caddy\n\nUSER ${USER}\n```\n\n### Exécution sans capacité\n\nMême lorsqu'il s'exécute en tant qu'utilisateur non-root, FrankenPHP a besoin de la capacité `CAP_NET_BIND_SERVICE` pour lier le serveur web sur les ports privilégiés (80 et 443).\n\nSi vous exposez FrankenPHP sur un port non privilégié (1024 et au-delà), il est possible d'exécuter le serveur web en tant qu'utilisateur non-root, et sans avoir besoin d'aucune capacité :\n\n```dockerfile\nFROM dunglas/frankenphp\n\nARG USER=appuser\n\nRUN \\\n\t# Utiliser \"adduser -D ${USER}\" pour les distros basées sur Alpine\n\tuseradd ${USER}; \\\n\t# Supprimer la capacité par défaut\n\tsetcap -r /usr/local/bin/frankenphp; \\\n\t# Donner un accès en écriture à /config/caddy et /data/caddy\n\tchown -R ${USER}:${USER} /config/caddy /data/caddy\n\nUSER ${USER}\n```\n\nEnsuite, définissez la variable d'environnement `SERVER_NAME` pour utiliser un port non privilégié.\nExemple : `:8000`\n\n## Mises à jour\n\nLes images Docker sont construites :\n\n- lorsqu'une nouvelle version est taguée\n- tous les jours à 4h UTC, si de nouvelles versions des images officielles PHP sont disponibles\n\n## Renforcer la sécurité des images\n\nPour réduire davantage la surface d'attaque et la taille de vos images Docker FrankenPHP, il est également possible de les construire sur une image [Google distroless](https://github.com/GoogleContainerTools/distroless) ou [Docker hardened](https://www.docker.com/products/hardened-images).\n\n> [!WARNING]\n> Ces images de base minimales n'incluent pas de shell ou de gestionnaire de paquets, ce qui rend le débogage plus difficile. Elles sont donc recommandées uniquement pour la production si la sécurité est une priorité.\n\nLors de l'ajout d'extensions PHP supplémentaires, vous aurez besoin d'une étape de build intermédiaire :\n\n```dockerfile\nFROM dunglas/frankenphp AS builder\n\n# Ajoutez ici des extensions PHP supplémentaires\nRUN install-php-extensions pdo_mysql pdo_pgsql #...\n\n# Copiez les bibliothèques partagées de frankenphp et toutes les extensions installées vers un emplacement temporaire\n# Vous pouvez également effectuer cette étape manuellement en analysant la sortie ldd du binaire frankenphp et de chaque fichier .so d'extension\nRUN apt-get update && apt-get install -y libtree && \\\n    EXT_DIR=\"$(php -r 'echo ini_get(\"extension_dir\");')\" && \\\n    FRANKENPHP_BIN=\"$(which frankenphp)\"; \\\n    LIBS_TMP_DIR=\"/tmp/libs\"; \\\n    mkdir -p \"$LIBS_TMP_DIR\"; \\\n    for target in \"$FRANKENPHP_BIN\" $(find \"$EXT_DIR\" -maxdepth 2 -type f -name \"*.so\"); do \\\n        libtree -pv \"$target\" | sed 's/.*── \\(.*\\) \\[.*/\\1/' | grep -v \"^$target\" | while IFS= read -r lib; do \\\n            [ -z \"$lib\" ] && continue; \\\n            base=$(basename \"$lib\"); \\\n            destfile=\"$LIBS_TMP_DIR/$base\"; \\\n            if [ ! -f \"$destfile\" ]; then \\\n                cp \"$lib\" \"$destfile\"; \\\n            fi; \\\n        done; \\\n    done\n\n\n# Image de base Debian distroless, assurez-vous que c'est la même version de Debian que l'image de base\nFROM gcr.io/distroless/base-debian13\n# Alternative d'image Docker renforcée\n# FROM dhi.io/debian:13\n\n# Emplacement de votre application et du Caddyfile à copier dans le conteneur\nARG PATH_TO_APP=\".\"\nARG PATH_TO_CADDYFILE=\"./Caddyfile\"\n\n# Copiez votre application dans /app\n# Pour un renforcement supplémentaire, assurez-vous que seuls les chemins accessibles en écriture sont détenus par l'utilisateur non-root\nCOPY --chown=nonroot:nonroot \"$PATH_TO_APP\" /app\nCOPY \"$PATH_TO_CADDYFILE\" /etc/caddy/Caddyfile\n\n# Copiez frankenphp et les bibliothèques nécessaires\nCOPY --from=builder /usr/local/bin/frankenphp /usr/local/bin/frankenphp\nCOPY --from=builder /usr/local/lib/php/extensions /usr/local/lib/php/extensions\nCOPY --from=builder /tmp/libs /usr/lib\n\n# Copiez les fichiers de configuration php.ini\nCOPY --from=builder /usr/local/etc/php/conf.d /usr/local/etc/php/conf.d\nCOPY --from=builder /usr/local/etc/php/php.ini-production /usr/local/etc/php/php.ini\n\n# Répertoires de données Caddy — doivent être accessibles en écriture pour l'utilisateur non-root, même sur un système de fichiers racine en lecture seule\nENV XDG_CONFIG_HOME=/config \\\n    XDG_DATA_HOME=/data\nCOPY --from=builder --chown=nonroot:nonroot /data/caddy /data/caddy\nCOPY --from=builder --chown=nonroot:nonroot /config/caddy /config/caddy\n\nUSER nonroot\n\nWORKDIR /app\n\n# Point d'entrée pour exécuter frankenphp avec le Caddyfile fourni\nENTRYPOINT [\"/usr/local/bin/frankenphp\", \"run\", \"-c\", \"/etc/caddy/Caddyfile\"]\n```\n\n## Versions de développement\n\nLes versions de développement sont disponibles dans le dépôt Docker [`dunglas/frankenphp-dev`](https://hub.docker.com/repository/docker/dunglas/frankenphp-dev). Un nouveau build est déclenché chaque fois qu'un commit est poussé sur la branche principale du dépôt GitHub.\n\nLes tags `latest*` pointent vers la tête de la branche `main`.\nLes tags sous la forme `sha-<git-commit-hash>` sont également disponibles.\n"
  },
  {
    "path": "docs/fr/early-hints.md",
    "content": "# Early Hints\n\nFrankenPHP prend nativement en charge le code de statut [103 Early Hints](https://developer.chrome.com/blog/early-hints/).\nL'utilisation des Early Hints peut améliorer le temps de chargement de vos pages web de 30 %.\n\n```php\n<?php\n\nheader('Link: </style.css>; rel=preload; as=style');\nheaders_send(103);\n\n// vos algorithmes lents et requêtes SQL 🤪\n\necho <<<'HTML'\n<!DOCTYPE html>\n<title>Hello FrankenPHP</title>\n<link rel=\"stylesheet\" href=\"style.css\">\nHTML;\n```\n\nLes Early Hints sont pris en charge à la fois par les modes \"standard\" et [worker](worker.md).\n"
  },
  {
    "path": "docs/fr/embed.md",
    "content": "# Applications PHP en tant que binaires autonomes\n\nFrankenPHP a la capacité d'incorporer le code source et les assets des applications PHP dans un binaire statique et autonome.\n\nGrâce à cette fonctionnalité, les applications PHP peuvent être distribuées en tant que binaires autonomes qui incluent l'application elle-même, l'interpréteur PHP et Caddy, un serveur web de qualité production.\n\nPour en savoir plus sur cette fonctionnalité, consultez [la présentation faite par Kévin à la SymfonyCon 2023](https://dunglas.dev/2023/12/php-and-symfony-apps-as-standalone-binaries/).\n\nPour embarquer des applications Laravel, [lisez ce point spécifique de la documentation](laravel.md#les-applications-laravel-en-tant-que-binaires-autonomes).\n\n## Préparer votre application\n\nAvant de créer le binaire autonome, assurez-vous que votre application est prête à être intégrée.\n\nVous devrez probablement :\n\n- Installer les dépendances de production de l'application\n- Dumper l'autoloader\n- Activer le mode production de votre application (si disponible)\n- Supprimer les fichiers inutiles tels que `.git` ou les tests pour réduire la taille de votre binaire final\n\nPar exemple, pour une application Symfony, lancez les commandes suivantes :\n\n```console\n# Exporter le projet pour se débarrasser de .git/, etc.\nmkdir $TMPDIR/my-prepared-app\ngit archive HEAD | tar -x -C $TMPDIR/my-prepared-app\ncd $TMPDIR/my-prepared-app\n\n# Définir les variables d'environnement appropriées\necho APP_ENV=prod > .env.local\necho APP_DEBUG=0 >> .env.local\n\n# Supprimer les tests et autres fichiers inutiles pour économiser de l'espace\n# Alternativement, ajoutez ces fichiers avec l'attribut export-ignore dans votre fichier .gitattributes\nrm -Rf tests/\n\n# Installer les dépendances\ncomposer install --ignore-platform-reqs --no-dev -a\n\n# Optimiser le .env\ncomposer dump-env prod\n```\n\n### Personnaliser la configuration\n\nPour personnaliser [la configuration](config.md),\nvous pouvez mettre un fichier `Caddyfile` ainsi qu'un fichier `php.ini`\ndans le répertoire principal de l'application à intégrer\n(`$TMPDIR/my-prepared-app` dans l'exemple précédent).\n\n## Créer un binaire Linux\n\nLa manière la plus simple de créer un binaire Linux est d'utiliser le builder basé sur Docker que nous fournissons.\n\n1. Créez un fichier nommé `static-build.Dockerfile` dans le répertoire de votre application préparée :\n\n   ```dockerfile\n   FROM --platform=linux/amd64 dunglas/frankenphp:static-builder-gnu\n   # Si vous envisagez d'exécuter le binaire sur des systèmes musl-libc, utilisez plutôt static-builder-musl\n\n   # Copy your app\n   WORKDIR /go/src/app/dist/app\n   COPY . .\n\n   WORKDIR /go/src/app/\n   RUN EMBED=dist/app/ ./build-static.sh\n   ```\n\n   > [!CAUTION]\n   >\n   > Certains fichiers `.dockerignore` (par exemple celui fourni par défaut par [Symfony Docker](https://github.com/dunglas/symfony-docker/blob/main/.dockerignore))\n   > empêchent la copie du dossier `vendor/` et des fichiers `.env`. Assurez-vous d'ajuster ou de supprimer le fichier `.dockerignore` avant le build.\n\n2. Construisez:\n\n   ```console\n   docker build -t static-app -f static-build.Dockerfile .\n   ```\n\n3. Extrayez le binaire :\n\n   ```console\n   docker cp $(docker create --name static-app-tmp static-app):/go/src/app/dist/frankenphp-linux-x86_64 my-app ; docker rm static-app-tmp\n   ```\n\nLe binaire généré sera nommé `my-app` dans le répertoire courant.\n\n## Créer un binaire pour d'autres systèmes d'exploitation\n\nSi vous ne souhaitez pas utiliser Docker, ou souhaitez construire un binaire macOS, utilisez le script shell que nous fournissons :\n\n```console\ngit clone https://github.com/php/frankenphp\ncd frankenphp\nEMBED=/path/to/your/app ./build-static.sh\n```\n\nLe binaire obtenu est le fichier nommé `frankenphp-<os>-<arch>` dans le répertoire `dist/`.\n\n## Utiliser le binaire\n\nC'est tout ! Le fichier `my-app` (ou `dist/frankenphp-<os>-<arch>` sur d'autres systèmes d'exploitation) contient votre application autonome !\n\nPour démarrer l'application web, exécutez :\n\n```console\n./my-app php-server\n```\n\nSi votre application contient un [script worker](worker.md), démarrez le worker avec quelque chose comme :\n\n```console\n./my-app php-server --worker public/index.php\n```\n\nPour activer HTTPS (un certificat Let's Encrypt est automatiquement créé), HTTP/2 et HTTP/3, spécifiez le nom de domaine à utiliser :\n\n```console\n./my-app php-server --domain localhost\n```\n\nVous pouvez également exécuter les scripts CLI PHP incorporés dans votre binaire :\n\n```console\n./my-app php-cli bin/console\n```\n\n## Extensions PHP\n\nPar défaut, le script construira les extensions requises par le fichier `composer.json` de votre projet, s'il y en a.\nSi le fichier `composer.json` n'existe pas, les extensions par défaut sont construites, comme documenté dans [Créer un binaire statique](static.md).\n\nPour personnaliser les extensions, utilisez la variable d'environnement `PHP_EXTENSIONS`.\n\n```console\nEMBED=/path/to/your/app \\\nPHP_EXTENSIONS=ctype,iconv,pdo_sqlite \\\n./build-static.sh\n```\n\n## Personnaliser la compilation\n\n[Consultez la documentation sur la compilation statique](static.md) pour voir comment personnaliser le binaire (extensions, version PHP...).\n\n## Distribuer le binaire\n\nSous Linux, le binaire est compressé par défaut à l'aide de [UPX](https://upx.github.io).\n\nSous Mac, pour réduire la taille du fichier avant de l'envoyer, vous pouvez le compresser.\nNous recommandons `xz`.\n"
  },
  {
    "path": "docs/fr/extension-workers.md",
    "content": "# Workers d'extension\n\nLes Workers d'extension permettent à votre [extension FrankenPHP](https://frankenphp.dev/docs/extensions/) de gérer un pool dédié de threads PHP pour exécuter des tâches en arrière-plan, gérer des événements asynchrones ou implémenter des protocoles personnalisés. Cela se révèle utile pour les systèmes de files d'attente, les event listeners, les planificateurs, etc.\n\n## Enregistrement du Worker\n\n### Enregistrement statique\n\nSi vous n'avez pas besoin de rendre le worker configurable par l'utilisateur (chemin de script fixe, nombre de threads fixe), vous pouvez simplement enregistrer le worker dans la fonction `init()`.\n\n```go\npackage myextension\n\nimport (\n\t\"github.com/dunglas/frankenphp\"\n\t\"github.com/dunglas/frankenphp/caddy\"\n)\n\n// Handle global pour communiquer avec le pool de workers\nvar worker frankenphp.Workers\n\nfunc init() {\n\t// Enregistre le worker lorsque le module est chargé.\n\tworker = caddy.RegisterWorkers(\n\t\t\"my-internal-worker\", // Nom unique\n\t\t\"worker.php\",         // Chemin du script (relatif à l'exécution ou absolu)\n\t\t2,                    // Nombre de threads fixe\n\t\t// Hooks de cycle de vie optionnels\n\t\tfrankenphp.WithWorkerOnServerStartup(func() {\n\t\t\t// Logique de configuration globale...\n\t\t}),\n\t)\n}\n```\n\n### Dans un module Caddy (configurable par l'utilisateur)\n\nSi vous prévoyez de partager votre extension (comme une file d'attente générique ou un écouteur d'événements), vous devriez l'envelopper dans un module Caddy. Cela permet aux utilisateurs de configurer le chemin du script et le nombre de threads via leur `Caddyfile`. Cela nécessite d'implémenter l'interface `caddy.Provisioner` et de parser le Caddyfile ([voir un exemple](https://github.com/dunglas/frankenphp-queue/blob/989120d394d66dd6c8e2101cac73dd622fade334/caddy.go)).\n\n### Dans une application Go pure (intégration)\n\nSi vous [intégrez FrankenPHP dans une application Go standard sans Caddy](https://pkg.go.dev/github.com/dunglas/frankenphp#example-ServeHTTP), vous pouvez enregistrer des workers d'extension en utilisant `frankenphp.WithExtensionWorkers` lors de l'initialisation des options.\n\n## Interaction avec les Workers\n\nUne fois le pool de workers actif, vous pouvez lui envoyer des tâches. Cela peut être fait à l'intérieur de [fonctions natives exportées vers PHP](https://frankenphp.dev/docs/extensions/#writing-the-extension), ou à partir de toute logique Go telle qu'un planificateur cron, un écouteur d'événements (MQTT, Kafka), ou toute autre goroutine.\n\n### Mode sans tête : `SendMessage`\n\nUtilisez `SendMessage` pour passer des données brutes directement à votre script worker. C'est idéal pour les files d'attente ou les commandes simples.\n\n#### Exemple : Une extension de file d'attente asynchrone\n\n```go\n// #include <Zend/zend_types.h>\nimport \"C\"\nimport (\n\t\"context\"\n\t\"unsafe\"\n\t\"github.com/dunglas/frankenphp\"\n)\n\n//export_php:function my_queue_push(mixed $data): bool\nfunc my_queue_push(data *C.zval) bool {\n\t// 1. S'assurer que le worker est prêt\n\tif worker == nil {\n\t\treturn false\n\t}\n\n\t// 2. Envoyer au worker en arrière-plan\n\t_, err := worker.SendMessage(\n\t\tcontext.Background(), // Contexte Go standard\n\t\tunsafe.Pointer(data), // Données à passer au worker\n\t\tnil, // http.ResponseWriter optionnel\n\t)\n\n\treturn err == nil\n}\n```\n\n### Émulation HTTP : `SendRequest`\n\nUtilisez `SendRequest` si votre extension doit invoquer un script PHP qui s'attend à un environnement web standard (remplir `$_SERVER`, `$_GET`, etc.).\n\n```go\n// #include <Zend/zend_types.h>\nimport \"C\"\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"unsafe\"\n\t\"github.com/dunglas/frankenphp\"\n)\n\n//export_php:function my_worker_http_request(string $path): string\nfunc my_worker_http_request(path *C.zend_string) unsafe.Pointer {\n\t// 1. Préparer la requête et l'enregistreur\n\turl := frankenphp.GoString(unsafe.Pointer(path))\n\treq, _ := http.NewRequest(\"GET\", url, http.NoBody)\n\trr := httptest.NewRecorder()\n\n\t// 2. Envoyer au worker\n\tif err := worker.SendRequest(rr, req); err != nil {\n\t\treturn nil\n\t}\n\n\t// 3. Retourner la réponse capturée\n\treturn frankenphp.PHPString(rr.Body.String(), false)\n}\n```\n\n## Script Worker\n\nLe script worker PHP s'exécute dans une boucle et peut gérer à la fois les messages bruts et les requêtes HTTP.\n\n```php\n<?php\n// Gérer à la fois les messages bruts et les requêtes HTTP dans la même boucle\n$handler = function ($payload = null) {\n    // Cas 1 : Mode Message\n    if ($payload !== null) {\n        return \"Received payload: \" . $payload;\n    }\n\n    // Cas 2 : Mode HTTP (les superglobales PHP standards sont peuplées)\n    echo \"Hello from page: \" . $_SERVER['REQUEST_URI'];\n};\n\nwhile (frankenphp_handle_request($handler)) {\n    gc_collect_cycles();\n}\n```\n\n## Hooks de Cycle de Vie\n\nFrankenPHP fournit des hooks pour exécuter du code Go à des points spécifiques du cycle de vie.\n\n| Type de Hook | Nom de l'Option                  | Signature            | Contexte et Cas d'Utilisation                                          |\n| :--------- | :--------------------------- | :------------------- | :--------------------------------------------------------------------- |\n| **Serveur** | `WithWorkerOnServerStartup`  | `func()`             | Configuration globale. Exécuté **Une fois**. Exemple : Connexion à NATS/Redis. |\n| **Serveur** | `WithWorkerOnServerShutdown` | `func()`             | Nettoyage global. Exécuté **Une fois**. Exemple : Fermeture des connexions partagées. |\n| **Thread** | `WithWorkerOnReady`          | `func(threadID int)` | Configuration par thread. Appelé lorsqu'un thread démarre. Reçoit l'ID du Thread. |\n| **Thread** | `WithWorkerOnShutdown`       | `func(threadID int)` | Nettoyage par thread. Reçoit l'ID du Thread.                           |\n\n### Exemple\n\n```go\npackage myextension\n\nimport (\n    \"fmt\"\n    \"github.com/dunglas/frankenphp\"\n    frankenphpCaddy \"github.com/dunglas/frankenphp/caddy\"\n)\n\nfunc init() {\n    workerHandle = frankenphpCaddy.RegisterWorkers(\n        \"my-worker\", \"worker.php\", 2,\n\n        // Démarrage du Serveur (Global)\n        frankenphp.WithWorkerOnServerStartup(func() {\n            fmt.Println(\"Extension : Démarrage du serveur...\")\n        }),\n\n        // Thread Prêt (Par Thread)\n        // Note : La fonction accepte un entier représentant l'ID du Thread\n        frankenphp.WithWorkerOnReady(func(id int) {\n            fmt.Printf(\"Extension : Le thread worker #%d est prêt.\\n\", id)\n        }),\n    )\n}\n```\n"
  },
  {
    "path": "docs/fr/extensions.md",
    "content": "# Écrire des extensions PHP en Go\n\nAvec FrankenPHP, vous pouvez **écrire des extensions PHP en Go**, ce qui vous permet de créer des **fonctions natives haute performance** qui peuvent être appelées directement depuis PHP. Vos applications peuvent tirer parti de toute bibliothèque Go existante ou nouvelle, ainsi que du célèbre modèle de concurrence des **goroutines directement depuis votre code PHP**.\n\nL'écriture d'extensions PHP se fait généralement en C, mais il est également possible de les écrire dans d'autres langages avec un peu de travail supplémentaire. Les extensions PHP permettent de tirer parti de la puissance des langages de bas niveau pour étendre les fonctionnalités de PHP, par exemple, en ajoutant des fonctions natives ou en optimisant des opérations spécifiques.\n\nGrâce aux modules Caddy, vous pouvez écrire des extensions PHP en Go et les intégrer très rapidement dans FrankenPHP.\n\n## Deux Approches\n\nFrankenPHP offre deux façons de créer des extensions PHP en Go :\n\n1. **Utilisation du Générateur d'Extensions** - L'approche recommandée qui génère tout le code standard nécessaire pour la plupart des cas d'usage, vous permettant de vous concentrer sur l'écriture de votre code Go\n2. **Implémentation Manuelle** - Contrôle total sur la structure de l'extension pour les cas d'usage avancés\n\nNous commencerons par l'approche du générateur, car c'est le moyen le plus facile de commencer, puis nous montrerons l'implémentation manuelle pour ceux qui ont besoin d'un contrôle complet.\n\n## Utilisation du Générateur d'Extensions\n\nFrankenPHP est livré avec un outil qui vous permet de **créer une extension PHP** en utilisant uniquement Go. **Pas besoin d'écrire du code C** ou d'utiliser CGO directement : FrankenPHP inclut également une **API de types publique** pour vous aider à écrire vos extensions en Go sans avoir à vous soucier du **jonglage de types entre PHP/C et Go**.\n\n> [!TIP]\n> Si vous voulez comprendre comment les extensions peuvent être écrites en Go à partir de zéro, vous pouvez lire la section d'implémentation manuelle ci-dessous démontrant comment écrire une extension PHP en Go sans utiliser le générateur.\n\nGardez à l'esprit que cet outil n'est **pas un générateur d'extensions complet**. Il est destiné à vous aider à écrire des extensions simples en Go, mais il ne fournit pas les fonctionnalités les plus avancées des extensions PHP. Si vous devez écrire une extension plus **complexe et optimisée**, vous devrez peut-être écrire du code C ou utiliser CGO directement.\n\n### Prérequis\n\nComme aussi couvert dans la section d'implémentation manuelle ci-dessous, vous devez [obtenir les sources PHP](https://www.php.net/downloads.php) et créer un nouveau module Go.\n\n#### Créer un Nouveau Module et Obtenir les Sources PHP\n\nLa première étape pour écrire une extension PHP en Go est de créer un nouveau module Go. Vous pouvez utiliser la commande suivante pour cela :\n\n```console\ngo mod init github.com/my-account/my-module\n```\n\nLa seconde étape est [l'obtention des sources PHP](https://www.php.net/downloads.php) pour les étapes suivantes. Une fois que vous les avez, décompressez-les dans le répertoire de votre choix, mais pas à l'intérieur de votre module Go :\n\n```console\ntar xf php-*\n```\n\n### Écrire l'Extension\n\nTout est maintenant configuré pour écrire votre fonction native en Go. Créez un nouveau fichier nommé `stringext.go`. Notre première fonction prendra une chaîne comme argument, le nombre de fois à la répéter, un booléen pour indiquer s'il faut inverser la chaîne, et retournera la chaîne résultante. Cela devrait ressembler à ceci :\n\n```go\npackage example\n\n// #include <Zend/zend_types.h>\nimport \"C\"\nimport (\n    \"strings\"\n    \"unsafe\"\n\n    \"github.com/dunglas/frankenphp\"\n)\n\n//export_php:function repeat_this(string $str, int $count, bool $reverse): string\nfunc repeat_this(s *C.zend_string, count int64, reverse bool) unsafe.Pointer {\n    str := frankenphp.GoString(unsafe.Pointer(s))\n\n    result := strings.Repeat(str, int(count))\n    if reverse {\n        runes := []rune(result)\n        for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {\n            runes[i], runes[j] = runes[j], runes[i]\n        }\n        result = string(runes)\n    }\n\n    return frankenphp.PHPString(result, false)\n}\n```\n\nIl y a deux choses importantes à noter ici :\n\n- Une directive `//export_php:function` définit la signature de la fonction en PHP. C'est ainsi que le générateur sait comment générer la fonction PHP avec les bons paramètres et le bon type de retour ;\n- La fonction doit retourner un `unsafe.Pointer`. FrankenPHP fournit une API pour vous aider avec le jonglage de types entre C et Go.\n\nAlors que le premier point parle de lui-même, le second peut être plus difficile à appréhender. Plongeons plus profondément dans le jonglage de types dans la section suivante.\n\n### Jonglage de Types\n\nBien que certains types de variables aient la même représentation mémoire entre C/PHP et Go, certains types nécessitent plus de logique pour être directement utilisés. C'est peut-être la partie la plus difficile quand il s'agit d'écrire des extensions car cela nécessite de comprendre les fonctionnements internes du moteur Zend et comment les variables sont stockées dans le moteur de PHP. Ce tableau résume ce que vous devez savoir :\n\n| Type PHP           | Type Go                       | Conversion directe | Assistant C vers Go               | Assistant Go vers C                | Support des Méthodes de Classe |\n| ------------------ | ----------------------------- | ------------------ | --------------------------------- | ---------------------------------- | ------------------------------ |\n| `int`              | `int64`                       | ✅                 | -                                 | -                                  | ✅                             |\n| `?int`             | `*int64`                      | ✅                 | -                                 | -                                  | ✅                             |\n| `float`            | `float64`                     | ✅                 | -                                 | -                                  | ✅                             |\n| `?float`           | `*float64`                    | ✅                 | -                                 | -                                  | ✅                             |\n| `bool`             | `bool`                        | ✅                 | -                                 | -                                  | ✅                             |\n| `?bool`            | `*bool`                       | ✅                 | -                                 | -                                  | ✅                             |\n| `string`/`?string` | `*C.zend_string`              | ❌                 | frankenphp.GoString()             | frankenphp.PHPString()             | ✅                             |\n| `array`            | `frankenphp.AssociativeArray` | ❌                 | `frankenphp.GoAssociativeArray()` | `frankenphp.PHPAssociativeArray()` | ✅                             |\n| `array`            | `map[string]any`              | ❌                 | `frankenphp.GoMap()`              | `frankenphp.PHPMap()`              | ✅                             |\n| `array`            | `[]any`                       | ❌                 | `frankenphp.GoPackedArray()`      | `frankenphp.PHPPackedArray()`      | ✅                             |\n| `mixed`            | `any`                         | ❌                 | `GoValue()`                       | `PHPValue()`                       | ❌                             |\n| `object`           | `struct`                      | ❌                 | _Pas encore implémenté_           | _Pas encore implémenté_            | ❌                             |\n\n> [!NOTE]\n> Ce tableau n'est pas encore exhaustif et sera complété au fur et à mesure que l'API de types FrankenPHP deviendra plus complète.\n>\n> Pour les méthodes de classe spécifiquement, les types primitifs et les tableaux sont supportés. Les objets ne peuvent pas encore être utilisés comme paramètres de méthode ou types de retour.\n\nSi vous vous référez à l'extrait de code de la section précédente, vous pouvez voir que des assistants sont utilisés pour convertir le premier paramètre et la valeur de retour. Les deuxième et troisième paramètres de notre fonction `repeat_this()` n'ont pas besoin d'être convertis car la représentation mémoire des types sous-jacents est la même pour C et Go.\n\n#### Travailler avec les Tableaux\n\nFrankenPHP fournit un support natif pour les tableaux PHP à travers `frankenphp.AssociativeArray` ou une conversion directe vers une map ou un slice.\n\n`AssociativeArray` représente une [hash map](https://fr.wikipedia.org/wiki/Table_de_hachage) composée d'un champ `Map: map[string]any` et d'un champ optionnel `Order: []string` (contrairement aux \"tableaux associatifs\" PHP, les maps Go ne sont pas ordonnées).\n\nSi l'ordre ou l'association ne sont pas nécessaires, il est également possible de convertir directement vers un slice `[]any` ou une map non ordonnée `map[string]any`.\n\n**Créer et manipuler des tableaux en Go :**\n\n```go\npackage example\n\n// #include <Zend/zend_types.h>\nimport \"C\"\nimport (\n    \"unsafe\"\n\n    \"github.com/dunglas/frankenphp\"\n)\n\n// export_php:function process_data_ordered(array $input): array\nfunc process_data_ordered_map(arr *C.zval) unsafe.Pointer {\n    // Convertir le tableau associatif PHP vers Go en conservant l'ordre\n    associativeArray, err := frankenphp.GoAssociativeArray[any](unsafe.Pointer(arr))\n    if err != nil {\n        // gérer l'erreur\n    }\n\n    // parcourir les entrées dans l'ordre\n    for _, key := range associativeArray.Order {\n        value, _ = associativeArray.Map[key]\n        // faire quelque chose avec key et value\n    }\n\n    // retourner un tableau ordonné\n    // si 'Order' n'est pas vide, seules les paires clé-valeur dans 'Order' seront respectées\n    return frankenphp.PHPAssociativeArray[string](frankenphp.AssociativeArray[string]{\n        Map: map[string]string{\n            \"key1\": \"value1\",\n            \"key2\": \"value2\",\n        },\n        Order: []string{\"key1\", \"key2\"},\n    })\n}\n\n// export_php:function process_data_unordered(array $input): array\nfunc process_data_unordered_map(arr *C.zval) unsafe.Pointer {\n    // Convertir le tableau associatif PHP vers une map Go sans conserver l'ordre\n    // ignorer l'ordre sera plus performant\n    goMap, err := frankenphp.GoMap[any](unsafe.Pointer(arr))\n    if err != nil {\n        // gérer l'erreur\n    }\n\n    // parcourir les entrées sans ordre spécifique\n    for key, value := range goMap {\n        // faire quelque chose avec key et value\n    }\n\n    // retourner un tableau non ordonné\n    return frankenphp.PHPMap(map[string]string {\n        \"key1\": \"value1\",\n        \"key2\": \"value2\",\n    })\n}\n\n// export_php:function process_data_packed(array $input): array\nfunc process_data_packed(arr *C.zval) unsafe.Pointer {\n    // Convertir le tableau packed PHP vers Go\n    goSlice, err := frankenphp.GoPackedArray(unsafe.Pointer(arr))\n    if err != nil {\n        // gérer l'erreur\n    }\n\n    // parcourir le slice dans l'ordre\n    for index, value := range goSlice {\n        // faire quelque chose avec index et value\n    }\n\n    // retourner un tableau packed\n    return frankenphp.PHPPackedArray([]string{\"value1\", \"value2\", \"value3\"})\n}\n```\n\n**Fonctionnalités clés de la conversion de tableaux :**\n\n- **Paires clé-valeur ordonnées** - Option pour conserver l'ordre du tableau associatif\n- **Optimisé pour plusieurs cas** - Option de ne pas conserver l'ordre pour de meilleures performances ou conversion directe vers un slice\n- **Détection automatique de liste** - Lors de la conversion vers PHP, détecte automatiquement si le tableau doit être une liste packed ou un hashmap\n- **Tableaux imbriqués** - Les tableaux peuvent être imbriqués et convertiront automatiquement tous les types supportés (`int64`, `float64`, `string`, `bool`, `nil`, `AssociativeArray`, `map[string]any`, `[]any`)\n- **Les objets ne sont pas supportés** - Actuellement, seuls les types scalaires et les tableaux peuvent être utilisés comme valeurs. Fournir un objet résultera en une valeur `null` dans le tableau PHP.\n\n##### Méthodes disponibles : Packed et Associatif\n\n- `frankenphp.PHPAssociativeArray(arr frankenphp.AssociativeArray) unsafe.Pointer` - Convertir vers un tableau PHP ordonné avec des paires clé-valeur\n- `frankenphp.PHPMap(arr map[string]any) unsafe.Pointer` - Convertir une map vers un tableau PHP non ordonné avec des paires clé-valeur\n- `frankenphp.PHPPackedArray(slice []any) unsafe.Pointer` - Convertir un slice vers un tableau PHP packed avec uniquement des valeurs indexées\n- `frankenphp.GoAssociativeArray(arr unsafe.Pointer, ordered bool) frankenphp.AssociativeArray` - Convertir un tableau PHP vers un `AssociativeArray` Go ordonné (map avec ordre)\n- `frankenphp.GoMap(arr unsafe.Pointer) map[string]any` - Convertir un tableau PHP vers une map Go non ordonnée\n- `frankenphp.GoPackedArray(arr unsafe.Pointer) []any` - Convertir un tableau PHP vers un slice Go\n- `frankenphp.IsPacked(zval *C.zend_array) bool` - Vérifie si le tableau PHP est une liste ou un tableau associatif\n\n### Travailler avec des Callables\n\nFrankenPHP propose un moyen de travailler avec les _callables_ PHP grâce au helper `frankenphp.CallPHPCallable()`. Cela permet d'appeler des fonctions ou des méthodes PHP depuis du code Go.\n\nPour illustrer cela, créons notre propre fonction `array_map()` qui prend un _callable_ et un tableau, applique le _callable_ à chaque élément du tableau, et retourne un nouveau tableau avec les résultats :\n\n```go\n// export_php:function my_array_map(array $data, callable $callback): array\nfunc my_array_map(arr *C.zend_array, callback *C.zval) unsafe.Pointer {\n\tgoSlice, err := frankenphp.GoPackedArray[any](unsafe.Pointer(arr))\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tresult := make([]any, len(goSlice))\n\n\tfor index, value := range goSlice {\n\t\tresult[index] = frankenphp.CallPHPCallable(unsafe.Pointer(callback), []interface{}{value})\n\t}\n\n\treturn frankenphp.PHPPackedArray(result)\n}\n```\n\nRemarquez comment nous utilisons `frankenphp.CallPHPCallable()` pour appeler le _callable_ PHP passé en paramètre. Cette fonction prend un pointeur vers le _callable_ et un tableau d'arguments, et elle retourne le résultat de l'exécution du _callable_. Vous pouvez utiliser la syntaxe habituelle des _callables_ :\n\n```php\n<?php\n\n$result = my_array_map([1, 2, 3], function($x) { return $x * 2; });\n// $result vaudra [2, 4, 6]\n\n$result = my_array_map(['hello', 'world'], 'strtoupper');\n// $result vaudra ['HELLO', 'WORLD']\n```\n\n### Déclarer une Classe PHP Native\n\nLe générateur prend en charge la déclaration de **classes opaques** comme structures Go, qui peuvent être utilisées pour créer des objets PHP. Vous pouvez utiliser la directive `//export_php:class` pour définir une classe PHP. Par exemple :\n\n```go\npackage example\n\n//export_php:class User\ntype UserStruct struct {\n    Name string\n    Age  int\n}\n```\n\n#### Que sont les Classes Opaques ?\n\nLes **classes opaques** sont des classes avec lesquelles la structure interne (comprendre : les propriétés) est cachée du code PHP. Cela signifie :\n\n- **Pas d'accès direct aux propriétés** : Vous ne pouvez pas lire ou écrire des propriétés directement depuis PHP (`$user->name` ne fonctionnera pas)\n- **Interface uniquement par méthodes** - Toutes les interactions doivent passer par les méthodes que vous définissez\n- **Meilleure encapsulation** - La structure de données interne est complètement contrôlée par le code Go\n- **Sécurité de type** - Aucun risque que le code PHP corrompe l'état interne avec de mauvais types\n- **API plus propre** - Force à concevoir une interface publique appropriée\n\nCette approche fournit une meilleure encapsulation et empêche le code PHP de corrompre accidentellement l'état interne de vos objets Go. Toutes les interactions avec l'objet doivent passer par les méthodes que vous définissez explicitement.\n\n#### Ajouter des Méthodes aux Classes\n\nPuisque les propriétés ne sont pas directement accessibles, vous **devez définir des méthodes** pour interagir avec vos classes opaques. Utilisez la directive `//export_php:method` pour définir cela :\n\n```go\npackage example\n\n// #include <Zend/zend_types.h>\nimport \"C\"\nimport (\n    \"unsafe\"\n\n    \"github.com/dunglas/frankenphp\"\n)\n\n//export_php:class User\ntype UserStruct struct {\n    Name string\n    Age  int\n}\n\n//export_php:method User::getName(): string\nfunc (us *UserStruct) GetUserName() unsafe.Pointer {\n    return frankenphp.PHPString(us.Name, false)\n}\n\n//export_php:method User::setAge(int $age): void\nfunc (us *UserStruct) SetUserAge(age int64) {\n    us.Age = int(age)\n}\n\n//export_php:method User::getAge(): int\nfunc (us *UserStruct) GetUserAge() int64 {\n    return int64(us.Age)\n}\n\n//export_php:method User::setNamePrefix(string $prefix = \"User\"): void\nfunc (us *UserStruct) SetNamePrefix(prefix *C.zend_string) {\n    us.Name = frankenphp.GoString(unsafe.Pointer(prefix)) + \": \" + us.Name\n}\n```\n\n#### Paramètres Nullables\n\nLe générateur prend en charge les paramètres nullables en utilisant le préfixe `?` dans les signatures PHP. Quand un paramètre est nullable, il devient un pointeur dans votre fonction Go, vous permettant de vérifier si la valeur était `null` en PHP :\n\n```go\npackage example\n\n// #include <Zend/zend_types.h>\nimport \"C\"\nimport (\n    \"unsafe\"\n\n    \"github.com/dunglas/frankenphp\"\n)\n\n//export_php:method User::updateInfo(?string $name, ?int $age, ?bool $active): void\nfunc (us *UserStruct) UpdateInfo(name *C.zend_string, age *int64, active *bool) {\n    // Vérifier si name a été fourni (pas null)\n    if name != nil {\n        us.Name = frankenphp.GoString(unsafe.Pointer(name))\n    }\n\n    // Vérifier si age a été fourni (pas null)\n    if age != nil {\n        us.Age = int(*age)\n    }\n\n    // Vérifier si active a été fourni (pas null)\n    if active != nil {\n        us.Active = *active\n    }\n}\n```\n\n**Points clés sur les paramètres nullables :**\n\n- **Types primitifs nullables** (`?int`, `?float`, `?bool`) deviennent des pointeurs (`*int64`, `*float64`, `*bool`) en Go\n- **Chaînes nullables** (`?string`) restent comme `*C.zend_string` mais peuvent être `nil`\n- **Vérifiez `nil`** avant de déréférencer les valeurs de pointeur\n- **PHP `null` devient Go `nil`** - quand PHP passe `null`, votre fonction Go reçoit un pointeur `nil`\n\n> [!WARNING]\n> Actuellement, les méthodes de classe ont les limitations suivantes. **Les objets ne sont pas supportés** comme types de paramètres ou types de retour. **Les tableaux sont entièrement supportés** pour les paramètres et types de retour. Types supportés : `string`, `int`, `float`, `bool`, `array`, et `void` (pour le type de retour). **Les types de paramètres nullables sont entièrement supportés** pour tous les types scalaires (`?string`, `?int`, `?float`, `?bool`).\n\nAprès avoir généré l'extension, vous serez autorisé à utiliser la classe et ses méthodes en PHP. Notez que vous **ne pouvez pas accéder aux propriétés directement** :\n\n```php\n<?php\n\n$user = new User();\n\n// ✅ Fonctionne - utilisation des méthodes\n$user->setAge(25);\necho $user->getName();           // Output : (vide, valeur par défaut)\necho $user->getAge();            // Output : 25\n$user->setNamePrefix(\"Employee\");\n\n// ✅ Fonctionne aussi - paramètres nullables\n$user->updateInfo(\"John\", 30, true);        // Tous les paramètres fournis\n$user->updateInfo(\"Jane\", null, false);     // L'âge est null\n$user->updateInfo(null, 25, null);          // Le nom et actif sont null\n\n// ❌ Ne fonctionnera PAS - accès direct aux propriétés\n// echo $user->name;             // Erreur : Impossible d'accéder à la propriété privée\n// $user->age = 30;              // Erreur : Impossible d'accéder à la propriété privée\n```\n\nCette conception garantit que votre code Go a un contrôle complet sur la façon dont l'état de l'objet est accédé et modifié, fournissant une meilleure encapsulation et sécurité de type.\n\n### Déclarer des Constantes\n\nLe générateur prend en charge l'exportation de constantes Go vers PHP en utilisant deux directives : `//export_php:const` pour les constantes globales et `//export_php:classconst` pour les constantes de classe. Cela vous permet de partager des valeurs de configuration, des codes de statut et d'autres constantes entre le code Go et PHP.\n\n#### Constantes Globales\n\nUtilisez la directive `//export_php:const` pour créer des constantes PHP globales :\n\n```go\npackage example\n\n//export_php:const\nconst MAX_CONNECTIONS = 100\n\n//export_php:const\nconst API_VERSION = \"1.2.3\"\n\n//export_php:const\nconst (\n\tSTATUS_OK = iota\n\tSTATUS_ERROR\n)\n```\n\n> [!NOTE]\n> Les constantes PHP prennent le nom de la constante Go, d'où l'utilisation de majuscules pour les noms des constants en Go.\n\n#### Constantes de Classe\n\nUtilisez la directive `//export_php:classconst ClassName` pour créer des constantes qui appartiennent à une classe PHP spécifique :\n\n```go\npackage example\n\n//export_php:classconst User\nconst STATUS_ACTIVE = 1\n\n//export_php:classconst User\nconst STATUS_INACTIVE = 0\n\n//export_php:classconst User\nconst ROLE_ADMIN = \"admin\"\n\n//export_php:classconst Order\nconst (\n\tSTATE_PENDING = iota\n\tSTATE_PROCESSING\n\tSTATE_COMPLETED\n)\n```\n\n> [!NOTE]\n> Comme les constantes globales, les constantes de classe prennent le nom de la constante Go.\n\nLes constantes de classe sont accessibles en utilisant la portée du nom de classe en PHP :\n\n```php\n<?php\n\n// Constantes globales\necho MAX_CONNECTIONS;    // 100\necho API_VERSION;        // \"1.2.3\"\n\n// Constantes de classe\necho User::STATUS_ACTIVE;    // 1\necho User::ROLE_ADMIN;       // \"admin\"\necho Order::STATE_PENDING;   // 0\n```\n\nLa directive prend en charge divers types de valeurs incluant les chaînes, entiers, booléens, flottants et constantes iota. Lors de l'utilisation de `iota`, le générateur assigne automatiquement des valeurs séquentielles (0, 1, 2, etc.). Les constantes globales deviennent disponibles dans votre code PHP comme constantes globales, tandis que les constantes de classe sont déclarées dans leurs classes respectives avec la visibilité publique. Lors de l'utilisation d'entiers, différentes notations possibles (binaire, hex, octale) sont supportées et dumpées telles quelles dans le fichier stub PHP.\n\nVous pouvez utiliser les constantes comme vous êtes habitué dans le code Go. Par exemple, prenons la fonction `repeat_this()` que nous avons déclarée plus tôt et changeons le dernier argument en entier :\n\n```go\npackage example\n\n// #include <Zend/zend_types.h>\nimport \"C\"\nimport (\n    \"strings\"\n    \"unsafe\"\n\n    \"github.com/dunglas/frankenphp\"\n)\n\n//export_php:const\nconst STR_REVERSE = iota\n\n//export_php:const\nconst STR_NORMAL = iota\n\n//export_php:classconst StringProcessor\nconst MODE_LOWERCASE = 1\n\n//export_php:classconst StringProcessor\nconst MODE_UPPERCASE = 2\n\n//export_php:function repeat_this(string $str, int $count, int $mode): string\nfunc repeat_this(s *C.zend_string, count int64, mode int) unsafe.Pointer {\n    str := frankenphp.GoString(unsafe.Pointer(s))\n\n    result := strings.Repeat(str, int(count))\n    if mode == STR_REVERSE {\n        // inverser la chaîne\n    }\n\n    if mode == STR_NORMAL {\n        // no-op, juste pour montrer la constante\n    }\n\n    return frankenphp.PHPString(result, false)\n}\n\n//export_php:class StringProcessor\ntype StringProcessorStruct struct {\n    // champs internes\n}\n\n//export_php:method StringProcessor::process(string $input, int $mode): string\nfunc (sp *StringProcessorStruct) Process(input *C.zend_string, mode int64) unsafe.Pointer {\n    str := frankenphp.GoString(unsafe.Pointer(input))\n\n    switch mode {\n    case MODE_LOWERCASE:\n        str = strings.ToLower(str)\n    case MODE_UPPERCASE:\n        str = strings.ToUpper(str)\n    }\n\n    return frankenphp.PHPString(str, false)\n}\n```\n\n### Utilisation des Espaces de Noms\n\nLe générateur prend en charge l'organisation des fonctions, classes et constantes de votre extension PHP sous un espace de noms (namespace) en utilisant la directive `//export_php:namespace`. Cela aide à éviter les conflits de noms et fournit une meilleure organisation pour l'API de votre extension.\n\n#### Déclarer un Espace de Noms\n\nUtilisez la directive `//export_php:namespace` en haut de votre fichier Go pour placer tous les symboles exportés sous un espace de noms spécifique :\n\n```go\n//export_php:namespace My\\Extension\npackage example\n\nimport (\n    \"unsafe\"\n\n    \"github.com/dunglas/frankenphp\"\n)\n\n//export_php:function hello(): string\nfunc hello() string {\n    return \"Bonjour depuis l'espace de noms My\\\\Extension !\"\n}\n\n//export_php:class User\ntype UserStruct struct {\n    // champs internes\n}\n\n//export_php:method User::getName(): string\nfunc (u *UserStruct) GetName() unsafe.Pointer {\n    return frankenphp.PHPString(\"Jean Dupont\", false)\n}\n\n//export_php:const\nconst STATUS_ACTIVE = 1\n```\n\n#### Utilisation de l'Extension avec Espace de Noms en PHP\n\nQuand un espace de noms est déclaré, toutes les fonctions, classes et constantes sont placées sous cet espace de noms en PHP :\n\n```php\n<?php\n\necho My\\Extension\\hello(); // \"Bonjour depuis l'espace de noms My\\Extension !\"\n\n$user = new My\\Extension\\User();\necho $user->getName(); // \"Jean Dupont\"\n\necho My\\Extension\\STATUS_ACTIVE; // 1\n```\n\n#### Notes Importantes\n\n- Seule **une** directive d'espace de noms est autorisée par fichier. Si plusieurs directives d'espace de noms sont trouvées, le générateur retournera une erreur.\n- L'espace de noms s'applique à **tous** les symboles exportés dans le fichier : fonctions, classes, méthodes et constantes.\n- Les noms d'espaces de noms suivent les conventions des espaces de noms PHP en utilisant les barres obliques inverses (`\\`) comme séparateurs.\n- Si aucun espace de noms n'est déclaré, les symboles sont exportés vers l'espace de noms global comme d'habitude.\n\n### Générer l'Extension\n\nC'est là que la magie opère, et votre extension peut maintenant être générée. Vous pouvez exécuter le générateur avec la commande suivante :\n\n```console\nGEN_STUB_SCRIPT=php-src/build/gen_stub.php frankenphp extension-init my_extension.go\n```\n\n> [!NOTE]\n> N'oubliez pas de définir la variable d'environnement `GEN_STUB_SCRIPT` sur le chemin du fichier `gen_stub.php` dans les sources PHP que vous avez téléchargées plus tôt. C'est le même script `gen_stub.php` mentionné dans la section d'implémentation manuelle.\n\nSi tout s'est bien passé, un nouveau répertoire nommé `build` devrait avoir été créé. Ce répertoire contient les fichiers générés pour votre extension, incluant le fichier `my_extension.go` avec les stubs de fonction PHP générés.\n\n### Intégrer l'Extension Générée dans FrankenPHP\n\nNotre extension est maintenant prête à être compilée et intégrée dans FrankenPHP. Pour ce faire, référez-vous à la [documentation de compilation](compile.md) de FrankenPHP pour apprendre comment compiler FrankenPHP. Ajoutez le module en utilisant le flag `--with`, pointant vers le chemin de votre module :\n\n```console\nCGO_ENABLED=1 \\\nXCADDY_GO_BUILD_FLAGS=\"-ldflags='-w -s' -tags=nobadger,nomysql,nopgx\" \\\nCGO_CFLAGS=$(php-config --includes) \\\nCGO_LDFLAGS=\"$(php-config --ldflags) $(php-config --libs)\" \\\nxcaddy build \\\n    --output frankenphp \\\n    --with github.com/my-account/my-module/build\n```\n\nNotez que vous pointez vers le sous-répertoire `/build` qui a été créé pendant l'étape de génération. Cependant, ce n'est pas obligatoire : vous pouvez aussi copier les fichiers générés dans le répertoire de votre module et pointer directement vers lui.\n\n### Tester Votre Extension Générée\n\nVous pouvez créer un fichier PHP pour tester les fonctions et classes que vous avez créées. Par exemple, créez un fichier `index.php` avec le contenu suivant :\n\n```php\n<?php\n\n// Utilisation des constantes globales\nvar_dump(repeat_this('Hello World', 5, STR_REVERSE));\n\n// Utilisation des constantes de classe\n$processor = new StringProcessor();\necho $processor->process('Hello World', StringProcessor::MODE_LOWERCASE);  // \"hello world\"\necho $processor->process('Hello World', StringProcessor::MODE_UPPERCASE);  // \"HELLO WORLD\"\n```\n\nUne fois que vous avez intégré votre extension dans FrankenPHP comme indiqué dans la section précédente, vous pouvez exécuter ce fichier de test en utilisant `./frankenphp php-server`, et vous devriez voir votre extension fonctionner.\n\n## Implémentation Manuelle\n\nSi vous voulez comprendre comment les extensions fonctionnent ou avez besoin d'un contrôle total sur votre extension, vous pouvez les écrire manuellement. Cette approche vous donne un contrôle complet mais nécessite plus de code intermédiaire.\n\n### Fonction de Base\n\nNous allons voir comment écrire une extension PHP simple en Go qui définit une nouvelle fonction native. Cette fonction sera appelée depuis PHP et déclenchera une goroutine qui enregistrera un message dans les logs de Caddy. Cette fonction ne prend aucun paramètre et ne retourne rien.\n\n#### Définir la Fonction Go\n\nDans votre module, vous devez définir une nouvelle fonction native qui sera appelée depuis PHP. Pour ce faire, créez un fichier avec le nom que vous voulez, par exemple, `extension.go`, et ajoutez le code suivant :\n\n```go\npackage example\n\n// #include \"extension.h\"\nimport \"C\"\nimport (\n    \"log/slog\"\n    \"unsafe\"\n\n    \"github.com/dunglas/frankenphp\"\n)\n\nfunc init() {\n    frankenphp.RegisterExtension(unsafe.Pointer(&C.ext_module_entry))\n}\n\n//export go_print_something\nfunc go_print_something() {\n    go func() {\n        slog.Info(\"Hello from a goroutine!\")\n    }()\n}\n```\n\nLa fonction `frankenphp.RegisterExtension()` simplifie le processus d'enregistrement d'extension en gérant la logique interne de PHP. La fonction `go_print_something` utilise la directive `//export` pour indiquer qu'elle sera accessible dans le code C que nous écrirons, grâce à CGO.\n\nDans cet exemple, notre nouvelle fonction déclenchera une goroutine qui enregistrera un message dans les logs de Caddy.\n\n#### Définir la Fonction PHP\n\nPour permettre à PHP d'appeler notre fonction, nous devons définir une fonction PHP correspondante. Pour cela, nous créerons un fichier stub, par exemple, `extension.stub.php`, qui contiendra le code suivant :\n\n```php\n<?php\n\n/** @generate-class-entries */\n\nfunction go_print(): void {}\n```\n\nCe fichier définit la signature de la fonction `go_print()`, qui sera appelée depuis PHP. La directive `@generate-class-entries` permet à PHP de générer automatiquement les entrées de fonction pour notre extension.\n\nCeci n'est pas fait manuellement mais en utilisant un script fourni dans les sources PHP (assurez-vous d'ajuster le chemin vers le script `gen_stub.php` selon l'emplacement de vos sources PHP) :\n\n```bash\nphp ../php-src/build/gen_stub.php extension.stub.php\n```\n\nCe script générera un fichier nommé `extension_arginfo.h` qui contient les informations nécessaires pour que PHP sache comment définir et appeler notre fonction.\n\n#### Écrire le Pont entre Go et C\n\nMaintenant, nous devons écrire le pont entre Go et C. Créez un fichier nommé `extension.h` dans le répertoire de votre module avec le contenu suivant :\n\n```c\n#ifndef _EXTENSION_H\n#define _EXTENSION_H\n\n#include <php.h>\n\nextern zend_module_entry ext_module_entry;\n\n#endif\n```\n\nEnsuite, créez un fichier nommé `extension.c` qui effectuera les étapes suivantes :\n\n- Inclure les en-têtes PHP ;\n- Déclarer notre nouvelle fonction PHP native `go_print()` ;\n- Déclarer les métadonnées de l'extension.\n\nCommençons par inclure les en-têtes requis :\n\n```c\n#include <php.h>\n#include \"extension.h\"\n#include \"extension_arginfo.h\"\n\n// Contient les symboles exportés par Go\n#include \"_cgo_export.h\"\n```\n\nNous définissons ensuite notre fonction PHP comme une fonction de langage natif :\n\n```c\nPHP_FUNCTION(go_print)\n{\n    ZEND_PARSE_PARAMETERS_NONE();\n\n    go_print_something();\n}\n\nzend_module_entry ext_module_entry = {\n    STANDARD_MODULE_HEADER,\n    \"ext_go\",\n    ext_functions, /* Functions */\n    NULL,          /* MINIT */\n    NULL,          /* MSHUTDOWN */\n    NULL,          /* RINIT */\n    NULL,          /* RSHUTDOWN */\n    NULL,          /* MINFO */\n    \"0.1.1\",\n    STANDARD_MODULE_PROPERTIES\n};\n```\n\nDans ce cas, notre fonction ne prend aucun paramètre et ne retourne rien. Elle appelle simplement la fonction Go que nous avons définie plus tôt, exportée en utilisant la directive `//export`.\n\nEnfin, nous définissons les métadonnées de l'extension dans une structure `zend_module_entry`, telles que son nom, sa version et ses propriétés. Cette information est nécessaire pour que PHP reconnaisse et charge notre extension. Notez que `ext_functions` est un tableau de pointeurs vers les fonctions PHP que nous avons définies, et il a été automatiquement généré par le script `gen_stub.php` dans le fichier `extension_arginfo.h`.\n\nL'enregistrement de l'extension est automatiquement géré par la fonction `RegisterExtension()` de FrankenPHP que nous appelons dans notre code Go.\n\n### Usage Avancé\n\nMaintenant que nous savons comment créer une extension PHP de base en Go, complexifions notre exemple. Nous allons maintenant créer une fonction PHP qui prend une chaîne comme paramètre et retourne sa version en majuscules.\n\n#### Définir le Stub de Fonction PHP\n\nPour définir la nouvelle fonction PHP, nous modifierons notre fichier `extension.stub.php` pour inclure la nouvelle signature de fonction :\n\n```php\n<?php\n\n/** @generate-class-entries */\n\n/**\n * Convertit une chaîne en majuscules.\n *\n * @param string $string La chaîne à convertir.\n * @return string La version en majuscules de la chaîne.\n */\nfunction go_upper(string $string): string {}\n```\n\n> [!TIP]\n> Ne négligez pas la documentation de vos fonctions ! Vous êtes susceptible de partager vos stubs d'extension avec d'autres développeurs pour documenter comment utiliser votre extension et quelles fonctionnalités sont disponibles.\n\nEn régénérant le fichier stub avec le script `gen_stub.php`, le fichier `extension_arginfo.h` devrait ressembler à ceci :\n\n```c\nZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_go_upper, 0, 1, IS_STRING, 0)\n    ZEND_ARG_TYPE_INFO(0, string, IS_STRING, 0)\nZEND_END_ARG_INFO()\n\nZEND_FUNCTION(go_upper);\n\nstatic const zend_function_entry ext_functions[] = {\n    ZEND_FE(go_upper, arginfo_go_upper)\n    ZEND_FE_END\n};\n```\n\nNous pouvons voir que la fonction `go_upper` est définie avec un paramètre de type `string` et un type de retour `string`.\n\n#### Jonglerie de Types entre Go et PHP/C\n\nVotre fonction Go ne peut pas accepter directement une chaîne PHP comme paramètre. Vous devez la convertir en chaîne Go. Heureusement, FrankenPHP fournit des fonctions d'aide pour gérer la conversion entre les chaînes PHP et les chaînes Go, similaire à ce que nous avons vu dans l'approche du générateur.\n\nLe fichier d'en-tête reste simple :\n\n```c\n#ifndef _EXTENSION_H\n#define _EXTENSION_H\n\n#include <php.h>\n\nextern zend_module_entry ext_module_entry;\n\n#endif\n```\n\nNous pouvons maintenant écrire le pont entre Go et C dans notre fichier `extension.c`. Nous passerons la chaîne PHP directement à notre fonction Go :\n\n```c\nPHP_FUNCTION(go_upper)\n{\n    zend_string *str;\n\n    ZEND_PARSE_PARAMETERS_START(1, 1)\n        Z_PARAM_STR(str)\n    ZEND_PARSE_PARAMETERS_END();\n\n    zend_string *result = go_upper(str);\n    RETVAL_STR(result);\n}\n```\n\nVous pouvez en apprendre plus sur `ZEND_PARSE_PARAMETERS_START` et l'analyse des paramètres dans la page dédiée du [PHP Internals Book](https://www.phpinternalsbook.com/php7/extensions_design/php_functions.html#parsing-parameters-zend-parse-parameters). Ici, nous disons à PHP que notre fonction prend un paramètre obligatoire de type `string` comme `zend_string`. Nous passons ensuite cette chaîne directement à notre fonction Go et retournons le résultat en utilisant `RETVAL_STR`.\n\nIl ne reste qu'une chose à faire : implémenter la fonction `go_upper` en Go.\n\n#### Implémenter la Fonction Go\n\nNotre fonction Go prendra un `*C.zend_string` comme paramètre, le convertira en chaîne Go en utilisant la fonction d'aide de FrankenPHP, le traitera, et retournera le résultat comme un nouveau `*C.zend_string`. Les fonctions d'aide gèrent toute la complexité de gestion de mémoire et de conversion pour nous.\n\n```go\npackage example\n\n// #include <Zend/zend_types.h>\nimport \"C\"\nimport (\n    \"unsafe\"\n    \"strings\"\n\n    \"github.com/dunglas/frankenphp\"\n)\n\n//export go_upper\nfunc go_upper(s *C.zend_string) *C.zend_string {\n    str := frankenphp.GoString(unsafe.Pointer(s))\n\n    upper := strings.ToUpper(str)\n\n    return (*C.zend_string)(frankenphp.PHPString(upper, false))\n}\n```\n\nCette approche est beaucoup plus propre et sûre que la gestion manuelle de la mémoire. Les fonctions d'aide de FrankenPHP gèrent la conversion entre le format `zend_string` de PHP et les chaînes Go automatiquement. Le paramètre `false` dans `PHPString()` indique que nous voulons créer une nouvelle chaîne non persistante (libérée à la fin de la requête).\n\n> [!TIP]\n> Dans cet exemple, nous n'effectuons aucune gestion d'erreur, mais vous devriez toujours vérifier que les pointeurs ne sont pas `nil` et que les données sont valides avant de les utiliser dans vos fonctions Go.\n\n### Intégrer l'Extension dans FrankenPHP\n\nNotre extension est maintenant prête à être compilée et intégrée dans FrankenPHP. Pour ce faire, référez-vous à la [documentation de compilation](compile.md) de FrankenPHP pour apprendre comment compiler FrankenPHP. Ajoutez le module en utilisant le flag `--with`, pointant vers le chemin de votre module :\n\n```console\nCGO_ENABLED=1 \\\nXCADDY_GO_BUILD_FLAGS=\"-ldflags='-w -s' -tags=nobadger,nomysql,nopgx\" \\\nCGO_CFLAGS=$(php-config --includes) \\\nCGO_LDFLAGS=\"$(php-config --ldflags) $(php-config --libs)\" \\\nxcaddy build \\\n    --output frankenphp \\\n    --with github.com/my-account/my-module\n```\n\nC'est tout ! Votre extension est maintenant intégrée dans FrankenPHP et peut être utilisée dans votre code PHP.\n\n### Tester Votre Extension\n\nAprès avoir intégré votre extension dans FrankenPHP, vous pouvez créer un fichier `index.php` avec des exemples pour les fonctions que vous avez implémentées :\n\n```php\n<?php\n\n// Tester la fonction de base\ngo_print();\n\n// Tester la fonction avancée\necho go_upper(\"hello world\") . \"\\n\";\n```\n\nVous pouvez maintenant exécuter FrankenPHP avec ce fichier en utilisant `./frankenphp php-server`, et vous devriez voir votre extension fonctionner.\n"
  },
  {
    "path": "docs/fr/github-actions.md",
    "content": "# Utilisation de GitHub Actions\n\nCe dépôt construit et déploie l'image Docker sur [le Hub Docker](https://hub.docker.com/r/dunglas/frankenphp) pour\nchaque pull request approuvée ou sur votre propre fork une fois configuré.\n\n## Configuration de GitHub Actions\n\nDans les paramètres du dépôt, sous \"secrets\", ajoutez les secrets suivants :\n\n- `REGISTRY_LOGIN_SERVER` : Le registre Docker à utiliser (par exemple, `docker.io`).\n- `REGISTRY_USERNAME` : Le nom d'utilisateur à utiliser pour se connecter au registre (par exemple, `dunglas`).\n- `REGISTRY_PASSWORD` : Le mot de passe à utiliser pour se connecter au registre (par exemple, une clé d'accès).\n- `IMAGE_NAME` : Le nom de l'image (par exemple, `dunglas/frankenphp`).\n\n## Construction et push de l'image\n\n1. Créez une Pull Request ou poussez vers votre fork.\n2. GitHub Actions va construire l'image et exécuter tous les tests.\n3. Si la construction est réussie, l'image sera poussée vers le registre en utilisant le tag `pr-x`, où `x` est le numéro de la PR.\n\n## Déploiement de l'image\n\n1. Une fois la Pull Request fusionnée, GitHub Actions exécutera à nouveau les tests et construira une nouvelle image.\n2. Si la construction est réussie, le tag `main` sera mis à jour dans le registre Docker.\n\n## Releases\n\n1. Créez un nouveau tag dans le dépôt.\n2. GitHub Actions va construire l'image et exécuter tous les tests.\n3. Si la compilation est réussie, l'image sera poussée vers le registre en utilisant le nom du tag comme tag (par exemple, `v1.2.3` et `v1.2` seront créés).\n4. Le tag `latest` sera également mis à jour.\n"
  },
  {
    "path": "docs/fr/hot-reload.md",
    "content": "# Hot Reload\n\nFrankenPHP inclut une fonctionnalité de **hot reload** intégrée, conçue pour améliorer considérablement l'expérience développeur.\n\n![Hot Reload](../hot-reload.png)\n\nCette fonctionnalité offre un workflow similaire au **Hot Module Replacement (HMR)** présent dans les outils JavaScript modernes (comme Vite ou webpack).\nAu lieu de rafraîchir manuellement le navigateur après chaque modification de fichier (code PHP, templates, fichiers JavaScript et CSS...),\nFrankenPHP met à jour le contenu de la page en temps réel.\n\nLe Hot Reload fonctionne nativement avec WordPress, Laravel, Symfony et toute autre application ou framework PHP.\n\nLorsqu'il est activé, FrankenPHP surveille votre répertoire de travail actuel pour détecter les modifications du système de fichiers.\nQuand un fichier est modifié, il envoie une mise à jour [Mercure](mercure.md) au navigateur.\n\nSelon votre configuration, le navigateur va soit :\n\n- **Transformer le DOM** (en préservant la position de défilement et l'état des champs de saisie) si [Idiomorph](https://github.com/bigskysoftware/idiomorph) est chargé.\n- **Recharger la page** (rechargement standard) si Idiomorph n'est pas présent.\n\n## Configuration\n\nPour activer le hot reload, activez Mercure, puis ajoutez la sous-directive `hot_reload` à la directive `php_server` dans votre `Caddyfile`.\n\n> [!WARNING]\n\n> Cette fonctionnalité est destinée **uniquement aux environnements de développement**.\n> N'activez pas `hot_reload` en production, car cette fonctionnalité n'est pas sécurisée (expose des détails internes sensibles) et ralentit l'application.\n\n```caddyfile\nlocalhost\n\nmercure {\n    anonymous\n}\n\nroot public/\nphp_server {\n    hot_reload\n}\n```\n\nPar défaut, FrankenPHP surveillera tous les fichiers du répertoire de travail actuel correspondant au motif glob suivant : `./**/*.{css,env,gif,htm,html,jpg,jpeg,js,mjs,php,png,svg,twig,webp,xml,yaml,yml}`\n\nIl est possible de définir explicitement les fichiers à surveiller en utilisant un motif glob :\n\n```caddyfile\nlocalhost\n\nmercure {\n    anonymous\n}\n\nroot public/\nphp_server {\n    hot_reload src/**/*{.php,.js} config/**/*.yaml\n}\n```\n\nUtilisez la forme longue de `hot_reload` pour spécifier le *topic* Mercure à utiliser ainsi que les répertoires ou fichiers à surveiller :\n\n```caddyfile\nlocalhost\n\nmercure {\n    anonymous\n}\n\nroot public/\nphp_server {\n    hot_reload {\n        topic hot-reload-topic\n        watch src/**/*.php\n        watch assets/**/*.{ts,json}\n        watch templates/\n        watch public/css/\n    }\n}\n```\n\n## Intégration côté client\n\nLe serveur détecte les modifications et publie les modifications automatiquement. Le navigateur doit s'abonner à ces événements pour mettre à jour la page.\nFrankenPHP expose l'URL du Hub Mercure à utiliser pour s'abonner aux modifications de fichiers via la variable d'environnement `$_SERVER['FRANKENPHP_HOT_RELOAD']`.\n\nLa bibliothèque JavaScript [frankenphp-hot-reload](https://www.npmjs.com/package/frankenphp-hot-reload) gére la logique côté client.\nPour l'utiliser, ajoutez le code suivant à votre gabarit (*layout*) principal :\n\n```php\n<!DOCTYPE html>\n<title>FrankenPHP Hot Reload</title>\n<?php if (isset($_SERVER['FRANKENPHP_HOT_RELOAD'])): ?>\n<meta name=\"frankenphp-hot-reload:url\" content=\"<?=$_SERVER['FRANKENPHP_HOT_RELOAD']?>\">\n<script src=\"https://cdn.jsdelivr.net/npm/idiomorph\"></script>\n<script src=\"https://cdn.jsdelivr.net/npm/frankenphp-hot-reload/+esm\" type=\"module\"></script>\n<?php endif ?>\n```\n\nLa bibliothèque s'abonnera automatiquement au hub Mercure, récupérera l'URL actuelle en arrière-plan lorsqu'une modification de fichier est détectée et transformera le DOM.\nElle est disponible en tant que package [npm](https://www.npmjs.com/package/frankenphp-hot-reload) et sur [GitHub](https://github.com/dunglas/frankenphp-hot-reload).\n\nAlternativement, vous pouvez implémenter votre propre logique côté client en vous abonnant directement au hub Mercure en utilisant la classe JavaScript native `EventSource`.\n\n### Conserver les nœuds DOM existants\n\nDans de rares cas, comme lors de l'utilisation d'outils de développement tels que [la *web debug toolbar* de Symfony](https://github.com/symfony/symfony/pull/62970),\nvous pouvez souhaiter conserver des nœuds DOM spécifiques.\nPour ce faire, ajoutez l'attribut `data-frankenphp-hot-reload-preserve` à l'élément HTML concerné :\n\n```html\n<div data-frankenphp-hot-reload-preserve><!-- Ma barre de développement --></div>\n```\n\n## Mode Worker\n\nSi vous exécutez votre application en [mode Worker](worker.md), le script de votre application reste en mémoire.\nCela signifie que les modifications de votre code PHP ne seront pas reflétées immédiatement, même si le navigateur recharge la page.\n\nPour une meilleure expérience de développement, combinez `hot_reload` avec [la sous-directive `watch` dans la directive `worker`](config.md#surveillance-des-modifications-de-fichier).\n\n- `hot_reload` : rafraîchit le **navigateur** lorsque les fichiers changent\n- `worker.watch` : redémarre le worker lorsque les fichiers changent\n\n```caddy\nlocalhost\n\nmercure {\n    anonymous\n}\n\nroot public/\nphp_server {\n    hot_reload\n    worker {\n        file /path/to/my_worker.php\n        watch\n    }\n}\n```\n\n## Comment ça fonctionne\n\n1. **Surveillance** : FrankenPHP surveille le système de fichiers pour les modifications en utilisant [la bibliothèque `e-dant/watcher`](https://github.com/e-dant/watcher) en interne (nous avons contribué à son binding Go).\n2. **Redémarrage (mode Worker)** : si `watch` est activé dans la configuration du worker, le worker PHP est redémarré pour charger le nouveau code.\n3. **Envoi** : un payload JSON contenant la liste des fichiers modifiés est envoyé au [hub Mercure](https://mercure.rocks) intégré.\n4. **Réception** : le navigateur, à l'écoute via la bibliothèque JavaScript, reçoit l'événement Mercure.\n5. **Mise à jour** :\n\n- Si **Idiomorph** est détecté, il récupère le contenu mis à jour et transforme le HTML actuel pour correspondre au nouvel état, appliquant les modifications instantanément sans perdre l'état.\n- Sinon, `window.location.reload()` est appelé pour rafraîchir la page.\n"
  },
  {
    "path": "docs/fr/known-issues.md",
    "content": "# Problèmes Connus\n\n## Extensions PHP non prises en charge\n\nLes extensions suivantes sont connues pour ne pas être compatibles avec FrankenPHP :\n\n| Nom                                                                                                         | Raison          | Alternatives                                                                                                         |\n| ----------------------------------------------------------------------------------------------------------- | --------------- | -------------------------------------------------------------------------------------------------------------------- |\n| [imap](https://www.php.net/manual/en/imap.installation.php)                                                 | Non thread-safe | [javanile/php-imap2](https://github.com/javanile/php-imap2), [webklex/php-imap](https://github.com/Webklex/php-imap) |\n| [newrelic](https://docs.newrelic.com/docs/apm/agents/php-agent/getting-started/introduction-new-relic-php/) | Non thread-safe | -                                                                                                                    |\n\n## Extensions PHP boguées\n\nLes extensions suivantes ont des bugs connus ou des comportements inattendus lorsqu'elles sont utilisées avec FrankenPHP :\n\n| Nom                                                           | Problème                                                                                                                                                                                                                                                                                                                                      |\n| ------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [ext-openssl](https://www.php.net/manual/fr/book.openssl.php) | Lors de l'utilisation d'une version statique de FrankenPHP (construite avec la libc musl), l'extension OpenSSL peut planter sous de fortes charges. Une solution consiste à utiliser une version liée dynamiquement (comme celle utilisée dans les images Docker). Ce bogue est [suivi par PHP](https://github.com/php/php-src/issues/13648). |\n\n## get_browser\n\nLa fonction [get_browser()](https://www.php.net/manual/fr/function.get-browser.php) semble avoir de mauvaises performances après un certain temps. Une solution est de mettre en cache (par exemple, avec [APCu](https://www.php.net/manual/en/book.apcu.php)) les résultats par agent utilisateur, car ils sont statiques.\n\n## Binaire autonome et images Docker basées sur Alpine\n\nLe binaire autonome et les images Docker basées sur Alpine (`dunglas/frankenphp:*-alpine`) utilisent [musl libc](https://musl.libc.org/) au lieu de [glibc et ses amis](https://www.etalabs.net/compare_libcs.html), pour garder une taille de binaire plus petite. Cela peut entraîner des problèmes de compatibilité. En particulier, le drapeau glob `GLOB_BRACE` n'est [pas disponible](https://www.php.net/manual/fr/function.glob.php).\n\n## Utilisation de `https://127.0.0.1` avec Docker\n\nPar défaut, FrankenPHP génère un certificat TLS pour `localhost`.\nC'est l'option la plus simple et recommandée pour le développement local.\n\nSi vous voulez vraiment utiliser `127.0.0.1` comme hôte, il est possible de configurer FrankenPHP pour générer un certificat pour cela en définissant le nom du serveur à `127.0.0.1`.\n\nMalheureusement, cela ne suffit pas lors de l'utilisation de Docker à cause de [son système de gestion des réseaux](https://docs.docker.com/network/).\nVous obtiendrez une erreur TLS similaire à `curl: (35) LibreSSL/3.3.6: error:1404B438:SSL routines:ST_CONNECT:tlsv1 alert internal error`.\n\nSi vous utilisez Linux, une solution est d'utiliser [le pilote de réseau \"hôte\"](https://docs.docker.com/network/network-tutorial-host/) :\n\n```console\ndocker run \\\n    -e SERVER_NAME=\"127.0.0.1\" \\\n    -v $PWD:/app/public \\\n    --network host \\\n    dunglas/frankenphp\n```\n\nLe pilote de réseau \"hôte\" n'est pas pris en charge sur Mac et Windows. Sur ces plateformes, vous devrez deviner l'adresse IP du conteneur et l'inclure dans les noms de serveur.\n\nExécutez la commande `docker network inspect bridge` et inpectez la clef `Containers` pour identifier la dernière adresse IP attribuée sous la clef `IPv4Address`, puis incrémentez-la d'un. Si aucun conteneur n'est en cours d'exécution, la première adresse IP attribuée est généralement `172.17.0.2`.\n\nEnsuite, incluez ceci dans la variable d'environnement `SERVER_NAME` :\n\n```console\ndocker run \\\n    -e SERVER_NAME=\"127.0.0.1, 172.17.0.3\" \\\n    -v $PWD:/app/public \\\n    -p 80:80 -p 443:443 -p 443:443/udp \\\n    dunglas/frankenphp\n```\n\n> [!CAUTION]\n>\n> Assurez-vous de remplacer `172.17.0.3` par l'IP qui sera attribuée à votre conteneur.\n\nVous devriez maintenant pouvoir accéder à `https://127.0.0.1` depuis la machine hôte.\n\nSi ce n'est pas le cas, lancez FrankenPHP en mode debug pour essayer de comprendre le problème :\n\n```console\ndocker run \\\n    -e CADDY_GLOBAL_OPTIONS=\"debug\" \\\n    -e SERVER_NAME=\"127.0.0.1\" \\\n    -v $PWD:/app/public \\\n    -p 80:80 -p 443:443 -p 443:443/udp \\\n    dunglas/frankenphp\n```\n\n## Scripts Composer Faisant Références à `@php`\n\nLes [scripts Composer](https://getcomposer.org/doc/articles/scripts.md) peuvent vouloir exécuter un binaire PHP pour certaines tâches, par exemple dans [un projet Laravel](laravel.md) pour exécuter `@php artisan package:discover --ansi`. Cela [echoue actuellement](https://github.com/php/frankenphp/issues/483#issuecomment-1899890915) pour deux raisons :\n\n- Composer ne sait pas comment appeler le binaire FrankenPHP ;\n- Composer peut ajouter des paramètres PHP en utilisant le paramètre `-d` dans la commande, ce que FrankenPHP ne supporte pas encore.\n\nComme solution de contournement, nous pouvons créer un script shell dans `/usr/local/bin/php` qui supprime les paramètres non supportés et appelle ensuite FrankenPHP :\n\n```bash\n#!/usr/bin/env bash\nargs=(\"$@\")\nindex=0\nfor i in \"$@\"\ndo\n    if [ \"$i\" == \"-d\" ]; then\n        unset 'args[$index]'\n        unset 'args[$index+1]'\n    fi\n    index=$((index+1))\ndone\n\n/usr/local/bin/frankenphp php-cli ${args[@]}\n```\n\nEnsuite, mettez la variable d'environnement `PHP_BINARY` au chemin de notre script `php` et lancez Composer :\n\n```console\nexport PHP_BINARY=/usr/local/bin/php\ncomposer install\n```\n\n## Résolution des problèmes TLS/SSL avec les binaires statiques\n\nLorsque vous utilisez les binaires statiques, vous pouvez rencontrer les erreurs suivantes liées à TLS, par exemple lors de l'envoi de courriels utilisant STARTTLS :\n\n```text\nUnable to connect with STARTTLS: stream_socket_enable_crypto(): SSL operation failed with code 5. OpenSSL Error messages:\nerror:80000002:system library::No such file or directory\nerror:80000002:system library::No such file or directory\nerror:80000002:system library::No such file or directory\nerror:0A000086:SSL routines::certificate verify failed\n```\n\nComme le binaire statique ne contient pas de certificats TLS, vous devez indiquer à OpenSSL l'installation de vos certificats CA locaux.\n\nInspectez la sortie de [`openssl_get_cert_locations()`](https://www.php.net/manual/en/function.openssl-get-cert-locations.php),\npour trouver l'endroit où les certificats CA doivent être installés et stockez-les à cet endroit.\n\n> [!WARNING]\n>\n> Les contextes Web et CLI peuvent avoir des paramètres différents.\n> Assurez-vous d'exécuter `openssl_get_cert_locations()` dans le bon contexte.\n\n[Les certificats CA extraits de Mozilla peuvent être téléchargés sur le site de cURL](https://curl.se/docs/caextract.html).\n\nAlternativement, de nombreuses distributions, y compris Debian, Ubuntu, et Alpine fournissent des paquets nommés `ca-certificates` qui contiennent ces certificats.\n\nIl est également possible d'utiliser `SSL_CERT_FILE` et `SSL_CERT_DIR` pour indiquer à OpenSSL où chercher les certificats CA :\n\n```console\n# Définir les variables d'environnement des certificats TLS\nexport SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt\nexport SSL_CERT_DIR=/etc/ssl/certs\n```\n"
  },
  {
    "path": "docs/fr/laravel.md",
    "content": "# Laravel\n\n## Docker\n\nDéployer une application web [Laravel](https://laravel.com) avec FrankenPHP est très facile. Il suffit de monter le projet dans le répertoire `/app` de l'image Docker officielle.\n\nExécutez cette commande depuis le répertoire principal de votre application Laravel :\n\n```console\ndocker run -p 80:80 -p 443:443 -p 443:443/udp -v $PWD:/app dunglas/frankenphp\n```\n\nEt profitez !\n\n## Installation Locale\n\nVous pouvez également exécuter vos projets Laravel avec FrankenPHP depuis votre machine locale :\n\n1. [Téléchargez le binaire correspondant à votre système](README.md#binaire-autonome)\n2. Ajoutez la configuration suivante dans un fichier nommé `Caddyfile` placé dans le répertoire racine de votre projet Laravel :\n\n   ```caddyfile\n   {\n   \tfrankenphp\n   }\n\n   # Le nom de domaine de votre serveur\n   localhost {\n   \t# Définir le répertoire racine sur le dossier public/\n   \troot public/\n   \t# Autoriser la compression (optionnel)\n   \tencode zstd br gzip\n   \t# Exécuter les scripts PHP du dossier public/ et servir les assets\n   \tphp_server {\n   \t\ttry_files {path} index.php\n   \t}\n   }\n   ```\n\n3. Démarrez FrankenPHP depuis le répertoire racine de votre projet Laravel : `frankenphp run`\n\n## Laravel Octane\n\nOctane peut être installé via le gestionnaire de paquets Composer :\n\n```console\ncomposer require laravel/octane\n```\n\nAprès avoir installé Octane, vous pouvez exécuter la commande Artisan `octane:install`, qui installera le fichier de configuration d'Octane dans votre application :\n\n```console\nphp artisan octane:install --server=frankenphp\n```\n\nLe serveur Octane peut être démarré via la commande Artisan `octane:frankenphp`.\n\n```console\nphp artisan octane:frankenphp\n```\n\nLa commande `octane:frankenphp` peut prendre les options suivantes :\n\n- `--host` : L'adresse IP à laquelle le serveur doit se lier (par défaut : `127.0.0.1`)\n- `--port` : Le port sur lequel le serveur doit être disponible (par défaut : `8000`)\n- `--admin-port` : Le port sur lequel le serveur administratif doit être disponible (par défaut : `2019`)\n- `--workers` : Le nombre de workers qui doivent être disponibles pour traiter les requêtes (par défaut : `auto`)\n- `--max-requests` : Le nombre de requêtes à traiter avant de recharger le serveur (par défaut : `500`)\n- `--caddyfile` : Le chemin vers le fichier `Caddyfile` de FrankenPHP\n- `--https` : Activer HTTPS, HTTP/2, et HTTP/3, et générer automatiquement et renouveler les certificats\n- `--http-redirect` : Activer la redirection HTTP vers HTTPS (uniquement activé si --https est passé)\n- `--watch` : Recharger automatiquement le serveur lorsque l'application est modifiée\n- `--poll` : Utiliser le sondage du système de fichiers pendant la surveillance pour surveiller les fichiers sur un réseau\n- `--log-level` : Enregistrer les messages au niveau de journalisation spécifié ou au-dessus, en utilisant le logger natif de Caddy\n\n> [!TIP]\n> Pour obtenir des logs structurés en JSON logs (utile quand vous utilisez des solutions d'analyse de logs), passez explicitement l'option `--log-level`.\n\nEn savoir plus sur Laravel Octane [dans sa documentation officielle](https://laravel.com/docs/octane).\n\n## Les Applications Laravel En Tant Que Binaires Autonomes\n\nEn utilisant la [fonctionnalité d'intégration d'applications de FrankenPHP](embed.md), il est possible de distribuer\nles applications Laravel sous forme de binaires autonomes.\n\nSuivez ces étapes pour empaqueter votre application Laravel en tant que binaire autonome pour Linux :\n\n1. Créez un fichier nommé `static-build.Dockerfile` dans le dépôt de votre application :\n\n   ```dockerfile\n   FROM --platform=linux/amd64 dunglas/frankenphp:static-builder-gnu\n   # Si vous avez l'intention d'exécuter le binaire sur des systèmes musl-libc, utilisez plutôt static-builder-musl\n\n   # Copiez votre application\n   WORKDIR /go/src/app/dist/app\n   COPY . .\n\n   # Supprimez les tests et autres fichiers inutiles pour gagner de la place\n   # Alternativement, ajoutez ces fichiers à un fichier .dockerignore\n   RUN rm -Rf tests/\n\n   # Copiez le fichier .env\n   RUN cp .env.example .env\n   # Modifier APP_ENV et APP_DEBUG pour qu'ils soient prêts pour la production\n   RUN sed -i'' -e 's/^APP_ENV=.*/APP_ENV=production/' -e 's/^APP_DEBUG=.*/APP_DEBUG=false/' .env\n\n   # Apportez d'autres modifications à votre fichier .env si nécessaire\n\n   # Installez les dépendances\n   RUN composer install --ignore-platform-reqs --no-dev -a\n\n   # Construire le binaire statique\n   WORKDIR /go/src/app/\n   RUN EMBED=dist/app/ ./build-static.sh\n   ```\n\n   > [!CAUTION]\n   >\n   > Certains fichiers `.dockerignore` ignoreront le répertoire `vendor/`\n   > et les fichiers `.env`. Assurez-vous d'ajuster ou de supprimer le fichier `.dockerignore` avant la construction.\n\n2. Build:\n\n   ```console\n   docker build -t static-laravel-app -f static-build.Dockerfile .\n   ```\n\n3. Extraire le binaire\n\n   ```console\n   docker cp $(docker create --name static-laravel-app-tmp static-laravel-app):/go/src/app/dist/frankenphp-linux-x86_64 frankenphp ; docker rm static-laravel-app-tmp\n   ```\n\n4. Remplir les caches :\n\n   ```console\n   frankenphp php-cli artisan optimize\n   ```\n\n5. Exécutez les migrations de base de données (s'il y en a) :\n\n   ```console\n   frankenphp php-cli artisan migrate\n   ```\n\n6. Générer la clé secrète de l'application :\n\n   ```console\n   frankenphp php-cli artisan key:generate\n   ```\n\n7. Démarrez le serveur:\n\n   ```console\n   frankenphp php-server\n   ```\n\nVotre application est maintenant prête !\n\nPour en savoir plus sur les options disponibles et sur la construction de binaires pour d'autres systèmes d'exploitation,\nconsultez la documentation [Applications PHP en tant que binaires autonomes](embed.md).\n\n### Changer le chemin de stockage\n\nPar défaut, Laravel stocke les fichiers téléchargés, les caches, les logs, etc. dans le répertoire `storage/` de l'application.\nCeci n'est pas adapté aux applications embarquées, car chaque nouvelle version sera extraite dans un répertoire temporaire différent.\n\nDéfinissez la variable d'environnement `LARAVEL_STORAGE_PATH` (par exemple, dans votre fichier `.env`) ou appelez la méthode `Illuminate\\Foundation\\Application::useStoragePath()` pour utiliser un répertoire en dehors du répertoire temporaire.\n\n### Exécuter Octane avec des binaires autonomes\n\nIl est même possible d'empaqueter les applications Laravel Octane en tant que binaires autonomes !\n\nPour ce faire, [installez Octane correctement](#laravel-octane) et suivez les étapes décrites dans [la section précédente](#les-applications-laravel-en-tant-que-binaires-autonomes).\n\nEnsuite, pour démarrer FrankenPHP en mode worker via Octane, exécutez :\n\n```console\nPATH=\"$PWD:$PATH\" frankenphp php-cli artisan octane:frankenphp\n```\n\n> [!CAUTION]\n>\n> Pour que la commande fonctionne, le binaire autonome **doit** être nommé `frankenphp`\n> car Octane a besoin d'un programme nommé `frankenphp` disponible dans le chemin\n"
  },
  {
    "path": "docs/fr/mercure.md",
    "content": "# Temps Réel\n\nFrankenPHP est livré avec un hub [Mercure](https://mercure.rocks) intégré.\nMercure permet de pousser des événements en temps réel vers tous les appareils connectés : ils recevront un événement JavaScript instantanément.\n\nAucune bibliothèque JS ou SDK requis !\n\n![Mercure](../mercure-hub.png)\n\nPour activer le hub Mercure, mettez à jour le `Caddyfile` comme décrit [sur le site de Mercure](https://mercure.rocks/docs/hub/config).\n\nPour pousser des mises à jour Mercure depuis votre code, nous recommandons le [Composant Mercure de Symfony](https://symfony.com/components/Mercure) (vous n'avez pas besoin du framework full stack Symfony pour l'utiliser).\n"
  },
  {
    "path": "docs/fr/metrics.md",
    "content": "# Métriques\n\nLorsque les [métriques Caddy](https://caddyserver.com/docs/metrics) sont activées, FrankenPHP expose les métriques suivantes :\n\n- `frankenphp_total_threads` : Le nombre total de threads PHP.\n- `frankenphp_busy_threads` : Le nombre de threads PHP en cours de traitement d'une requête (les workers en cours d'exécution consomment toujours un thread).\n- `frankenphp_queue_depth` : Le nombre de requêtes régulières en file d'attente\n- `frankenphp_total_workers{worker=« [nom_du_worker] »}` : Le nombre total de workers.\n- `frankenphp_busy_workers{worker=« [nom_du_worker] »}` : Le nombre de workers qui traitent actuellement une requête.\n- `frankenphp_worker_request_time{worker=« [nom_du_worker] »}` : Le temps passé à traiter les requêtes par tous les workers.\n- `frankenphp_worker_request_count{worker=« [nom_du_worker] »}` : Le nombre de requêtes traitées par tous les workers.\n- `frankenphp_ready_workers{worker=« [nom_du_worker] »}` : Le nombre de workers qui ont appelé `frankenphp_handle_request` au moins une fois.\n- `frankenphp_worker_crashes{worker=« [nom_du_worker] »}` : Le nombre de fois où un worker s'est arrêté de manière inattendue.\n- `frankenphp_worker_restarts{worker=« [nom_du_worker] »}` : Le nombre de fois où un worker a été délibérément redémarré.\n- `frankenphp_worker_queue_depth{worker=« [nom_du_worker] »}` : Le nombre de requêtes en file d'attente.\n\nPour les métriques de worker, le placeholder `[nom_du_worker]` est remplacé par le nom du worker dans le Caddyfile, sinon le chemin absolu du fichier du worker sera utilisé.\n"
  },
  {
    "path": "docs/fr/performance.md",
    "content": "# Performance\n\nPar défaut, FrankenPHP essaie d'offrir un bon compromis entre performance et facilité d'utilisation.\nCependant, il est possible d'améliorer considérablement les performances en utilisant une configuration appropriée.\n\n## Nombre de threads et de workers\n\nPar défaut, FrankenPHP démarre deux fois plus de threads et de workers (en mode worker) que le nombre de cœurs de CPU disponibles.\n\nLes valeurs appropriées dépendent fortement de la manière dont votre application est écrite, de ce qu'elle fait et de votre matériel.\nNous recommandons vivement de modifier ces valeurs. Pour une stabilité optimale du système, il est recommandé d'avoir `num_threads` x `memory_limit` < `available_memory`.\n\nPour trouver les bonnes valeurs, il est préférable d'effectuer des tests de charge simulant le trafic réel.\n[k6](https://k6.io) et [Gatling](https://gatling.io) sont de bons outils pour cela.\n\nPour configurer le nombre de threads, utilisez l'option `num_threads` des directives `php_server` et `php`.\nPour changer le nombre de workers, utilisez l'option `num` de la section `worker` de la directive `frankenphp`.\n\n### `max_threads`\n\nBien qu'il soit toujours préférable de savoir exactement à quoi ressemblera votre trafic, les applications réelles\nont tendance à être plus imprévisibles. La [configuration](config.md#configuration-du-caddyfile) `max_threads` permet à FrankenPHP de créer automatiquement des threads supplémentaires au moment de l'exécution, jusqu'à la limite spécifiée.\n`max_threads` peut vous aider à déterminer le nombre de threads dont vous avez besoin pour gérer votre trafic et peut rendre le serveur plus résistant aux pics de latence.\nSi elle est fixée à `auto`, la limite sera estimée en fonction de la valeur de `memory_limit` dans votre `php.ini`. Si ce n'est pas possible,\n`auto` prendra par défaut 2x `num_threads`. Gardez à l'esprit que `auto` peut fortement sous-estimer le nombre de threads nécessaires.\n`max_threads` est similaire à [pm.max_children](https://www.php.net/manual/en/install.fpm.configuration.php#pm.max-children) de PHP FPM. La principale différence est que FrankenPHP utilise des threads au lieu de\nprocessus et les délègue automatiquement à différents scripts worker et au 'mode classique' selon les besoins.\n\n## Mode worker\n\nActiver [le mode worker](worker.md) améliore considérablement les performances,\nmais votre application doit être adaptée pour être compatible avec ce mode :\nvous devez créer un script worker et vous assurer que l'application n'a pas de fuite de mémoire.\n\n## Ne pas utiliser musl\n\nLa variante Alpine Linux des images Docker officielles et les binaires par défaut que nous fournissons utilisent [la bibliothèque musl](https://musl.libc.org).\n\nPHP est connu pour être [plus lent](https://gitlab.alpinelinux.org/alpine/aports/-/issues/14381) lorsqu'il utilise cette bibliothèque C alternative au lieu de la bibliothèque GNU traditionnelle,\nsurtout lorsqu'il est compilé en mode ZTS (_thread-safe_), ce qui est nécessaire pour FrankenPHP. La différence peut être significative dans un environnement fortement multithreadé.\n\nEn outre, [certains bogues ne se produisent que lors de l'utilisation de musl](https://github.com/php/php-src/issues?q=sort%3Aupdated-desc+is%3Aissue+is%3Aopen+label%3ABug+musl).\n\nDans les environnements de production, nous recommandons d'utiliser FrankenPHP lié à glibc, compilé avec un niveau d'optimisation approprié.\n\nCela peut être réalisé en utilisant les images Docker Debian, en utilisant [les paquets .deb, .rpm ou .apk proposés par l'un de nos mainteneurs](https://pkgs.henderkes.com), ou en [compilant FrankenPHP à partir des sources](compile.md).\n\nPour des conteneurs plus légers ou plus sécurisés, vous pourriez envisager [une image Debian renforcée](docker.md#hardening-images) plutôt qu'Alpine.\n\n## Configuration du runtime Go\n\nFrankenPHP est écrit en Go.\n\nEn général, le runtime Go ne nécessite pas de configuration particulière, mais dans certaines circonstances,\nune configuration spécifique améliore les performances.\n\nVous voudrez probablement mettre la variable d'environnement `GODEBUG` à `cgocheck=0` (la valeur par défaut dans les images Docker de FrankenPHP).\n\nSi vous exécutez FrankenPHP dans des conteneurs (Docker, Kubernetes, LXC...) et que vous limitez la mémoire disponible pour les conteneurs,\nmettez la variable d'environnement `GOMEMLIMIT` à la quantité de mémoire disponible.\n\nPour plus de détails, [la page de documentation Go dédiée à ce sujet](https://pkg.go.dev/runtime#hdr-Environment_Variables) est à lire absolument pour tirer le meilleur parti du runtime.\n\n## `file_server`\n\nPar défaut, la directive `php_server` met automatiquement en place un serveur de fichiers\npour servir les fichiers statiques (assets) stockés dans le répertoire racine.\n\nCette fonctionnalité est pratique, mais a un coût.\nPour la désactiver, utilisez la configuration suivante :\n\n```caddyfile\nphp_server {\n    file_server off\n}\n```\n\n## `try_files`\n\nEn plus des fichiers statiques et des fichiers PHP, `php_server` essaiera aussi de servir les fichiers d'index\net d'index de répertoire de votre application (`/path/` -> `/path/index.php`). Si vous n'avez pas besoin des index de répertoires,\nvous pouvez les désactiver en définissant explicitement `try_files` comme ceci :\n\n```caddyfile\nphp_server {\n    try_files {path} index.php\n    root /root/to/your/app # l'ajout explicite de la racine ici permet une meilleure mise en cache\n}\n```\n\nCela permet de réduire considérablement le nombre d'opérations inutiles sur les fichiers.\nUn équivalent worker de la configuration précédente serait :\n\n```caddyfile\nroute {\n    php_server { # utilisez \"php\" au lieu de \"php_server\" si vous n'avez pas du tout besoin du serveur de fichiers\n        root /root/to/your/app\n        worker /path/to/worker.php {\n            match * # envoie toutes les requêtes directement au worker\n        }\n    }\n}\n```\n\nUne approche alternative avec 0 opérations inutiles sur le système de fichiers serait d'utiliser la directive `php`\net de diviser les fichiers de PHP par chemin. Cette approche fonctionne bien si votre application entière est servie par un seul fichier d'entrée.\nUn exemple de [configuration](config.md#configuration-du-caddyfile) qui sert des fichiers statiques derrière un dossier `/assets` pourrait ressembler à ceci :\n\n```caddyfile\nroute {\n    @assets {\n        path /assets/*\n    }\n\n    # tout ce qui se trouve derrière /assets est géré par le serveur de fichiers\n    file_server @assets {\n        root /root/to/your/app\n    }\n\n    # tout ce qui n'est pas dans /assets est géré par votre fichier PHP d'index ou worker\n    rewrite index.php\n    php {\n        root /root/to/your/app # l'ajout explicite de la racine ici permet une meilleure mise en cache\n    }\n}\n```\n\n## _Placeholders_\n\nVous pouvez utiliser des [_placeholders_](https://caddyserver.com/docs/conventions#placeholders) dans les directives `root` et `env`.\nCependant, cela empêche la mise en cache de ces valeurs et a un coût important en termes de performances.\n\nSi possible, évitez les _placeholders_ dans ces directives.\n\n## `resolve_root_symlink`\n\nPar défaut, si le _document root_ est un lien symbolique, il est automatiquement résolu par FrankenPHP (c'est nécessaire pour le bon fonctionnement de PHP).\nSi la racine du document n'est pas un lien symbolique, vous pouvez désactiver cette fonctionnalité.\n\n```caddyfile\nphp_server {\n    resolve_root_symlink false\n}\n```\n\nCela améliorera les performances si la directive `root` contient des [_placeholders_](https://caddyserver.com/docs/conventions#placeholders).\nLe gain sera négligeable dans les autres cas.\n\n## Journaux\n\nLa journalisation est évidemment très utile, mais, par définition, elle nécessite des opérations d'_I/O_ et des allocations de mémoire,\nce qui réduit considérablement les performances.\nAssurez-vous de [définir le niveau de journalisation](https://caddyserver.com/docs/caddyfile/options#log) correctement,\net de ne journaliser que ce qui est nécessaire.\n\n## Performances de PHP\n\nFrankenPHP utilise l'interpréteur PHP officiel.\nToutes les optimisations de performances habituelles liées à PHP s'appliquent à FrankenPHP.\n\nEn particulier :\n\n- vérifiez que [OPcache](https://www.php.net/manual/en/book.opcache.php) est installé, activé et correctement configuré\n- activez [les optimisations de l'autoloader de Composer](https://getcomposer.org/doc/articles/autoloader-optimization.md)\n- assurez-vous que le cache `realpath` est suffisamment grand pour les besoins de votre application\n- utilisez le [pré-chargement](https://www.php.net/manual/en/opcache.preloading.php)\n\nPour plus de détails, lisez [l'entrée de la documentation dédiée de Symfony](https://symfony.com/doc/current/performance.html)\n(la plupart des conseils sont utiles même si vous n'utilisez pas Symfony).\n\n## Division du pool de threads\n\nIl est courant que les applications interagissent avec des services externes lents, comme une\nAPI qui a tendance à être peu fiable sous forte charge ou qui met constamment plus de 10 secondes à répondre.\nDans de tels cas, il peut être bénéfique de diviser le pool de threads pour avoir des pools \"lents\" dédiés.\nCela empêche les points d'accès lents de consommer toutes les ressources/threads du serveur et\nlimite la concurrence des requêtes se dirigeant vers le point d'accès lent, à l'instar d'un\npool de connexions.\n\n```caddyfile\nexample.com {\n    php_server {\n        root /app/public # la racine de votre application\n        worker index.php {\n            match /slow-endpoint/* # toutes les requêtes avec le chemin /slow-endpoint/* sont gérées par ce pool de threads\n            num 1 # minimum 1 thread pour les requêtes correspondant à /slow-endpoint/*\n            max_threads 20 # autorise jusqu'à 20 threads pour les requêtes correspondant à /slow-endpoint/*, si nécessaire\n        }\n        worker index.php {\n            match * # toutes les autres requêtes sont gérées séparément\n            num 1 # minimum 1 thread pour les autres requêtes, même si les points d'accès lents commencent à bloquer\n            max_threads 20 # autorise jusqu'à 20 threads pour les autres requêtes, si nécessaire\n        }\n    }\n}\n```\n\nDe manière générale, il est également conseillé de gérer les points d'accès très lents de manière asynchrone, en utilisant des mécanismes pertinents tels que les files d'attente de messages.\n"
  },
  {
    "path": "docs/fr/production.md",
    "content": "# Déploiement en Production\n\nDans ce tutoriel, nous apprendrons comment déployer une application PHP sur un serveur unique en utilisant Docker Compose.\n\nSi vous utilisez Symfony, lisez plutôt la page de documentation \"[Déployer en production](https://github.com/dunglas/symfony-docker/blob/main/docs/production.md)\" du projet Symfony Docker (qui utilise FrankenPHP).\n\nSi vous utilisez API Platform (qui utilise également FrankenPHP), référez-vous à [la documentation de déploiement du framework](https://api-platform.com/docs/deployment/).\n\n## Préparer votre application\n\nTout d'abord, créez un `Dockerfile` dans le répertoire racine de votre projet PHP :\n\n```dockerfile\nFROM dunglas/frankenphp\n\n# Assurez-vous de remplacer \"your-domain-name.example.com\" par votre nom de domaine\nENV SERVER_NAME=your-domain-name.example.com\n# Si vous souhaitez désactiver HTTPS, utilisez cette valeur à la place :\n#ENV SERVER_NAME=:80\n\n# Si votre projet n'utilise pas le répertoire \"public\" comme racine web, vous pouvez le définir ici :\n# ENV SERVER_ROOT=web/\n\n# Activer les paramètres de production de PHP\nRUN mv \"$PHP_INI_DIR/php.ini-production\" \"$PHP_INI_DIR/php.ini\"\n\n# Copiez les fichiers PHP de votre projet dans le répertoire public\nCOPY . /app/public\n# Si vous utilisez Symfony ou Laravel, vous devez copier l'intégralité du projet à la place :\n#COPY . /app\n```\n\nConsultez \"[Construire une image Docker personnalisée](docker.md)\" pour plus de détails et d'options,\net pour apprendre à personnaliser la configuration, installer des extensions PHP et des modules Caddy.\n\nSi votre projet utilise Composer, assurez-vous de l'inclure dans l'image Docker et d'installer vos dépendances.\n\nEnsuite, ajoutez un fichier `compose.yaml` :\n\n```yaml\nservices:\n  php:\n    image: dunglas/frankenphp\n    restart: always\n    ports:\n      - \"80:80\" # HTTP\n      - \"443:443\" # HTTPS\n      - \"443:443/udp\" # HTTP/3\n    volumes:\n      - caddy_data:/data\n      - caddy_config:/config\n\n# Volumes nécessaires pour les certificats et la configuration de Caddy\nvolumes:\n  caddy_data:\n  caddy_config:\n```\n\n> [!NOTE]\n>\n> Les exemples précédents sont destinés à une utilisation en production.\n> En développement, vous pourriez vouloir utiliser un volume, une configuration PHP différente et une valeur différente pour la variable d'environnement `SERVER_NAME`.\n>\n> Jetez un œil au projet [Symfony Docker](https://github.com/dunglas/symfony-docker)\n> (qui utilise FrankenPHP) pour un exemple plus avancé utilisant des images multi-étapes,\n> Composer, des extensions PHP supplémentaires, etc.\n\nPour finir, si vous utilisez Git, commitez ces fichiers et poussez-les.\n\n## Préparer un serveur\n\nPour déployer votre application en production, vous avez besoin d'un serveur.\nDans ce tutoriel, nous utiliserons une machine virtuelle fournie par DigitalOcean, mais n'importe quel serveur Linux peut fonctionner.\nSi vous avez déjà un serveur Linux avec Docker installé, vous pouvez passer directement à [la section suivante](#configurer-un-nom-de-domaine).\n\nSinon, utilisez [ce lien affilié](https://m.do.co/c/5d8aabe3ab80) pour obtenir 200$ de crédit gratuit, créez un compte, puis cliquez sur \"Créer un Droplet\".\nEnsuite, cliquez sur l'onglet \"Marketplace\" sous la section \"Choisir une image\" et recherchez l'application nommée \"Docker\".\nCela provisionnera un serveur Ubuntu avec les dernières versions de Docker et Docker Compose déjà installées !\n\nPour des fins de test, les plans les moins chers seront suffisants.\nPour une utilisation en production réelle, vous voudrez probablement choisir un plan dans la section \"General Usage\" pour répondre à vos besoins.\n\n![Déployer FrankenPHP sur DigitalOcean avec Docker](../digitalocean-droplet.png)\n\nVous pouvez conserver les paramètres par défaut pour les autres paramètres, ou les ajuster selon vos besoins.\nN'oubliez pas d'ajouter votre clé SSH ou de créer un mot de passe puis appuyez sur le bouton \"Finalize and create\".\n\nEnsuite, attendez quelques secondes pendant que votre Droplet est en cours de provisionnement.\nLorsque votre Droplet est prêt, utilisez SSH pour vous connecter :\n\n```console\nssh root@<droplet-ip>\n```\n\n## Configurer un nom de domaine\n\nDans la plupart des cas, vous souhaiterez associer un nom de domaine à votre site.\nSi vous ne possédez pas encore de nom de domaine, vous devrez en acheter un via un registraire.\n\nEnsuite, créez un enregistrement DNS de type `A` pour votre nom de domaine pointant vers l'adresse IP de votre serveur :\n\n```dns\nyour-domain-name.example.com.  IN  A     207.154.233.113\n```\n\nExemple avec le service DigitalOcean Domains (\"Networking\" > \"Domains\") :\n\n![Configurer les DNS sur DigitalOcean](../digitalocean-dns.png)\n\n> [!NOTE]\n>\n> Let's Encrypt, le service utilisé par défaut par FrankenPHP pour générer automatiquement un certificat TLS, ne prend pas en charge l'utilisation d'adresses IP nues. L'utilisation d'un nom de domaine est obligatoire pour utiliser Let's Encrypt.\n\n## Déploiement\n\nCopiez votre projet sur le serveur en utilisant `git clone`, `scp`, ou tout autre outil qui pourrait répondre à votre besoin.\nSi vous utilisez GitHub, vous voudrez peut-être utiliser [une clef de déploiement](https://docs.github.com/en/free-pro-team@latest/developers/overview/managing-deploy-keys#deploy-keys).\nLes clés de déploiement sont également [prises en charge par GitLab](https://docs.gitlab.com/ee/user/project/deploy_keys/).\n\nExemple avec Git :\n\n```console\ngit clone git@github.com:<username>/<project-name>.git\n```\n\nAccédez au répertoire contenant votre projet (`<project-name>`), et démarrez l'application en mode production :\n\n```console\ndocker compose up -d --wait\n```\n\nVotre serveur est opérationnel, et un certificat HTTPS a été automatiquement généré pour vous.\nRendez-vous sur `https://your-domain-name.example.com` !\n\n> [!CAUTION]\n>\n> Docker peut avoir une couche de cache, assurez-vous d'avoir la bonne version de build pour chaque déploiement ou reconstruisez votre projet avec l'option `--no-cache` pour éviter les problèmes de cache.\n\n## Déploiement sur Plusieurs Nœuds\n\nSi vous souhaitez déployer votre application sur un cluster de machines, vous pouvez utiliser [Docker Swarm](https://docs.docker.com/engine/swarm/stack-deploy/), qui est compatible avec les fichiers Compose fournis.\nPour un déploiement sur Kubernetes, jetez un œil au [Helm chart fourni avec API Platform](https://api-platform.com/docs/deployment/kubernetes/), qui utilise FrankenPHP.\n"
  },
  {
    "path": "docs/fr/static.md",
    "content": "# Créer un binaire statique\n\nAu lieu d'utiliser une installation locale de la bibliothèque PHP, il est possible de créer un build statique de FrankenPHP grâce à l'excellent projet [static-php-cli](https://github.com/crazywhalecc/static-php-cli) (malgré son nom, ce projet prend en charge tous les SAPIs, pas seulement CLI).\n\nAvec cette méthode, un binaire portable unique contiendra l'interpréteur PHP, le serveur web Caddy et FrankenPHP !\n\nLes exécutables natifs entièrement statiques ne nécessitent aucune dépendance et peuvent même être exécutés sur une [image Docker `scratch`](https://docs.docker.com/build/building/base-images/#create-a-minimal-base-image-using-scratch).\nCependant, ils ne peuvent pas charger les extensions dynamiques de PHP (comme Xdebug) et ont quelques limitations parce qu'ils utilisent la librairie musl.\n\nLa plupart des binaires statiques ne nécessitent que la `glibc` et peuvent charger des extensions dynamiques.\n\nLorsque c'est possible, nous recommandons d'utiliser des binaires statiques basés sur la glibc.\n\nFrankenPHP permet également [d'embarquer l'application PHP dans le binaire statique](embed.md).\n\n## Linux\n\nNous fournissons des images Docker pour créer des binaires statiques pour Linux :\n\n### Build entièrement statique, basé sur musl\n\nPour un binaire entièrement statique qui fonctionne sur n'importe quelle distribution Linux sans dépendances,\nmais qui ne prend pas en charge le chargement dynamique des extensions :\n\n```console\ndocker buildx bake --load static-builder-musl\ndocker cp $(docker create --name static-builder-musl dunglas/frankenphp:static-builder-musl):/go/src/app/dist/frankenphp-linux-$(uname -m) frankenphp ; docker rm static-builder-musl\n```\n\nPour améliorer les performances dans les scénarios fortement concurrents, envisagez d'utiliser l'allocateur [mimalloc](https://github.com/microsoft/mimalloc).\n\n```console\ndocker buildx bake --load --set static-builder-musl.args.MIMALLOC=1 static-builder-musl\n```\n\n### Construction principalement statique (avec prise en charge des extensions dynamiques), basé sur la glibc\n\nPour un binaire qui supporte le chargement dynamique des extensions PHP tout en ayant les extensions sélectionnées compilées statiquement :\n\n```console\ndocker buildx bake --load static-builder-gnu\ndocker cp $(docker create --name static-builder-gnu dunglas/frankenphp:static-builder-gnu):/go/src/app/dist/frankenphp-linux-$(uname -m) frankenphp ; docker rm static-builder-gnu\n```\n\nCe binaire supporte toutes les versions 2.17 et supérieures de la glibc mais ne fonctionne pas sur les systèmes basés sur musl (comme Alpine Linux).\n\nLe binaire résultant, principalement statique (à l'exception de `glibc`), est nommé `frankenphp` et est disponible dans le répertoire courant.\n\nSi vous voulez construire le binaire statique sans Docker, jetez un coup d'œil aux instructions pour macOS, qui fonctionnent aussi pour Linux.\n\n### Extensions personnalisées\n\nPar défaut, la plupart des extensions PHP populaires sont compilées.\n\nPour réduire la taille du binaire et diminuer la surface d'attaque, vous pouvez choisir la liste des extensions à construire en utilisant l'argument Docker `PHP_EXTENSIONS`.\n\nPar exemple, exécutez la commande suivante pour ne construire que l'extension `opcache` :\n\n```console\ndocker buildx bake --load --set static-builder-musl.args.PHP_EXTENSIONS=opcache,pdo_sqlite static-builder-musl\n# ...\n```\n\nPour ajouter des bibliothèques permettant des fonctionnalités supplémentaires aux extensions que vous avez activées, vous pouvez utiliser l'argument Docker `PHP_EXTENSION_LIBS` :\n\n```console\ndocker buildx bake \\\n  --load \\\n  --set static-builder-musl.args.PHP_EXTENSIONS=gd \\\n  --set static-builder-musl.args.PHP_EXTENSION_LIBS=libjpeg,libwebp \\\n  static-builder-musl\n```\n\n### Modules supplémentaires de Caddy\n\nPour ajouter des modules Caddy supplémentaires ou passer d'autres arguments à [xcaddy](https://github.com/caddyserver/xcaddy), utilisez l'argument Docker `XCADDY_ARGS` :\n\n```console\ndocker buildx bake \\\n  --load \\\n  --set static-builder-musl.args.XCADDY_ARGS=\"--with github.com/darkweak/souin/plugins/caddy --with github.com/dunglas/caddy-cbrotli --with github.com/dunglas/mercure/caddy --with github.com/dunglas/vulcain/caddy\" \\\n  static-builder-musl\n```\n\nDans cet exemple, nous ajoutons le module de cache HTTP [Souin](https://souin.io) pour Caddy ainsi que les modules [cbrotli](https://github.com/dunglas/caddy-cbrotli), [Mercure](https://mercure.rocks) et [Vulcain](https://vulcain.rocks).\n\n> [!TIP]\n>\n> Les modules cbrotli, Mercure et Vulcain sont inclus par défaut si `XCADDY_ARGS` est vide ou n'est pas défini.\n> Si vous personnalisez la valeur de `XCADDY_ARGS`, vous devez les inclure explicitement si vous voulez qu'ils soient inclus.\n\nVoir aussi comment [personnaliser la construction](#personnalisation-de-la-construction)\n\n### Jeton GitHub\n\nSi vous atteignez la limite de taux d'appels de l'API GitHub, définissez un jeton d'accès personnel GitHub dans une variable d'environnement nommée `GITHUB_TOKEN` :\n\n```console\nGITHUB_TOKEN=\"xxx\" docker --load buildx bake static-builder-musl\n# ...\n```\n\n## macOS\n\nExécutez le script suivant pour créer un binaire statique pour macOS (vous devez avoir [Homebrew](https://brew.sh/) d'installé) :\n\n```console\ngit clone https://github.com/php/frankenphp\ncd frankenphp\n./build-static.sh\n```\n\nNote : ce script fonctionne également sur Linux (et probablement sur d'autres Unix) et est utilisé en interne par le builder statique basé sur Docker que nous fournissons.\n\n## Personnalisation de la construction\n\nLes variables d'environnement suivantes peuvent être transmises à `docker build` et au script `build-static.sh` pour personnaliser la construction statique :\n\n- `FRANKENPHP_VERSION` : la version de FrankenPHP à utiliser\n- `PHP_VERSION` : la version de PHP à utiliser\n- `PHP_EXTENSIONS` : les extensions PHP à construire ([liste des extensions prises en charge](https://static-php.dev/en/guide/extensions.html))\n- `PHP_EXTENSION_LIBS` : bibliothèques supplémentaires à construire qui ajoutent des fonctionnalités aux extensions\n- `XCADDY_ARGS` : arguments à passer à [xcaddy](https://github.com/caddyserver/xcaddy), par exemple pour ajouter des modules Caddy supplémentaires\n- `EMBED` : chemin de l'application PHP à intégrer dans le binaire\n- `CLEAN` : lorsque défini, `libphp` et toutes ses dépendances sont construites à partir de zéro (pas de cache)\n- `DEBUG_SYMBOLS` : lorsque défini, les symboles de débogage ne seront pas supprimés et seront ajoutés dans le binaire\n- `NO_COMPRESS`: ne pas compresser le binaire avec UPX\n- `MIMALLOC`: (expérimental, Linux seulement) remplace l'allocateur mallocng de musl par [mimalloc](https://github.com/microsoft/mimalloc) pour des performances améliorées\n- `RELEASE` : (uniquement pour les mainteneurs) lorsque défini, le binaire résultant sera uploadé sur GitHub\n\n## Extensions\n\nAvec la glibc ou les binaires basés sur macOS, vous pouvez charger des extensions PHP dynamiquement. Cependant, ces extensions devront être compilées avec le support ZTS.\nComme la plupart des gestionnaires de paquets ne proposent pas de versions ZTS de leurs extensions, vous devrez les compiler vous-même.\n\nPour cela, vous pouvez construire et exécuter le conteneur Docker `static-builder-gnu`, vous y connecter à distance et compiler les extensions avec `./configure --with-php-config=/go/src/app/dist/static-php-cli/buildroot/bin/php-config`.\n\nExemple d'étapes pour [l'extension Xdebug](https://xdebug.org) :\n\n```console\ndocker build -t gnu-ext -f static-builder-gnu.Dockerfile --build-arg FRANKENPHP_VERSION=1.0 .\ndocker create --name static-builder-gnu -it gnu-ext /bin/sh\ndocker start static-builder-gnu\ndocker exec -it static-builder-gnu /bin/sh\ncd /go/src/app/dist/static-php-cli/buildroot/bin\ngit clone https://github.com/xdebug/xdebug.git && cd xdebug\nsource scl_source enable devtoolset-10\n../phpize\n./configure --with-php-config=/go/src/app/dist/static-php-cli/buildroot/bin/php-config\nmake\nexit\ndocker cp static-builder-gnu:/go/src/app/dist/static-php-cli/buildroot/bin/xdebug/modules/xdebug.so xdebug-zts.so\ndocker cp static-builder-gnu:/go/src/app/dist/frankenphp-linux-$(uname -m) ./frankenphp\ndocker stop static-builder-gnu\ndocker rm static-builder-gnu\ndocker rmi gnu-ext\n```\n\nCela aura créé `frankenphp` et `xdebug-zts.so` dans le répertoire courant.\nSi vous déplacez `xdebug-zts.so` dans votre répertoire d'extension, ajoutez `zend_extension=xdebug-zts.so` à votre php.ini\net lancez FrankenPHP, il chargera Xdebug.\n"
  },
  {
    "path": "docs/fr/worker.md",
    "content": "# Utilisation des workers FrankenPHP\n\nDémarrez votre application une fois et gardez-la en mémoire.\nFrankenPHP traitera les requêtes entrantes en quelques millisecondes.\n\n## Démarrage des scripts workers\n\n### Docker\n\nDéfinissez la valeur de la variable d'environnement `FRANKENPHP_CONFIG` à `worker /path/to/your/worker/script.php` :\n\n```console\ndocker run \\\n    -e FRANKENPHP_CONFIG=\"worker /app/path/to/your/worker/script.php\" \\\n    -v $PWD:/app \\\n    -p 80:80 -p 443:443 -p 443:443/udp \\\n    dunglas/frankenphp\n```\n\n### Binaire autonome\n\nUtilisez l'option `--worker` de la commande `php-server` pour servir le contenu du répertoire courant en utilisant un worker :\n\n```console\nfrankenphp php-server --worker /path/to/your/worker/script.php\n```\n\nSi votre application PHP est [intégrée dans le binaire](embed.md), vous pouvez ajouter un `Caddyfile` personnalisé dans le répertoire racine de l'application.\nIl sera utilisé automatiquement.\n\nIl est également possible de [redémarrer le worker en cas de changement de fichier](config.md#watching-for-file-changes) avec l'option `--watch`.\nLa commande suivante déclenchera un redémarrage si un fichier se terminant par `.php` dans le répertoire `/path/to/your/app/` ou ses sous-répertoires est modifié :\n\n```console\nfrankenphp php-server --worker /path/to/your/worker/script.php --watch=\"/path/to/your/app/**/*.php\"\n```\n\nCette fonctionnalité se combine très bien avec le [rechargement à chaud](hot-reload.md).\n\n## Runtime Symfony\n\n> [!TIP]\n> La section suivante est nécessaire uniquement avant Symfony 7.4, où le support natif du mode worker de FrankenPHP a été introduit.\n\nLe mode worker de FrankenPHP est pris en charge par le [Composant Runtime de Symfony](https://symfony.com/doc/current/components/runtime.html).\nPour démarrer une application Symfony dans un worker, installez le package FrankenPHP de [PHP Runtime](https://github.com/php-runtime/runtime) :\n\n```console\ncomposer require runtime/frankenphp-symfony\n```\n\nDémarrez votre serveur d'application en définissant la variable d'environnement `APP_RUNTIME` pour utiliser le Runtime Symfony de FrankenPHP :\n\n```console\ndocker run \\\n    -e FRANKENPHP_CONFIG=\"worker ./public/index.php\" \\\n    -e APP_RUNTIME=Runtime\\\\FrankenPhpSymfony\\\\Runtime \\\n    -v $PWD:/app \\\n    -p 80:80 -p 443:443 -p 443:443/udp \\\n    dunglas/frankenphp\n```\n\n## Laravel Octane\n\nVoir [la documentation dédiée](laravel.md#laravel-octane).\n\n## Applications Personnalisées\n\nL'exemple suivant montre comment créer votre propre script worker sans dépendre d'une bibliothèque tierce :\n\n```php\n<?php\n// public/index.php\n\n// Démarrer votre application\nrequire __DIR__.'/vendor/autoload.php';\n\n$myApp = new \\App\\Kernel();\n$myApp->boot();\n\n// Déclarer le handler en dehors de la boucle pour de meilleures performances (moins de travail effectué)\n$handler = static function () use ($myApp) {\n    try {\n        // Appelé lorsqu'une requête est reçue,\n        // les superglobales, php://input, etc., sont réinitialisés\n        echo $myApp->handle($_GET, $_POST, $_COOKIE, $_FILES, $_SERVER);\n    } catch (\\Throwable $exception) {\n        // `set_exception_handler` est appelé uniquement lorsque le script worker se termine,\n        // ce qui peut ne pas être ce que vous attendez, alors interceptez et gérez les exceptions ici\n        (new \\MyCustomExceptionHandler)->handleException($exception);\n    }\n};\n\n$maxRequests = (int)($_SERVER['MAX_REQUESTS'] ?? 0);\nfor ($nbRequests = 0; !$maxRequests || $nbRequests < $maxRequests; ++$nbRequests) {\n    $keepRunning = \\frankenphp_handle_request($handler);\n\n    // Faire quelque chose après l'envoi de la réponse HTTP\n    $myApp->terminate();\n\n    // Exécuter le ramasse-miettes pour réduire les chances qu'il soit déclenché au milieu de la génération d'une page\n    gc_collect_cycles();\n\n    if (!$keepRunning) break;\n}\n\n// Nettoyage\n$myApp->shutdown();\n```\n\nEnsuite, démarrez votre application et utilisez la variable d'environnement `FRANKENPHP_CONFIG` pour configurer votre worker :\n\n```console\ndocker run \\\n    -e FRANKENPHP_CONFIG=\"worker ./public/index.php\" \\\n    -v $PWD:/app \\\n    -p 80:80 -p 443:443 -p 443:443/udp \\\n    dunglas/frankenphp\n```\n\nPar défaut, 2 workers par CPU sont démarrés.\nVous pouvez également configurer le nombre de workers à démarrer :\n\n```console\ndocker run \\\n    -e FRANKENPHP_CONFIG=\"worker ./public/index.php 42\" \\\n    -v $PWD:/app \\\n    -p 80:80 -p 443:443 -p 443:443/udp \\\n    dunglas/frankenphp\n```\n\n### Redémarrer le worker après un certain nombre de requêtes\n\nComme PHP n'a pas été initialement conçu pour des processus de longue durée, de nombreuses bibliothèques et codes anciens présentent encore des fuites de mémoire.\nUne solution pour utiliser ce type de code en mode worker est de redémarrer le script worker après avoir traité un certain nombre de requêtes :\n\nLe code du worker précédent permet de configurer un nombre maximal de requêtes à traiter en définissant une variable d'environnement nommée `MAX_REQUESTS`.\n\n### Redémarrer les workers manuellement\n\nBien qu'il soit possible de redémarrer les workers [en cas de changement de fichier](config.md#watching-for-file-changes),\nil est également possible de redémarrer tous les workers de manière élégante via l'[API Admin de Caddy](https://caddyserver.com/docs/api).\nSi l'administration est activée dans votre [Caddyfile](config.md#caddyfile-config), vous pouvez envoyer un ping\nà l'endpoint de redémarrage avec une simple requête POST comme celle-ci :\n\n```console\ncurl -X POST http://localhost:2019/frankenphp/workers/restart\n```\n\n### Échecs des workers\n\nSi un script de worker se plante avec un code de sortie non nul, FrankenPHP le redémarre avec une stratégie de backoff exponentielle.\nSi le script worker reste en place plus longtemps que le dernier backoff \\* 2, FrankenPHP ne pénalisera pas le script et le redémarrera à nouveau.\nToutefois, si le script de worker continue d'échouer avec un code de sortie non nul dans un court laps de temps\n(par exemple, une faute de frappe dans un script), FrankenPHP plantera avec l'erreur : `too many consecutive failures` (trop d'échecs consécutifs).\n\nLe nombre d'échecs consécutifs peut être configuré dans votre [Caddyfile](config.md#caddyfile-config) avec l'option `max_consecutive_failures` :\n\n```caddyfile\nfrankenphp {\n    worker {\n        # ...\n        max_consecutive_failures 10\n    }\n}\n```\n\n## Comportement des superglobales\n\n[Les superglobales PHP](https://www.php.net/manual/fr/language.variables.superglobals.php) (`$_SERVER`, `$_ENV`, `$_GET`...)\nse comportent comme suit :\n\n- avant le premier appel à `frankenphp_handle_request()`, les superglobales contiennent des valeurs liées au script worker lui-même\n- pendant et après l'appel à `frankenphp_handle_request()`, les superglobales contiennent des valeurs générées à partir de la requête HTTP traitée, chaque appel à `frankenphp_handle_request()` change les valeurs des superglobales\n\nPour accéder aux superglobales du script worker à l'intérieur de la fonction de rappel, vous devez les copier et importer la copie dans le scope de la fonction :\n\n```php\n<?php\n// Copier la superglobale $_SERVER du worker avant le premier appel à frankenphp_handle_request()\n$workerServer = $_SERVER;\n\n$handler = static function () use ($workerServer) {\n    var_dump($_SERVER); // $_SERVER lié à la requête\n    var_dump($workerServer); // $_SERVER du script worker\n};\n\n// ...\n"
  },
  {
    "path": "docs/fr/x-sendfile.md",
    "content": "# Servir efficacement les gros fichiers statiques (`X-Sendfile`/`X-Accel-Redirect`)\n\nHabituellement, les fichiers statiques peuvent être servis directement par le serveur web,\nmais parfois, il est nécessaire d'exécuter du code PHP avant de les envoyer :\ncontrôle d'accès, statistiques, en-têtes HTTP personnalisés...\n\nMalheureusement, utiliser PHP pour servir de gros fichiers statiques est inefficace comparé à\nl'utilisation directe du serveur web (surcharge mémoire, diminution des performances...).\n\nFrankenPHP permet de déléguer l'envoi des fichiers statiques au serveur web\n**après** avoir exécuté du code PHP personnalisé.\n\nPour ce faire, votre application PHP n'a qu'à définir un en-tête HTTP personnalisé\ncontenant le chemin du fichier à servir. FrankenPHP se chargera du reste.\n\nCette fonctionnalité est connue sous le nom de **`X-Sendfile`** pour Apache, et **`X-Accel-Redirect`** pour NGINX.\n\nDans les exemples suivants, nous supposons que le \"document root\" du projet est le répertoire `public/`\net que nous voulons utiliser PHP pour servir des fichiers stockés en dehors du dossier `public/`,\ndepuis un répertoire nommé `private-files/`.\n\n## Configuration\n\nTout d'abord, ajoutez la configuration suivante à votre `Caddyfile` pour activer cette fonctionnalité :\n\n```patch\n\troot public/\n\t# ...\n\n+\t# Needed for Symfony, Laravel and other projects using the Symfony HttpFoundation component\n+\trequest_header X-Sendfile-Type x-accel-redirect\n+\trequest_header X-Accel-Mapping ../private-files=/private-files\n+\n+\tintercept {\n+\t\t@accel header X-Accel-Redirect *\n+\t\thandle_response @accel {\n+\t\t\troot private-files/\n+\t\t\trewrite * {resp.header.X-Accel-Redirect}\n+\t\t\tmethod * GET\n+\n+\t\t\t# Remove the X-Accel-Redirect header set by PHP for increased security\n+\t\t\theader -X-Accel-Redirect\n+\n+\t\t\tfile_server\n+\t\t}\n+\t}\n\n\tphp_server\n```\n\n## PHP simple\n\nDéfinissez le chemin relatif du fichier (à partir de `private-files/`) comme valeur de l'en-tête `X-Accel-Redirect` :\n\n```php\nheader('X-Accel-Redirect: file.txt') ;\n```\n\n## Projets utilisant le composant Symfony HttpFoundation (Symfony, Laravel, Drupal...)\n\nSymfony HttpFoundation [supporte nativement cette fonctionnalité](https://symfony.com/doc/current/components/http_foundation.html#serving-files).\nIl va automatiquement déterminer la bonne valeur pour l'en-tête `X-Accel-Redirect` et l'ajoutera à la réponse.\n\n```php\nuse Symfony\\Component\\HttpFoundation\\BinaryFileResponse;\n\nBinaryFileResponse::trustXSendfileTypeHeader();\n$response = new BinaryFileResponse(__DIR__.'/../private-files/file.txt');\n\n// ...\n```\n"
  },
  {
    "path": "docs/github-actions.md",
    "content": "# Using GitHub Actions\n\nThis repository builds and deploys the Docker image to [Docker Hub](https://hub.docker.com/r/dunglas/frankenphp) on\nevery approved pull request or on your own fork once setup.\n\n## Setting up GitHub Actions\n\nIn the repository settings, under secrets, add the following secrets:\n\n- `REGISTRY_LOGIN_SERVER`: The Docker registry to use (e.g. `docker.io`).\n- `REGISTRY_USERNAME`: The username to use to log in to the registry (e.g. `dunglas`).\n- `REGISTRY_PASSWORD`: The password to use to log in to the registry (e.g. an access key).\n- `IMAGE_NAME`: The name of the image (e.g. `dunglas/frankenphp`).\n\n## Building and Pushing the Image\n\n1. Create a Pull Request or push to your fork.\n2. GitHub Actions will build the image and run any tests.\n3. If the build is successful, the image will be pushed to the registry using the `pr-x`, where `x` is the PR number, as the tag.\n\n## Deploying the Image\n\n1. Once the Pull Request is merged, GitHub Actions will again run the tests and build a new image.\n2. If the build is successful, the `main` tag will be updated in the Docker registry.\n\n## Releases\n\n1. Create a new tag in the repository.\n2. GitHub Actions will build the image and run any tests.\n3. If the build is successful, the image will be pushed to the registry using the tag name as the tag (e.g. `v1.2.3` and `v1.2` will be created).\n4. The `latest` tag will also be updated.\n"
  },
  {
    "path": "docs/hot-reload.md",
    "content": "# Hot Reload\n\nFrankenPHP includes a built-in **hot reload** feature designed to vastly improve the developer experience.\n\n![Hot Reload](hot-reload.png)\n\nThis feature provides a workflow similar to **Hot Module Replacement (HMR)** in modern JavaScript tooling such as Vite or webpack.\nInstead of manually refreshing the browser after every file change (PHP code, templates, JavaScript, and CSS files...),\nFrankenPHP updates the page content in real-time.\n\nHot Reload natively works with WordPress, Laravel, Symfony, and any other PHP application or framework.\n\nWhen enabled, FrankenPHP watches your current working directory for filesystem changes.\nWhen a file is modified, it pushes a [Mercure](mercure.md) update to the browser.\n\nDepending on your setup, the browser will either:\n\n- **Morph the DOM** (preserving scroll position and input state) if [Idiomorph](https://github.com/bigskysoftware/idiomorph) is loaded.\n- **Reload the page** (standard live reload) if Idiomorph is not present.\n\n## Configuration\n\nTo enable hot reloading, enable Mercure, then add the `hot_reload` sub-directive to the `php_server` directive in your `Caddyfile`.\n\n> [!WARNING]\n>\n> This feature is intended for **development environments only**.\n> Do not enable `hot_reload` in production, as this feature is not secure (exposes sensitive internal details) and slows down the application.\n>\n```caddyfile\nlocalhost\n\nmercure {\n    anonymous\n}\n\nroot public/\nphp_server {\n    hot_reload\n}\n```\n\nBy default, FrankenPHP will watch all files in the current working directory matching this glob pattern: `./**/*.{css,env,gif,htm,html,jpg,jpeg,js,mjs,php,png,svg,twig,webp,xml,yaml,yml}`\n\nIt's possible to set the files to watch using the glob syntax explicitly:\n\n```caddyfile\nlocalhost\n\nmercure {\n    anonymous\n}\n\nroot public/\nphp_server {\n    hot_reload src/**/*{.php,.js} config/**/*.yaml\n}\n```\n\nUse the long form of `hot_reload` to specify the Mercure topic to use, as well as which directories or files to watch:\n\n```caddyfile\nlocalhost\n\nmercure {\n    anonymous\n}\n\nroot public/\nphp_server {\n    hot_reload {\n        topic hot-reload-topic\n        watch src/**/*.php\n        watch assets/**/*.{ts,json}\n        watch templates/\n        watch public/css/\n    }\n}\n```\n\n## Client-Side Integration\n\nWhile the server detects changes, the browser needs to subscribe to these events to update the page.\nFrankenPHP exposes the Mercure Hub URL to use for subscribing to file changes via the `$_SERVER['FRANKENPHP_HOT_RELOAD']` environment variable.\n\nA convenience JavaScript library, [frankenphp-hot-reload](https://www.npmjs.com/package/frankenphp-hot-reload), is also available to handle the client-side logic.\nTo use it, add the following to your main layout:\n\n```php\n<!DOCTYPE html>\n<title>FrankenPHP Hot Reload</title>\n<?php if (isset($_SERVER['FRANKENPHP_HOT_RELOAD'])): ?>\n<meta name=\"frankenphp-hot-reload:url\" content=\"<?=$_SERVER['FRANKENPHP_HOT_RELOAD']?>\">\n<script src=\"https://cdn.jsdelivr.net/npm/idiomorph\"></script>\n<script src=\"https://cdn.jsdelivr.net/npm/frankenphp-hot-reload/+esm\" type=\"module\"></script>\n<?php endif ?>\n```\n\nThe library will automatically subscribe to the Mercure hub, fetch the current URL in the background when a file change is detected, and morph the DOM.\nIt is available as an [npm](https://www.npmjs.com/package/frankenphp-hot-reload) package and on [GitHub](https://github.com/dunglas/frankenphp-hot-reload).\n\nAlternatively, you can implement your own client-side logic by subscribing directly to the Mercure hub using the `EventSource` native JavaScript class.\n\n### Preserving Existing DOM Nodes\n\nIn rare cases, such as when using development tools [like the Symfony web debug toolbar](https://github.com/symfony/symfony/pull/62970),\nyou may want to preserve specific DOM nodes.\nTo do so, add the `data-frankenphp-hot-reload-preserve` attribute to the relevant HTML element:\n\n```html\n<div data-frankenphp-hot-reload-preserve><!-- My debug bar --></div>\n```\n\n## Worker Mode\n\nIf you are running your application in [Worker Mode](https://frankenphp.dev/docs/worker/), your application script remains in memory.\nThis means changes to your PHP code will not be reflected immediately, even if the browser reloads.\n\nFor the best developer experience, you should combine `hot_reload` with [the `watch` sub-directive in the `worker` directive](config.md#watching-for-file-changes).\n\n- `hot_reload`: refreshes the **browser** when files change\n- `worker.watch`: restarts the worker when files change\n\n```caddy\nlocalhost\n\nmercure {\n    anonymous\n}\n\nroot public/\nphp_server {\n    hot_reload\n    worker {\n        file /path/to/my_worker.php\n        watch\n    }\n}\n```\n\n## How It Works\n\n1. **Watch**: FrankenPHP monitors the filesystem for modifications using [the `e-dant/watcher` library](https://github.com/e-dant/watcher) under the hood (we contributed the Go binding).\n2. **Restart (Worker Mode)**: if `watch` is enabled in the worker config, the PHP worker is restarted to load the new code.\n3. **Push**: a JSON payload containing the list of changed files is sent to the built-in [Mercure hub](https://mercure.rocks).\n4. **Receive**: The browser, listening via the JavaScript library, receives the Mercure event.\n5. **Update**:\n\n- If **Idiomorph** is detected, it fetches the updated content and morphs the current HTML to match the new state, applying changes instantly without losing state.\n- Otherwise, `window.location.reload()` is called to refresh the page.\n"
  },
  {
    "path": "docs/ja/CONTRIBUTING.md",
    "content": "# コントリビューション\n\n## PHPのコンパイル\n\n### Dockerを使用する場合（Linux）\n\n開発用Dockerイメージをビルドします：\n\n```console\ndocker build -t frankenphp-dev -f dev.Dockerfile .\ndocker run --cap-add=SYS_PTRACE --security-opt seccomp=unconfined -p 8080:8080 -p 443:443 -p 443:443/udp -v $PWD:/go/src/app -it frankenphp-dev\n```\n\nこのイメージには通常の開発ツール（Go、GDB、Valgrind、Neovimなど）が含まれており、PHP設定ファイルは以下の場所に配置されます。\n\n- php.ini: `/etc/frankenphp/php.ini` 開発用のプリセットが適用されたphp.iniファイルがデフォルトで提供されます。\n- 追加の設定ファイル: `/etc/frankenphp/php.d/*.ini`\n- PHP拡張モジュール: `/usr/lib/frankenphp/modules/`\n\nお使いのDockerのバージョンが23.0未満の場合、dockerignore[パターンの問題](https://github.com/moby/moby/pull/42676)によりビルドに失敗する可能性があります。以下のように`.dockerignore`にディレクトリを追加してください。\n\n```patch\n !testdata/*.php\n !testdata/*.txt\n+!caddy\n+!internal\n```\n\n### Dockerを使用しない場合（LinuxおよびmacOS）\n\n[ソースからのコンパイル手順](https://frankenphp.dev/docs/compile/)に従い、`--debug`設定フラグを渡してください。\n\n## テストスイートの実行\n\n```console\ngo test -race -v ./...\n```\n\n## Caddyモジュール\n\nFrankenPHPのCaddyモジュール付きでCaddyをビルドします：\n\n```console\ncd caddy/frankenphp/\ngo build -tags nobadger,nomysql,nopgx\ncd ../../\n```\n\nFrankenPHPのCaddyモジュール付きでCaddyを実行します：\n\n```console\ncd testdata/\n../caddy/frankenphp/frankenphp run\n```\n\nサーバーは`127.0.0.1:80`で待ち受けています：\n\n> [!NOTE]\n> Dockerを使用している場合は、コンテナのポート80をバインドするか、コンテナ内で実行する必要があります。\n\n```console\ncurl -vk http://127.0.0.1/phpinfo.php\n```\n\n## 最小構成のテストサーバー\n\n最小構成のテストサーバーをビルドします：\n\n```console\ncd internal/testserver/\ngo build\ncd ../../\n```\n\nテストサーバーを実行します：\n\n```console\ncd testdata/\n../internal/testserver/testserver\n```\n\nサーバーは`127.0.0.1:8080`で待ち受けています：\n\n```console\ncurl -v http://127.0.0.1:8080/phpinfo.php\n```\n\n## Dockerイメージをローカルでビルドする\n\nbakeプランを出力します：\n\n```console\ndocker buildx bake -f docker-bake.hcl --print\n```\n\namd64用のFrankenPHPイメージをローカルでビルドします：\n\n```console\ndocker buildx bake -f docker-bake.hcl --pull --load --set \"*.platform=linux/amd64\"\n```\n\narm64用のFrankenPHPイメージをローカルでビルドします：\n\n```console\ndocker buildx bake -f docker-bake.hcl --pull --load --set \"*.platform=linux/arm64\"\n```\n\narm64とamd64用のFrankenPHPイメージをスクラッチからビルドしてDocker Hubにプッシュします：\n\n```console\ndocker buildx bake -f docker-bake.hcl --pull --no-cache --push\n```\n\n## 静的ビルドでのセグメンテーション違反のデバッグ\n\n1. GitHubからFrankenPHPバイナリのデバッグ版をダウンロードするか、デバッグシンボルを含む独自の静的ビルドを作成します：\n\n   ```console\n   docker buildx bake \\\n       --load \\\n       --set static-builder.args.DEBUG_SYMBOLS=1 \\\n       --set \"static-builder.platform=linux/amd64\" \\\n       static-builder\n   docker cp $(docker create --name static-builder-musl dunglas/frankenphp:static-builder-musl):/go/src/app/dist/frankenphp-linux-$(uname -m) frankenphp\n   ```\n\n2. 現在使用している`frankenphp`を、デバッグ版のFrankenPHP実行ファイルに置き換えます\n3. 通常通りFrankenPHPを起動します（あるいは、GDBで直接FrankenPHPを開始することもできます：`gdb --args frankenphp run`）\n4. GDBでプロセスにアタッチします：\n\n   ```console\n   gdb -p `pidof frankenphp`\n   ```\n\n5. 必要に応じて、GDBシェルで`continue`と入力します\n6. FrankenPHPをクラッシュさせます\n7. GDBシェルで`bt`と入力します\n8. 出力結果をコピーします\n\n## GitHub Actionsでのセグメンテーション違反のデバッグ\n\n1. `.github/workflows/tests.yml`を開きます\n2. PHPデバッグシンボルを有効にします\n\n   ```patch\n       - uses: shivammathur/setup-php@v2\n         # ...\n         env:\n           phpts: ts\n   +       debug: true\n   ```\n\n3. `tmate`を有効にしてコンテナに接続できるようにします\n\n   ```patch\n       - name: Set CGO flags\n         run: echo \"CGO_CFLAGS=$(php-config --includes)\" >> \"$GITHUB_ENV\"\n   +   - run: |\n   +       sudo apt install gdb\n   +       mkdir -p /home/runner/.config/gdb/\n   +       printf \"set auto-load safe-path /\\nhandle SIG34 nostop noprint pass\" > /home/runner/.config/gdb/gdbinit\n   +   - uses: mxschmitt/action-tmate@v3\n   ```\n\n4. コンテナに接続します\n5. `frankenphp.go`を開きます\n6. `cgosymbolizer`を有効にします\n\n   ```patch\n   -\t//_ \"github.com/ianlancetaylor/cgosymbolizer\"\n   +\t_ \"github.com/ianlancetaylor/cgosymbolizer\"\n   ```\n\n7. モジュールを取得します：`go get`\n8. コンテナ内で、GDBなどを使用できます：\n\n   ```console\n   go test -c -ldflags=-w\n   gdb --args frankenphp.test -test.run ^MyTest$\n   ```\n\n9. バグが修正されたら、これらの変更をすべて元に戻します\n\n## その他の開発リソース\n\n- [uWSGIでのPHP埋め込み](https://github.com/unbit/uwsgi/blob/master/plugins/php/php_plugin.c)\n- [NGINX UnitでのPHP埋め込み](https://github.com/nginx/unit/blob/master/src/nxt_php_sapi.c)\n- [Go言語でのPHP埋め込み (go-php)](https://github.com/deuill/go-php)\n- [Go言語でのPHP埋め込み (GoEmPHP)](https://github.com/mikespook/goemphp)\n- [C++でのPHP埋め込み](https://gist.github.com/paresy/3cbd4c6a469511ac7479aa0e7c42fea7)\n- [Sara Golemon 著『Extending and Embedding PHP』](https://books.google.fr/books?id=zMbGvK17_tYC&pg=PA254&lpg=PA254#v=onepage&q&f=false)\n- [TSRMLS_CCとは何か？](http://blog.golemon.com/2006/06/what-heck-is-tsrmlscc-anyway.html)\n- [SDL バインディング](https://pkg.go.dev/github.com/veandco/go-sdl2@v0.4.21/sdl#Main)\n\n## Docker関連リソース\n\n- [Bakeファイル定義](https://docs.docker.com/build/customize/bake/file-definition/)\n- [`docker buildx build`](https://docs.docker.com/engine/reference/commandline/buildx_build/)\n\n## 便利なコマンド\n\n```console\napk add strace util-linux gdb\nstrace -e 'trace=!futex,epoll_ctl,epoll_pwait,tgkill,rt_sigreturn' -p 1\n```\n\n## ドキュメントの翻訳\n\n新しい言語でドキュメントとサイトを翻訳するには、\n以下の手順で行ってください。\n\n1. このリポジトリの`docs/`ディレクトリに、言語の2文字ISOコードを名前にした新しいディレクトリを作成します\n2. `docs/`ディレクトリのルートにある全ての`.md`ファイルを新しいディレクトリにコピーします（翻訳のソースとして常に英語版を使用してください。英語版が最新版だからです）\n3. ルートディレクトリから`README.md`と`CONTRIBUTING.md`ファイルを新しいディレクトリにコピーします\n4. ファイルの内容を翻訳しますが、ファイル名は変更せず、`> [!`で始まる文字列も翻訳しないでください（これはGitHub用の特別なマークアップです）\n5. 翻訳でプルリクエストを作成します\n6. [サイトリポジトリ](https://github.com/dunglas/frankenphp-website/tree/main)で、`content/`、`data/`、`i18n/`ディレクトリの翻訳ファイルをコピーして翻訳します\n7. 作成されたYAMLファイルの値を翻訳します\n8. サイトリポジトリでプルリクエストを開きます\n"
  },
  {
    "path": "docs/ja/README.md",
    "content": "# FrankenPHP: PHPのためのモダンなアプリケーションサーバー\n\n<h1 align=\"center\"><a href=\"https://frankenphp.dev\"><img src=\"frankenphp.png\" alt=\"FrankenPHP\" width=\"600\"></a></h1>\n\nFrankenPHPは、[Caddy](https://caddyserver.com/) Webサーバーをベースに構築された、PHPのためのモダンなアプリケーションサーバーです。\n\nFrankenPHPは、[_Early Hints_](https://frankenphp.dev/docs/early-hints/)、[ワーカーモード](https://frankenphp.dev/docs/worker/)、[リアルタイム機能](https://frankenphp.dev/docs/mercure/)、自動HTTPS、HTTP/2、HTTP/3などの驚異的な機能により、あなたのPHPアプリに強力な力を与えます。\n\nFrankenPHPはあらゆるPHPアプリと連携し、ワーカーモードの公式統合によってLaravelやSymfonyプロジェクトをこれまで以上に高速化します。\n\nまた、FrankenPHPはスタンドアロンのGoライブラリとしても利用可能で、`net/http`を使って任意のアプリにPHPを埋め込むことができます。\n\n[**詳しくは** _frankenphp.dev_](https://frankenphp.dev)と、このスライド資料もご参照ください：\n\n<a href=\"https://dunglas.dev/2022/10/frankenphp-the-modern-php-app-server-written-in-go/\"><img src=\"https://dunglas.dev/wp-content/uploads/2022/10/frankenphp.png\" alt=\"Slides\" width=\"600\"></a>\n\n## はじめに\n\nWindowsをお使いの場合は、[WSL](https://learn.microsoft.com/windows/wsl/)を使用してFrankenPHPを実行してください。\n\n### インストールスクリプト\n\n以下のコマンドをターミナルに貼り付けると、環境に合ったバージョンが自動的にインストールされます：\n\n```console\ncurl https://frankenphp.dev/install.sh | sh\n```\n\n### スタンドアロンバイナリ\n\nLinuxとmacOS向けに、開発用途の静的FrankenPHPバイナリを提供しています。\n[PHP 8.4](https://www.php.net/releases/8.4/en.php)と主要なPHP拡張が含まれます。\n\n[FrankenPHPをダウンロード](https://github.com/php/frankenphp/releases)\n\n**拡張のインストール：** よく使われる拡張は同梱されています。追加の拡張をインストールすることはできません。\n\n### rpm パッケージ\n\nメンテナーが `dnf` を使用するすべてのシステム向けに rpm パッケージを提供しています。インストール方法：\n\n```console\nsudo dnf install https://rpm.henderkes.com/static-php-1-0.noarch.rpm\nsudo dnf module enable php-zts:static-8.4 # 8.2-8.5 利用可能\nsudo dnf install frankenphp\n```\n\n**拡張のインストール：** `sudo dnf install php-zts-<extension>`\n\nデフォルトで提供されていない拡張については [PIE](https://github.com/php/pie) を使用してください：\n\n```console\nsudo dnf install pie-zts\nsudo pie-zts install asgrim/example-pie-extension\n```\n\n### deb パッケージ\n\nメンテナーが `apt` を使用するすべてのシステム向けに deb パッケージを提供しています。インストール方法：\n\n```console\nsudo curl -fsSL https://key.henderkes.com/static-php.gpg -o /usr/share/keyrings/static-php.gpg && \\\necho \"deb [signed-by=/usr/share/keyrings/static-php.gpg] https://deb.henderkes.com/ stable main\" | sudo tee /etc/apt/sources.list.d/static-php.list && \\\nsudo apt update\nsudo apt install frankenphp\n```\n\n**拡張のインストール：** `sudo apt install php-zts-<extension>`\n\nデフォルトで提供されていない拡張については [PIE](https://github.com/php/pie) を使用してください：\n\n```console\nsudo apt install pie-zts\nsudo pie-zts install asgrim/example-pie-extension\n```\n\n### Docker\n\nまた、[Dockerイメージ](https://frankenphp.dev/docs/docker/)も利用可能です：\n\n```console\ndocker run -v .:/app/public \\\n    -p 80:80 -p 443:443 -p 443:443/udp \\\n    dunglas/frankenphp\n```\n\nブラウザで`https://localhost`にアクセスして、FrankenPHPをお楽しみください！\n\n> [!TIP]\n>\n> `https://127.0.0.1`ではなく、`https://localhost`を使用して、自己署名証明書を受け入れてください。\n> 使用するドメインを変更したい場合は、[`SERVER_NAME` 環境変数](docs/config.md#environment-variables)を設定してください。\n\n### Homebrew\n\nFrankenPHPはmacOSおよびLinux向けに[Homebrew](https://brew.sh)パッケージとしても利用可能です。\n\nインストール方法：\n\n```console\nbrew install dunglas/frankenphp/frankenphp\n```\n\n**拡張のインストール：** [PIE](https://github.com/php/pie) を使用してください。\n\n### 使い方\n\n現在のディレクトリのコンテンツを配信するには、以下を実行してください：\n\n```console\nfrankenphp php-server\n```\n\nコマンドラインスクリプトも実行できます：\n\n```console\nfrankenphp php-cli /path/to/your/script.php\n```\n\ndeb / rpm パッケージの場合は、systemd サービスを起動することもできます：\n\n```console\nsudo systemctl start frankenphp\n```\n\n## ドキュメント\n\n- [クラシックモード](https://frankenphp.dev/docs/classic/)\n- [ワーカーモード](https://frankenphp.dev/docs/worker/)\n- [Early Hintsサポート（103 HTTPステータスコード）](https://frankenphp.dev/docs/early-hints/)\n- [リアルタイム](https://frankenphp.dev/docs/mercure/)\n- [大きな静的ファイルの効率的な提供](https://frankenphp.dev/docs/x-sendfile/)\n- [設定](https://frankenphp.dev/docs/config/)\n- [Dockerイメージ](https://frankenphp.dev/docs/docker/)\n- [本番環境でのデプロイ](https://frankenphp.dev/docs/production/)\n- [パフォーマンス最適化](https://frankenphp.dev/docs/performance/)\n- [**スタンドアロン**、自己実行可能なPHPアプリの作成](https://frankenphp.dev/docs/embed/)\n- [静的バイナリの作成](https://frankenphp.dev/docs/static/)\n- [ソースからのコンパイル](https://frankenphp.dev/docs/compile/)\n- [FrankenPHPの監視](https://frankenphp.dev/docs/metrics/)\n- [Laravel統合](https://frankenphp.dev/docs/laravel/)\n- [既知の問題](https://frankenphp.dev/docs/known-issues/)\n- [デモアプリ（Symfony）とベンチマーク](https://github.com/dunglas/frankenphp-demo)\n- [Goライブラリドキュメント](https://pkg.go.dev/github.com/dunglas/frankenphp)\n- [コントリビューションとデバッグ](https://frankenphp.dev/docs/contributing/)\n\n## 例とスケルトン\n\n- [Symfony](https://github.com/dunglas/symfony-docker)\n- [API Platform](https://api-platform.com/docs/symfony)\n- [Laravel](https://frankenphp.dev/docs/laravel/)\n- [Sulu](https://sulu.io/blog/running-sulu-with-frankenphp)\n- [WordPress](https://github.com/StephenMiracle/frankenwp)\n- [Drupal](https://github.com/dunglas/frankenphp-drupal)\n- [Joomla](https://github.com/alexandreelise/frankenphp-joomla)\n- [TYPO3](https://github.com/ochorocho/franken-typo3)\n- [Magento2](https://github.com/ekino/frankenphp-magento2)\n"
  },
  {
    "path": "docs/ja/classic.md",
    "content": "# クラシックモードの使用\n\n追加の設定を行わなくても、FrankenPHPはクラシックモードで動作します。このモードでは、FrankenPHPは従来のPHPサーバーのように機能し、PHPファイルを直接提供します。これにより、PHP-FPMやmod_phpを使ったApacheの置き換えとしてシームレスに利用できます。\n\nCaddyと同様に、FrankenPHPは無制限の接続を受け付け、[固定数のスレッド](config.md#caddyfile-config)でそれらを処理します。受け入れられキューに入れられる接続の数は、利用可能なシステムリソースによってのみ制限されます。\nPHPスレッドプールは、起動時に初期化された固定数のスレッドで動作し、これはPHP-FPMの静的モードに相当します。また、PHP-FPMの動的モードと同様に、[実行時にスレッドを自動的にスケール](performance.md#max_threads)させることも可能です。\n\nキューに入った接続は、PHPスレッドが空くまで無期限に待機します。これを避けるために、FrankenPHP のグローバル設定内の `max_wait_time` [設定](config.md#caddyfile-config)を使って、リクエストが空きスレッドを待てる最大時間を制限し、それを超えるとリクエストが拒否されるようにできます。\n加えて、[Caddy側で適切な書き込みタイムアウト](https://caddyserver.com/docs/caddyfile/options#timeouts)を設定することも可能です。\n\n各Caddyインスタンスは、1つのFrankenPHPスレッドプールのみを起動し、すべての`php_server`ブロック間でこのプールを共有します。\n"
  },
  {
    "path": "docs/ja/compile.md",
    "content": "# ソースからのコンパイル\n\nこのドキュメントでは、PHPを動的ライブラリとしてロードするFrankenPHPバイナリの作成方法を説明します。\nこれが推奨される方法です。\n\nまたは、[完全静的およびほぼ静的なビルド](static.md)も作成できます。\n\n## PHPのインストール\n\nFrankenPHPはPHP 8.2以上と互換性があります。\n\n### Homebrewを使用する場合（LinuxとMac）\n\nFrankenPHPと互換性のあるlibphpのバージョンをインストールする最も簡単な方法は、[Homebrew PHP](https://github.com/shivammathur/homebrew-php)が提供するZTSパッケージを使用することです。\n\nまず、まだインストールしていない場合は[Homebrew](https://brew.sh)をインストールしてください。\n\n次に、PHPのZTSバリアント、Brotli（オプション、圧縮サポート用）、watcher（オプション、ファイル変更検出用）をインストールします：\n\n```console\nbrew install shivammathur/php/php-zts brotli watcher\nbrew link --overwrite --force shivammathur/php/php-zts\n```\n\n### PHPをコンパイルする場合\n\n別の方法として、FrankenPHPに必要なオプションを指定してPHPをソースからコンパイルすることもできます。\n\nまず、[PHPのソース](https://www.php.net/downloads.php)を取得して展開します：\n\n```console\ntar xf php-*\ncd php-*/\n```\n\n次に、プラットフォームに応じて必要なオプションを指定して`configure`スクリプトを実行します。\n以下の`./configure`フラグは必須ですが、例えば拡張機能モジュールや追加機能をコンパイルするために他のフラグを追加することもできます。\n\n#### Linux\n\n```console\n./configure \\\n    --enable-embed \\\n    --enable-zts \\\n    --disable-zend-signals \\\n    --enable-zend-max-execution-timers\n```\n\n#### Mac\n\n[Homebrew](https://brew.sh/)パッケージマネージャーを使用して、必須およびオプションの依存関係をインストールします：\n\n```console\nbrew install libiconv bison brotli re2c pkg-config watcher\necho 'export PATH=\"/opt/homebrew/opt/bison/bin:$PATH\"' >> ~/.zshrc\n```\n\nその後、以下のようにconfigureスクリプトを実行します：\n\n```console\n./configure \\\n    --enable-embed \\\n    --enable-zts \\\n    --disable-zend-signals \\\n    --with-iconv=/opt/homebrew/opt/libiconv/\n```\n\n#### PHPのコンパイル\n\n最後に、PHPをコンパイルしてインストールします：\n\n```console\nmake -j\"$(getconf _NPROCESSORS_ONLN)\"\nsudo make install\n```\n\n## オプション依存関係のインストール\n\nFrankenPHPの一部の機能は、システムにインストールされているオプションの依存パッケージに依存しています。\nまたは、Goコンパイラにビルドタグを渡すことで、これらの機能を無効にできます。\n\n| 機能                           | 依存関係                                                              | 無効にするためのビルドタグ |\n| ------------------------------ | --------------------------------------------------------------------- | -------------------------- |\n| Brotli圧縮                     | [Brotli](https://github.com/google/brotli)                            | nobrotli                   |\n| ファイル変更時のワーカー再起動 | [Watcher C](https://github.com/e-dant/watcher/tree/release/watcher-c) | nowatcher                  |\n\n## Goアプリのコンパイル\n\nいよいよ最終的なバイナリをビルドできるようになりました。\n\n### xcaddyを使う場合\n\n推奨される方法は、[xcaddy](https://github.com/caddyserver/xcaddy)を使用してFrankenPHPをコンパイルする方法です。\n`xcaddy`を使うと、[Caddyのカスタムモジュール](https://caddyserver.com/docs/modules/)やFrankenPHP拡張を簡単に追加できます：\n\n```console\nCGO_ENABLED=1 \\\nXCADDY_GO_BUILD_FLAGS=\"-ldflags='-w -s' -tags=nobadger,nomysql,nopgx\" \\\nCGO_CFLAGS=$(php-config --includes) \\\nCGO_LDFLAGS=\"$(php-config --ldflags) $(php-config --libs)\" \\\nxcaddy build \\\n    --output frankenphp \\\n    --with github.com/dunglas/frankenphp/caddy \\\n    --with github.com/dunglas/mercure/caddy \\\n    --with github.com/dunglas/vulcain/caddy\n    # 追加のCaddyモジュールとFrankenPHP拡張をここに追加\n```\n\n> [!TIP]\n>\n> musl libc（Alpine Linuxのデフォルト）とSymfonyを使用している場合、\n> デフォルトのスタックサイズを増やす必要がある場合があります。\n> そうしないと、`PHP Fatal error: Maximum call stack size of 83360 bytes reached during compilation. Try splitting expression`のようなエラーが発生する可能性があります。\n>\n> これを行うには、`XCADDY_GO_BUILD_FLAGS`環境変数を\n> `XCADDY_GO_BUILD_FLAGS=$'-ldflags \"-w -s -extldflags \\'-Wl,-z,stack-size=0x80000\\'\"'`のようなものに変更してください\n> （アプリの要件に応じてスタックサイズの値を変更してください）。\n\n### xcaddyを使用しない場合\n\n代替として、`xcaddy`を使わずに`go`コマンドを直接使ってFrankenPHPをコンパイルすることも可能です：\n\n```console\ncurl -L https://github.com/php/frankenphp/archive/refs/heads/main.tar.gz | tar xz\ncd frankenphp-main/caddy/frankenphp\nCGO_CFLAGS=$(php-config --includes) CGO_LDFLAGS=\"$(php-config --ldflags) $(php-config --libs)\" go build -tags=nobadger,nomysql,nopgx\n```\n"
  },
  {
    "path": "docs/ja/config.md",
    "content": "# 設定\n\nFrankenPHP、Caddy、そして[Mercure](mercure.md)や[Vulcain](https://vulcain.rocks)モジュールは、[Caddyでサポートされる形式](https://caddyserver.com/docs/getting-started#your-first-config)を使用して設定できます。\n\n最も一般的な形式は`Caddyfile`で、シンプルで人間が読めるテキスト形式です。\nデフォルトでは、FrankenPHPは現在のディレクトリにある`Caddyfile`を探します。\n`-c`または`--config`オプションでカスタムパスを指定できます。\n\nPHPアプリケーションを配信するための最小限の`Caddyfile`を以下に示します：\n\n```caddyfile\n# レスポンスするホスト名\nlocalhost\n\n# オプションで、ファイルを配信するディレクトリ。指定しない場合は現在のディレクトリがデフォルト\n#root public/\nphp_server\n```\n\nより多くの機能を有効にし、便利な環境変数を提供するより高度な`Caddyfile`は、[FrankenPHPリポジトリ](https://github.com/php/frankenphp/blob/main/caddy/frankenphp/Caddyfile)およびDockerイメージに同梱されています。\n\nPHP自体は、[`php.ini` ファイルを使用](https://www.php.net/manual/en/configuration.file.php)して設定できます。\n\nインストール方法に応じて、FrankenPHPとPHPインタープリターは以下の場所に記載された設定ファイルを探します。\n\n## Docker\n\nFrankenPHP:\n\n- `/etc/frankenphp/Caddyfile`: メインの設定ファイル\n- `/etc/frankenphp/Caddyfile.d/*.caddyfile`: 自動的にロードされる追加の設定ファイル\n\nPHP:\n\n- `php.ini`: `/usr/local/etc/php/php.ini`（デフォルトでは`php.ini`は含まれていません）\n- 追加の設定ファイル: `/usr/local/etc/php/conf.d/*.ini`\n- PHP拡張モジュール: `/usr/local/lib/php/extensions/no-debug-zts-<YYYYMMDD>/`\n- PHPプロジェクトが提供する公式テンプレートをコピーすることを推奨します：\n\n```dockerfile\nFROM dunglas/frankenphp\n\n# Production:\nRUN cp $PHP_INI_DIR/php.ini-production $PHP_INI_DIR/php.ini\n\n# Or development:\nRUN cp $PHP_INI_DIR/php.ini-development $PHP_INI_DIR/php.ini\n```\n\n## RPMおよびDebianパッケージ\n\nFrankenPHP:\n\n- `/etc/frankenphp/Caddyfile`: メインの設定ファイル\n- `/etc/frankenphp/Caddyfile.d/*.caddyfile`: 自動的にロードされる追加の設定ファイル\n\nPHP:\n\n- `php.ini`: `/etc/php-zts/php.ini`（本番環境向けのプリセットの`php.ini`ファイルがデフォルトで提供されます）\n- 追加の設定ファイル: `/etc/php-zts/conf.d/*.ini`\n\n## 静的バイナリ\n\nFrankenPHP:\n\n- 現在の作業ディレクトリ: `Caddyfile`\n\nPHP:\n\n- `php.ini`: `frankenphp run`または`frankenphp php-server`を実行したディレクトリ内、なければ`/etc/frankenphp/php.ini`を参照\n- 追加の設定ファイル: `/etc/frankenphp/php.d/*.ini`\n- PHP拡張モジュール: ロードできません、バイナリ自体にバンドルする必要があります\n- [PHPソース](https://github.com/php/php-src/)で提供される`php.ini-production`または`php.ini-development`のいずれかをコピーしてください。\n\n## Caddyfileの設定\n\n`php_server`または`php`の[HTTPディレクティブ](https://caddyserver.com/docs/caddyfile/concepts#directives)は、サイトブロック内で使用してPHPアプリを配信できます。\n\n最小構成の例：\n\n```caddyfile\nlocalhost {\n\t# 圧縮を有効化（オプション）\n\tencode zstd br gzip\n\t# 現在のディレクトリ内のPHPファイルを実行し、アセットを配信\n\tphp_server\n}\n```\n\nFrankenPHPは、`frankenphp`の[グローバルオプション](https://caddyserver.com/docs/caddyfile/concepts#global-options)を使用して明示的に設定することもできます：\n\n```caddyfile\n{\n\tfrankenphp {\n\t\tnum_threads <num_threads> # 開始するPHPスレッド数を設定します。デフォルト: 利用可能なCPU数の2倍。\n\t\tmax_threads <num_threads> # 実行時に追加で開始できるPHPスレッドの最大数を制限します。デフォルト: num_threads。 'auto'を設定可能。\n\t\tmax_wait_time <duration> # リクエストがタイムアウトする前にPHPのスレッドが空くのを待つ最大時間を設定します。デフォルト: 無効。\n\t\tmax_idle_time <duration> # 自動スケーリングされたスレッドが非アクティブ化されるまでにアイドル状態である最大時間を設定します。デフォルト: 5秒。\n\t\tphp_ini <key> <value> # php.iniのディレクティブを設定します。複数のディレクティブを設定するために何度でも使用できます。\n\t\tworker {\n\t\t\tfile <path> # ワーカースクリプトのパスを設定します。\n\t\t\tnum <num> # 開始するPHPスレッド数を設定します。デフォルト: 利用可能なCPU数の2倍。\n\t\t\tenv <key> <value> # 追加の環境変数を指定された値に設定する。複数の環境変数に対して複数回指定することができます。\n\t\t\twatch <path> # ファイル変更を監視するパスを設定します。複数のパスに対して複数回指定できます。\n\t\t\tname <name> # ワーカーの名前を設定します。ログとメトリクスで使用されます。デフォルト: ワーカーファイルの絶対パス\n\t\t\tmax_consecutive_failures <num> # workerが不健全とみなされるまでの、連続失敗の最大回数を設定します。 -1 はワーカーを常に再起動することを意味します。デフォルトは 6 です。\n\t\t}\n\t}\n}\n\n# ...\n```\n\n代わりに、`worker`オプションのワンライナー形式を使用することもできます：\n\n```caddyfile\n{\n\tfrankenphp {\n\t\tworker <file> <num>\n\t}\n}\n\n# ...\n```\n\n同じサーバーで複数のアプリを提供する場合は、複数のワーカーを定義することもできます：\n\n```caddyfile\napp.example.com {\n    root /path/to/app/public\n\tphp_server {\n\t\troot /path/to/app/public # キャッシュ効率を高める\n\t\tworker index.php <num>\n\t}\n}\n\nother.example.com {\n    root /path/to/other/public\n\tphp_server {\n\t\troot /path/to/other/public\n\t\tworker index.php <num>\n\t}\n}\n\n# ...\n```\n\n通常は`php_server`ディレクティブを使えば十分ですが、\nより細かい制御が必要な場合は、より低レベルの`php`ディレクティブを使用できます。\n`php`ディレクティブは、対象がPHPファイルかどうかを確認せず、すべての入力をPHPに渡します。\n詳しくは[パフォーマンスページ](performance.md#try_files)をお読みください。\n\n`php_server`ディレクティブの使用は、以下の設定と同等です：\n\n```caddyfile\nroute {\n\t# ディレクトリへのリクエストに末尾スラッシュを追加\n\t@canonicalPath {\n\t\tfile {path}/index.php\n\t\tnot path */\n\t}\n\tredir @canonicalPath {path}/ 308\n\t# 要求されたファイルが存在しない場合は、indexファイルを試行\n\t@indexFiles file {\n\t\ttry_files {path} {path}/index.php index.php\n\t\tsplit_path .php\n\t}\n\trewrite @indexFiles {http.matchers.file.relative}\n\t# FrankenPHP!\n\t@phpFiles path *.php\n\tphp @phpFiles\n\tfile_server\n}\n```\n\n`php_server`と`php`ディレクティブには以下のオプションがあります：\n\n```caddyfile\nphp_server [<matcher>] {\n\troot <directory> # サイトのルートフォルダを設定します。デフォルト: `root`ディレクティブ。\n\tsplit_path <delim...> # URIを2つの部分に分割するための部分文字列を設定します。最初にマッチする部分文字列がURIから「パス情報」を分割するために使用されます。最初の部分はマッチする部分文字列で接尾辞が付けられ、実際のリソース（CGIスクリプト）名とみなされます。2番目の部分はスクリプトが使用する PATH_INFO に設定されます。デフォルト: `.php`\n\tresolve_root_symlink false # シンボリックリンクが存在する場合`root`ディレクトリをシンボリックリンクの評価によって実際の値に解決することを無効にする（デフォルトで有効）。\n\tenv <key> <value> # 追加の環境変数を指定された値に設定する。複数の環境変数を指定する場合は、複数回指定することができます。\n\tfile_server off # 組み込みのfile_serverディレクティブを無効にします。\n\tworker { # このサーバー固有のワーカーを作成します。複数のワーカーに対して複数回指定できます。\n\t\tfile <path> # ワーカースクリプトへのパスを設定します。php_serverのルートからの相対パスとなります。\n\t\tnum <num> # 起動するPHPスレッド数を設定します。デフォルトは利用可能なCPU数の2倍です。\n\t\tname <name> # ログとメトリクスで使用されるワーカーの名前を設定します。デフォルト: ワーカーファイルの絶対パス。php_server ブロックで定義されている場合は、常にm#で始まります。\n\t\twatch <path> # ファイルの変更を監視するパスを設定する。複数のパスに対して複数回指定することができる。\n\t\tenv <key> <value> # 追加の環境変数を指定された値に設定する。複数の環境変数を指定する場合は、複数回指定することができます。このワーカーの環境変数もphp_serverの親から継承されますが、 ここで上書きすることもできます。\n\t\tmatch <path> # ワーカーをパスパターンにマッチさせます。try_filesを上書きし、php_serverディレクティブでのみ使用できます。\n\t}\n\tworker <other_file> <num> # グローバルfrankenphpブロックのような短縮形式も使用できます。\n}\n```\n\n### ファイルの変更監視\n\nワーカーはアプリケーションを一度だけ起動してメモリに保持するため、\nPHPファイルに変更を加えても即座には反映されません。\n\n代わりに、`watch`ディレクティブを使用してファイル変更時にワーカーを再起動させることができます。\nこれは開発環境において有用です。\n\n```caddyfile\n{\n\tfrankenphp {\n\t\tworker {\n\t\t\tfile  /path/to/app/public/worker.php\n\t\t\twatch\n\t\t}\n\t}\n}\n```\n\nこの機能は、[ホットリロード](hot-reload.md)と組み合わせてよく使用されます。\n\n`watch`ディレクトリが指定されていない場合、`./**/*.{env,php,twig,yaml,yml}`にフォールバックします。\nこれは、FrankenPHPプロセスが開始されたディレクトリおよびそのサブディレクトリ内のすべての`.env`、`.php`、`.twig`、`.yaml`、`.yml`ファイルすべてを監視します。\n代わりに、[シェルのファイル名パターン](https://pkg.go.dev/path/filepath#Match)を使用して\n1つ以上のディレクトリを指定することもできます：\n\n```caddyfile\n{\n\tfrankenphp {\n\t\tworker {\n\t\t\tfile  /path/to/app/public/worker.php\n\t\t\twatch /path/to/app # /path/to/app以下すべてのサブディレクトリのファイルを監視\n\t\t\twatch /path/to/app/*.php # /path/to/app内の.phpで終わるファイルを監視\n\t\t\twatch /path/to/app/**/*.php # /path/to/appおよびサブディレクトリのPHPファイルを監視\n\t\t\twatch /path/to/app/**/*.{php,twig} # /path/to/appおよびサブディレクトリ内のPHPとTwigファイルを監視\n\t\t}\n\t}\n}\n```\n\n- `**` パターンは再帰的な監視を意味します\n- ディレクトリは相対パス（FrankenPHPプロセスの開始ディレクトリから）にもできます\n- 複数のワーカーが定義されている場合、いずれかのファイルが変更されるとすべてのワーカーが再起動されます\n- 実行時に生成されるファイル（ログなど）を監視対象に含めると、意図しないワーカーの再起動を引き起こす可能性があるため注意が必要です\n\nファイルウォッチャーは[e-dant/watcher](https://github.com/e-dant/watcher)に基づいています。\n\n## パスにワーカーをマッチさせる\n\n従来のPHPアプリケーションでは、スクリプトは常にpublicディレクトリに配置されます。\nこれはワーカースクリプトにも当てはまり、他のPHPスクリプトと同様に扱われます。\nワーカースクリプトをpublicディレクトリの外に配置したい場合は、`match`ディレクティブを使用して実現できます。\n\n`match`ディレクティブは、`try_files`の最適化された代替手段であり、`php_server`および`php`の中でのみ使用できます。\n次の例では、public ディレクトリ内にファイルが存在すればそれを配信し、\n存在しなければ、パスパターンに一致するワーカーにリクエストを転送します。\n\n```caddyfile\n{\n\tfrankenphp {\n\t\tphp_server {\n\t\t\tworker {\n\t\t\t\tfile /path/to/worker.php # ファイルはpublicパス外でも可\n\t\t\t\tmatch /api/* # /api/で始まるすべてのリクエストはこのワーカーで処理される\n\t\t\t}\n\t\t}\n\t}\n}\n```\n\n## 環境変数\n\n以下の環境変数を使用することで、`Caddyfile`を直接変更せずにCaddyディレクティブを注入できます：\n\n- `SERVER_NAME`: [待ち受けアドレス](https://caddyserver.com/docs/caddyfile/concepts#addresses)を変更し、指定したホスト名はTLS証明書の生成にも使用されます\n- `SERVER_ROOT`: サイトのルートディレクトリを変更します。デフォルトは`public/`\n- `CADDY_GLOBAL_OPTIONS`: [グローバルオプション](https://caddyserver.com/docs/caddyfile/options)を注入します\n- `FRANKENPHP_CONFIG`: `frankenphp`ディレクティブの下に設定を注入します\n\nFPM や CLI SAPI と同様に、環境変数はデフォルトで`$_SERVER`スーパーグローバルで公開されます。\n\n[`variables_order` PHPディレクティブ](https://www.php.net/manual/en/ini.core.php#ini.variables-order)の`S`値は、このディレクティブ内での`E`の位置にかかわらず常に`ES`と同等です。\n\n## PHP設定\n\n[追加のPHP設定ファイル](https://www.php.net/manual/en/configuration.file.php#configuration.file.scan)を読み込むには、\n`PHP_INI_SCAN_DIR`環境変数を使用できます。\n設定されると、PHPは指定されたディレクトリに存在する`.ini`拡張子を持つすべてのファイルを読み込みます。\n\nまた、`Caddyfile`の`php_ini`ディレクティブを使用してPHP設定を変更することもできます：\n\n```caddyfile\n{\n    frankenphp {\n        php_ini memory_limit 256M\n\n        # or\n\n        php_ini {\n            memory_limit 256M\n            max_execution_time 15\n        }\n    }\n}\n```\n\n### HTTPSの無効化\n\nデフォルトでは、FrankenPHPは`localhost`を含むすべてのホスト名に対してHTTPSを自動的に有効にします。\nHTTPSを無効にしたい場合（例えば開発環境で）、`SERVER_NAME`環境変数を`http://`または`:80`に設定できます：\n\nまたは、[Caddyのドキュメント](https://caddyserver.com/docs/automatic-https#activation)に記載されている他のすべての方法を使用することもできます。\n\n`localhost`ホスト名の代わりに`127.0.0.1` IPアドレスでHTTPSを使用したい場合は、[既知の問題](known-issues.md#using-https127001-with-docker)セクションを読んでください。\n\n### フルデュプレックス（HTTP/1）\n\nHTTP/1.xを使用する場合、全体のボディが読み取られる前にレスポンスを書き込めるようにするため、\nフルデュプレックスモードを有効にすることが望ましい場合があります（例：[Mercure](mercure.md)、WebSocket、Server-Sent Eventsなど）。\n\nこれは明示的に有効化する必要がある設定で、`Caddyfile`のグローバルオプションに追加する必要があります：\n\n```caddyfile\n{\n  servers {\n    enable_full_duplex\n  }\n}\n```\n\n> [!CAUTION]\n>\n> このオプションを有効にすると、フルデュプレックスをサポートしない古いHTTP/1.xクライアントでデッドロックが発生する可能性があります。\n> これは`CADDY_GLOBAL_OPTIONS`環境設定を使用しても設定できます：\n\n```sh\nCADDY_GLOBAL_OPTIONS=\"servers {\n  enable_full_duplex\n}\"\n```\n\nこの設定の詳細については、[Caddyドキュメント](https://caddyserver.com/docs/caddyfile/options#enable-full-duplex)をご覧ください。\n\n## デバッグモードの有効化\n\nDockerイメージを使用する場合、`CADDY_GLOBAL_OPTIONS`環境変数に`debug`を設定するとデバッグモードが有効になります：\n\n```console\ndocker run -v $PWD:/app/public \\\n    -e CADDY_GLOBAL_OPTIONS=debug \\\n    -p 80:80 -p 443:443 -p 443:443/udp \\\n    dunglas/frankenphp\n```\n\n## Shell Completion\n\nFrankenPHPはBash、Zsh、Fish、およびPowerShell用のシェル補完機能を内蔵しています。これにより、すべてのコマンド（`php-server`、`php-cli`、`extension-init`などのカスタムコマンドを含む）とそのフラグのオートコンプリートが可能になります。\n\n### Bash\n\n現在のシェルセッションで補完を読み込むには：\n\n```console\nsource <(frankenphp completion bash)\n```\n\n新しいセッションごとに補完を読み込むには、以下を実行してください：\n\n**Linux:**\n\n```console\nfrankenphp completion bash > /usr/share/bash-completion/completions/frankenphp\n```\n\n**macOS:**\n\n```console\nfrankenphp completion bash > $(brew --prefix)/share/bash-completion/completions/frankenphp\n```\n\n### Zsh\n\nシェル補完がまだ環境で有効になっていない場合は、有効にする必要があります。以下のコマンドを一度実行してください：\n\n```console\necho \"autoload -U compinit; compinit\" >> ~/.zshrc\n```\n\n各セッションで補完を読み込むには、一度実行してください：\n\n```console\nfrankenphp completion zsh > \"${fpath[1]}/_frankenphp\"\n```\n\nこの設定を有効にするには、新しいシェルを起動する必要があります。\n\n### Fish\n\n現在のシェルセッションで補完を読み込むには：\n\n```console\nfrankenphp completion fish | source\n```\n\n新しいセッションごとに補完を読み込むには、一度実行してください：\n\n```console\nfrankenphp completion fish > ~/.config/fish/completions/frankenphp.fish\n```\n\n### PowerShell\n\n現在のシェルセッションで補完を読み込むには：\n\n```powershell\nfrankenphp completion powershell | Out-String | Invoke-Expression\n```\n\n新しいセッションごとに補完を読み込むには、一度実行してください：\n\n```powershell\nfrankenphp completion powershell | Out-File -FilePath (Join-Path (Split-Path $PROFILE) \"frankenphp.ps1\")\nAdd-Content -Path $PROFILE -Value '. (Join-Path (Split-Path $PROFILE) \"frankenphp.ps1\")'\n```\n\nこの設定を有効にするには、新しいシェルを起動する必要があります。\n\nこの設定を有効にするには、新しいシェルを起動する必要があります。\n"
  },
  {
    "path": "docs/ja/docker.md",
    "content": "# カスタムDockerイメージのビルド\n\n[FrankenPHPのDockerイメージ](https://hub.docker.com/r/dunglas/frankenphp)は、[公式PHPイメージ](https://hub.docker.com/_/php/)をベースにしています。主要なアーキテクチャに対してDebianとAlpine Linuxのバリアントを提供しており、Debianバリアントの使用を推奨しています。\n\nPHP 8.2、8.3、8.4、8.5向けのバリアントが提供されています。\n\nタグは次のパターンに従います：`dunglas/frankenphp:<frankenphp-version>-php<php-version>-<os>`\n\n- `<frankenphp-version>`および`<php-version>`は、それぞれFrankenPHPおよびPHPのバージョン番号で、メジャー（例：`1`）、マイナー（例：`1.2`）からパッチバージョン（例：`1.2.3`）まであります。\n- `<os>`は`trixie`（Debian Trixie用）、`bookworm`（Debian Bookworm用）、または`alpine`（Alpine最新安定版用）のいずれかです。\n\n[タグを閲覧](https://hub.docker.com/r/dunglas/frankenphp/tags)。\n\n## イメージの使用方法\n\nプロジェクトに`Dockerfile`を作成します：\n\n```dockerfile\nFROM dunglas/frankenphp\n\nCOPY . /app/public\n```\n\n次に、以下のコマンドを実行してDockerイメージをビルドし、実行します：\n\n```console\ndocker build -t my-php-app .\ndocker run -it --rm --name my-running-app my-php-app\n```\n\n## 設定を調整する方法\n\n利便性のため、役立つ環境変数を含む[デフォルトの`Caddyfile`](https://github.com/php/frankenphp/blob/main/caddy/frankenphp/Caddyfile)がイメージに含まれています。\n\n## PHP拡張モジュールの追加インストール方法\n\nベースイメージには[`docker-php-extension-installer`](https://github.com/mlocati/docker-php-extension-installer)スクリプトが含まれており、\n追加のPHP拡張モジュールを簡単にインストールできます：\n\n```dockerfile\nFROM dunglas/frankenphp\n\n# ここに追加の拡張モジュールを追加：\nRUN install-php-extensions \\\n\tpdo_mysql \\\n\tgd \\\n\tintl \\\n\tzip \\\n\topcache\n```\n\n## Caddyモジュールの追加インストール方法\n\nFrankenPHPはCaddyをベースに構築されているため、すべての[Caddyモジュール](https://caddyserver.com/docs/modules/)をFrankenPHPでも使用できます。\n\nカスタムCaddyモジュールをインストールする最も簡単な方法は、[xcaddy](https://github.com/caddyserver/xcaddy)を使用することです：\n\n```dockerfile\nFROM dunglas/frankenphp:builder AS builder\n\n# builderイメージにxcaddyをコピー\nCOPY --from=caddy:builder /usr/bin/xcaddy /usr/bin/xcaddy\n\n# FrankenPHPをビルドするにはCGOを有効にする必要があります\nRUN CGO_ENABLED=1 \\\n    XCADDY_SETCAP=1 \\\n    XCADDY_GO_BUILD_FLAGS=\"-ldflags='-w -s' -tags=nobadger,nomysql,nopgx\" \\\n    CGO_CFLAGS=$(php-config --includes) \\\n    CGO_LDFLAGS=\"$(php-config --ldflags) $(php-config --libs)\" \\\n    xcaddy build \\\n        --output /usr/local/bin/frankenphp \\\n        --with github.com/dunglas/frankenphp=./ \\\n        --with github.com/dunglas/frankenphp/caddy=./caddy/ \\\n        --with github.com/dunglas/caddy-cbrotli \\\n        # MercureとVulcainは公式ビルドに含まれていますが、お気軽に削除してください\n        --with github.com/dunglas/mercure/caddy \\\n        --with github.com/dunglas/vulcain/caddy\n        # ここに追加のCaddyモジュールを指定してください\n\nFROM dunglas/frankenphp AS runner\n\n# 公式バイナリをカスタムモジュールを含むものに置き換え\nCOPY --from=builder /usr/local/bin/frankenphp /usr/local/bin/frankenphp\n```\n\nFrankenPHPが提供する`builder`イメージには、コンパイル済みの`libphp`が含まれています。\n[ビルダーイメージ](https://hub.docker.com/r/dunglas/frankenphp/tags?name=builder)は、FrankenPHPおよびPHPのすべてのバージョンに対して、DebianとAlpineの両方が提供されています。\n\n> [!TIP]\n>\n> Alpine LinuxとSymfonyを使用している場合は、\n> [デフォルトのスタックサイズを増やす](compile.md#using-xcaddy) 必要がある場合があります。\n\n## デフォルトでワーカーモードを有効にする\n\nFrankenPHPをワーカースクリプトで起動するには、`FRANKENPHP_CONFIG`環境変数を設定します：\n\n```dockerfile\nFROM dunglas/frankenphp\n\n# ...\n\nENV FRANKENPHP_CONFIG=\"worker ./public/index.php\"\n```\n\n## 開発時にボリュームを使う\n\nFrankenPHPでの開発を簡単に行うには、ホスト側のアプリケーションのソースコードを含むディレクトリを、Dockerコンテナ内にボリュームとしてマウントします：\n\n```console\ndocker run -v $PWD:/app/public -p 80:80 -p 443:443 -p 443:443/udp --tty my-php-app\n```\n\n> [!TIP]\n>\n> `--tty`オプションを使うと、JSONではなく人間が読みやすいログが表示されます。\n\nDocker Composeを使用する場合：\n\n```yaml\n# compose.yaml\n\nservices:\n  php:\n    image: dunglas/frankenphp\n    # カスタムDockerfileを使用したい場合は以下の行のコメントを外してください\n    #build: .\n    # 本番環境で使用する場合は以下の行のコメントを外してください\n    # restart: always\n    ports:\n      - \"80:80\" # HTTP\n      - \"443:443\" # HTTPS\n      - \"443:443/udp\" # HTTP/3\n    volumes:\n      - ./:/app/public\n      - caddy_data:/data\n      - caddy_config:/config\n    # 開発環境で人間が読みやすいログを出力するため、本番ではこの行をコメントアウトしてください\n    tty: true\n\n# Caddyの証明書や設定に必要なボリューム\nvolumes:\n  caddy_data:\n  caddy_config:\n```\n\n## 非rootユーザーとして実行する\n\nFrankenPHPはDockerで非rootユーザーとして実行できます。\n\nこれを行うサンプル`Dockerfile`は以下の通りです：\n\n```dockerfile\nFROM dunglas/frankenphp\n\nARG USER=appuser\n\nRUN \\\n\t# Alpine系ディストリビューションでは \"adduser -D ${USER}\" を使用\n\tuseradd ${USER}; \\\n\t# ポート 80 や 443 にバインドするための追加ケーパビリティを追加\n\tsetcap CAP_NET_BIND_SERVICE=+eip /usr/local/bin/frankenphp; \\\n\t# /config/caddy および /data/caddy への書き込み権限を付与\n\tchown -R ${USER}:${USER} /config/caddy /data/caddy\n\nUSER ${USER}\n```\n\n### ケーパビリティなしでの実行\n\nFrankenPHPをroot以外のユーザーで実行する場合でも、特権ポート（80と443）でWebサーバーを\nバインドするために`CAP_NET_BIND_SERVICE`ケーパビリティが必要です。\n\nFrankenPHPを非特権ポート（1024以上）で公開する場合は、\nウェブサーバーを非rootユーザーとして実行し、ケーパビリティを必要とせずに実行することが可能です：\n\n```dockerfile\nFROM dunglas/frankenphp\n\nARG USER=appuser\n\nRUN \\\n\t# Alpine 系ディストリビューションでは \"adduser -D ${USER}\" を使用\n\tuseradd ${USER}; \\\n\t# デフォルトのケーパビリティを削除\n\tsetcap -r /usr/local/bin/frankenphp; \\\n\t# /config/caddy と /data/caddy への書き込み権限を付与\n\tchown -R ${USER}:${USER} /config/caddy /data/caddy\n\nUSER ${USER}\n```\n\nその後、`SERVER_NAME`環境変数を設定して非特権ポートを使用します。\n例： `:8000`\n\n## アップデート\n\nDockerイメージは以下のタイミングでビルドされます：\n\n- 新しいリリースがタグ付けされたとき\n- 公式PHPイメージに新しいバージョンがある場合、毎日UTC午前4時に自動ビルド\n\n## イメージの強化\n\nFrankenPHP Dockerイメージの攻撃対象領域とサイズをさらに削減するために、[Google distroless](https://github.com/GoogleContainerTools/distroless)または[Docker hardened](https://www.docker.com/products/hardened-images)イメージをベースにビルドすることも可能です。\n\n> [!WARNING]\n> これらの最小限のベースイメージにはシェルやパッケージマネージャーが含まれていないため、デバッグがより困難になります。そのため、セキュリティが最優先される本番環境でのみ推奨されます。\n\n追加のPHP拡張機能を追加する場合は、中間ビルドステージが必要になります：\n\n```dockerfile\nFROM dunglas/frankenphp AS builder\n\n# ここに追加のPHP拡張機能を追加\nRUN install-php-extensions pdo_mysql pdo_pgsql #...\n\n# frankenphpとインストールされているすべての拡張機能の共有ライブラリを一時的な場所にコピー\n# この手順は、frankenphpバイナリおよび各拡張機能の.soファイルのldd出力を分析することで手動で行うこともできます\nRUN apt-get update && apt-get install -y libtree && \\\n    EXT_DIR=\"$(php -r 'echo ini_get(\"extension_dir\");')\" && \\\n    FRANKENPHP_BIN=\"$(which frankenphp)\"; \\\n    LIBS_TMP_DIR=\"/tmp/libs\"; \\\n    mkdir -p \"$LIBS_TMP_DIR\"; \\\n    for target in \"$FRANKENPHP_BIN\" $(find \"$EXT_DIR\" -maxdepth 2 -type f -name \"*.so\"); do \\\n        libtree -pv \"$target\" | sed 's/.*── \\(.*\\) \\[.*/\\1/' | grep -v \"^$target\" | while IFS= read -r lib; do \\\n            [ -z \"$lib\" ] && continue; \\\n            base=$(basename \"$lib\"); \\\n            destfile=\"$LIBS_TMP_DIR/$base\"; \\\n            if [ ! -f \"$destfile\" ]; then \\\n                cp \"$lib\" \"$destfile\"; \\\n            fi; \\\n        done; \\\n    done\n\n\n# Distroless Debianベースイメージ。ベースイメージと同じDebianバージョンであることを確認してください\nFROM gcr.io/distroless/base-debian13\n# Docker hardened イメージの代替\n# FROM dhi.io/debian:13\n\n# コンテナにコピーするアプリケーションとCaddyfileの場所\nARG PATH_TO_APP=\".\"\nARG PATH_TO_CADDYFILE=\"./Caddyfile\"\n\n# アプリケーションを/appにコピー\n# さらに強化するために、書き込み可能なパスのみが非rootユーザーに所有されていることを確認してください\nCOPY --chown=nonroot:nonroot \"$PATH_TO_APP\" /app\nCOPY \"$PATH_TO_CADDYFILE\" /etc/caddy/Caddyfile\n\n# frankenphpと必要なライブラリをコピー\nCOPY --from=builder /usr/local/bin/frankenphp /usr/local/bin/frankenphp\nCOPY --from=builder /usr/local/lib/php/extensions /usr/local/lib/php/extensions\nCOPY --from=builder /tmp/libs /usr/lib\n\n# php.ini設定ファイルをコピー\nCOPY --from=builder /usr/local/etc/php/conf.d /usr/local/etc/php/conf.d\nCOPY --from=builder /usr/local/etc/php/php.ini-production /usr/local/etc/php/php.ini\n\n# Caddyデータディレクトリ — 読み取り専用のルートファイルシステム上でも、非rootユーザーが書き込み可能である必要があります\nENV XDG_CONFIG_HOME=/config \\\n    XDG_DATA_HOME=/data\nCOPY --from=builder --chown=nonroot:nonroot /data/caddy /data/caddy\nCOPY --from=builder --chown=nonroot:nonroot /config/caddy /config/caddy\n\nUSER nonroot\n\nWORKDIR /app\n\n# 指定されたCaddyfileでfrankenphpを実行するエントリポイント\nENTRYPOINT [\"/usr/local/bin/frankenphp\", \"run\", \"-c\", \"/etc/caddy/Caddyfile\"]\n```\n\n## 開発版\n\n開発版は[`dunglas/frankenphp-dev`](https://hub.docker.com/repository/docker/dunglas/frankenphp-dev)Dockerリポジトリで利用できます。\nGitHubリポジトリの`main`ブランチにコミットがpushされるたびに新しいビルドが実行されます。\n\n`latest*`タグは`main`ブランチのヘッドを指しており、`sha-<git-commit-hash>` 形式のタグも利用可能です。\n"
  },
  {
    "path": "docs/ja/early-hints.md",
    "content": "# Early Hints\n\nFrankenPHPは[103 Early Hints ステータスコード](https://developer.chrome.com/blog/early-hints/)をネイティブサポートしています。\nEarly Hintsを使用することで、ウェブページの読み込み時間を30%改善できます。\n\n```php\n<?php\n\nheader('Link: </style.css>; rel=preload; as=style');\nheaders_send(103);\n\n// 遅いアルゴリズムとSQLクエリ 🤪\n\necho <<<'HTML'\n<!DOCTYPE html>\n<title>Hello FrankenPHP</title>\n<link rel=\"stylesheet\" href=\"style.css\">\nHTML;\n```\n\nEarly Hintsは通常モードと[ワーカー](worker.md)モードの両方でサポートされています。\n"
  },
  {
    "path": "docs/ja/embed.md",
    "content": "# PHPアプリのスタンドアロンバイナリ化\n\nFrankenPHPには、PHPアプリケーションのソースコードやアセットを静的な自己完結型バイナリに埋め込む機能があります。\n\nこの機能により、PHPアプリケーション自体に加えて、PHPインタープリターや本番環境対応のWebサーバーCaddyも含んだスタンドアロンバイナリとして配布できます。\n\nこの機能について詳しくは、[SymfonyCon 2023でKévinが行ったプレゼンテーション](https://dunglas.dev/2023/12/php-and-symfony-apps-as-standalone-binaries/)をご覧ください。\n\nLaravelアプリケーションの埋め込みについては、[こちらの専用ドキュメント](laravel.md#laravel-apps-as-standalone-binaries)をお読みください。\n\n## アプリの準備\n\n自己完結型バイナリを作成する前に、アプリが埋め込みに対応できる状態にあることを確認してください。\n\n例えば、以下のような作業が必要です：\n\n- 本番環境用の依存パッケージをインストールする\n- オートローダーをダンプする\n- アプリケーションの本番モードを有効にする（ある場合）\n- 最終バイナリのサイズを減らすために`.git`やテストなどの不要なファイルを除外する\n\n例えば、Symfonyアプリの場合、以下のコマンドを使用できます：\n\n```console\n# .git/ などを除去するためにプロジェクトをエクスポート\nmkdir $TMPDIR/my-prepared-app\ngit archive HEAD | tar -x -C $TMPDIR/my-prepared-app\ncd $TMPDIR/my-prepared-app\n\n# 適切な環境変数を設定\necho APP_ENV=prod > .env.local\necho APP_DEBUG=0 >> .env.local\n\n# テストやその他不要ファイルを削除して容量削減\n# あるいは、 .gitattributes の export-ignore 属性にこれらを追加してもよい\nrm -Rf tests/\n\n# 依存パッケージをインストール\ncomposer install --ignore-platform-reqs --no-dev -a\n\n# .env を最適化\ncomposer dump-env prod\n```\n\n### 設定のカスタマイズ\n\n[設定](config.md) をカスタマイズするには、埋め込まれるアプリのメインディレクトリ\n（前の例では`$TMPDIR/my-prepared-app`）に`Caddyfile`と`php.ini`ファイルを配置できます。\n\n## Linux用バイナリの作成\n\nLinux用バイナリを作成する最も簡単な方法は、提供されているDockerベースのビルダーを使用することです。\n\n1. アプリのリポジトリに`static-build.Dockerfile`というファイルを作成します：\n\n   ```dockerfile\n   FROM --platform=linux/amd64 dunglas/frankenphp:static-builder-gnu\n   # バイナリをmusl-libcシステムで実行する場合は、static-builder-musl を使用してください\n\n   # アプリをコピー\n   WORKDIR /go/src/app/dist/app\n   COPY . .\n\n   # 静的バイナリをビルド\n   WORKDIR /go/src/app/\n   RUN EMBED=dist/app/ ./build-static.sh\n   ```\n\n   > [!CAUTION]\n   >\n   > 一部の`.dockerignore`ファイル（例：デフォルトの[Symfony Docker `.dockerignore`](https://github.com/dunglas/symfony-docker/blob/main/.dockerignore)）\n   > は`vendor/`ディレクトリと`.env`ファイルを無視します。ビルド前に`.dockerignore`ファイルを調整または削除してください。\n\n2. ビルドします：\n\n   ```console\n   docker build -t static-app -f static-build.Dockerfile .\n   ```\n\n3. バイナリを抽出します：\n\n   ```console\n   docker cp $(docker create --name static-app-tmp static-app):/go/src/app/dist/frankenphp-linux-x86_64 my-app ; docker rm static-app-tmp\n   ```\n\n生成されるバイナリは、現在のディレクトリの`my-app`というファイル名になります。\n\n## 他のOS用のバイナリの作成\n\nDockerを使用したくない場合や、macOSバイナリを作成したい場合は、提供されているシェルスクリプトを使用してください：\n\n```console\ngit clone https://github.com/php/frankenphp\ncd frankenphp\nEMBED=/path/to/your/app ./build-static.sh\n```\n\n生成されるバイナリは、`dist/`ディレクトリの`frankenphp-<os>-<arch>`という名前のファイルです。\n\n## バイナリの使い方\n\nこれで完了です！`my-app`ファイル（または他のOSでは`dist/frankenphp-<os>-<arch>`）には、自己完結型アプリが含まれています！\n\nWebアプリを起動するには、以下を実行します：\n\n```console\n./my-app php-server\n```\n\nアプリに[ワーカースクリプト](worker.md)が含まれている場合は、以下のようにワーカーを開始します：\n\n```console\n./my-app php-server --worker public/index.php\n```\n\nHTTPS（Let's Encrypt証明書は自動作成）、HTTP/2、HTTP/3を有効にするには、使用するドメイン名を指定してください：\n\n```console\n./my-app php-server --domain localhost\n```\n\nバイナリに埋め込まれたPHP CLIスクリプトも実行できます：\n\n```console\n./my-app php-cli bin/console\n```\n\n## PHP拡張モジュール\n\nデフォルトでは、スクリプトはプロジェクトの`composer.json`ファイルで必要な拡張モジュールをビルドします（存在する場合）。\n`composer.json`ファイルが存在しない場合、[静的ビルドのドキュメント](static.md)に記載されているデフォルトの拡張モジュールがビルドされます。\n\n拡張モジュールをカスタマイズしたい場合は、`PHP_EXTENSIONS`環境変数を使用してください。\n\n## ビルドのカスタマイズ\n\nバイナリをカスタマイズする方法（拡張モジュール、PHPバージョンなど）については、[静的ビルドのドキュメント](static.md)をお読みください。\n\n## バイナリの配布\n\nLinuxでは、作成されたバイナリは[UPX](https://upx.github.io)を使用して圧縮されます。\n\nMacでは、送信前にファイルサイズを減らすために圧縮できます。\n`xz`の使用をお勧めします。\n"
  },
  {
    "path": "docs/ja/extension-workers.md",
    "content": "# 拡張ワーカー\n\n拡張ワーカーは、[FrankenPHP拡張機能](https://frankenphp.dev/docs/extensions/)がバックグラウンドタスクの実行、非同期イベントの処理、またはカスタムプロトコルの実装のために、PHPスレッドの専用プールを管理できるようにします。キューシステム、イベントリスナー、スケジューラーなどに役立ちます。\n\n## ワーカーの登録\n\n### 静的登録\n\nワーカーをユーザーが構成可能にする必要がない場合（固定スクリプトパス、固定スレッド数）、`init()` 関数でワーカーを登録するだけです。\n\n```go\npackage myextension\n\nimport (\n\t\"github.com/dunglas/frankenphp\"\n\t\"github.com/dunglas/frankenphp/caddy\"\n)\n\n// ワーカープールと通信するためのグローバルハンドル\nvar worker frankenphp.Workers\n\nfunc init() {\n\t// モジュールがロードされたときにワーカーを登録します。\n\tworker = caddy.RegisterWorkers(\n\t\t\"my-internal-worker\", // ユニークな名前\n\t\t\"worker.php\",         // スクリプトパス（実行場所からの相対パス、または絶対パス）\n\t\t2,                    // 固定スレッド数\n\t\t// オプションのライフサイクルフック\n\t\tfrankenphp.WithWorkerOnServerStartup(func() {\n\t\t\t// グローバルなセットアップロジック...\n\t\t}),\n\t)\n}\n```\n\n### Caddyモジュール内 (ユーザーが構成可能)\n\n拡張機能を共有する予定がある場合（一般的なキューやイベントリスナーなど）、Caddyモジュールにラップする必要があります。これにより、ユーザーは `Caddyfile` を介してスクリプトパスとスレッド数を構成できます（[例を見る](https://github.com/dunglas/frankenphp-queue/blob/989120d394d66dd6c8e2101cac73dd622fade334/caddy.go)）。\n\n### 純粋なGoアプリケーション内 (組み込み)\n\n[Caddyなしで標準GoアプリケーションにFrankenPHPを組み込む](https://pkg.go.dev/github.com/dunglas/frankenphp#example-ServeHTTP)場合、初期化オプションで `frankenphp.WithExtensionWorkers` を使用して拡張ワーカーを登録できます。\n\n## ワーカーとの対話\n\nワーカープールがアクティブになったら、タスクをディスパッチできます。これは、[PHPにエクスポートされたネイティブ関数](https://frankenphp.dev/docs/extensions/#writing-the-extension)内、またはGoのロジック（cronスケジューラー、イベントリスナー（MQTT、Kafka）、その他のゴルーチンなど）から実行できます。\n\n### ヘッドレスモード: `SendMessage`\n\n`SendMessage` を使用して、生データをワーカーのスクリプトに直接渡します。これはキューや単純なコマンドに最適です。\n\n#### 例: 非同期キュー拡張機能\n\n```go\n// #include <Zend/zend_types.h>\nimport \"C\"\nimport (\n\t\"context\"\n\t\"unsafe\"\n\t\"github.com/dunglas/frankenphp\"\n)\n\n//export_php:function my_queue_push(mixed $data): bool\nfunc my_queue_push(data *C.zval) bool {\n\t// 1. ワーカーが準備できていることを確認する\n\tif worker == nil {\n\t\treturn false\n\t}\n\n\t// 2. バックグラウンドワーカーにディスパッチする\n\t_, err := worker.SendMessage(\n\t\tcontext.Background(), // 標準のGoコンテキスト\n\t\tunsafe.Pointer(data), // ワーカーに渡すデータ\n\t\tnil, // オプションのhttp.ResponseWriter\n\t)\n\n\treturn err == nil\n}\n```\n\n### HTTPエミュレーション: `SendRequest`\n\n拡張機能が標準のウェブ環境（`$_SERVER`、`$_GET` など）を期待するPHPスクリプトを呼び出す必要がある場合は、`SendRequest` を使用します。\n\n```go\n// #include <Zend/zend_types.h>\nimport \"C\"\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"unsafe\"\n\t\"github.com/dunglas/frankenphp\"\n)\n\n//export_php:function my_worker_http_request(string $path): string\nfunc my_worker_http_request(path *C.zend_string) unsafe.Pointer {\n\t// 1. リクエストとレコーダーを準備する\n\turl := frankenphp.GoString(unsafe.Pointer(path))\n\treq, _ := http.NewRequest(\"GET\", url, http.NoBody)\n\trr := httptest.NewRecorder()\n\n\t// 2. ワーカーにディスパッチする\n\tif err := worker.SendRequest(rr, req); err != nil {\n\t\treturn nil\n\t}\n\n\t// 3. キャプチャされたレスポンスを返す\n\treturn frankenphp.PHPString(rr.Body.String(), false)\n}\n```\n\n## ワーカーのスクリプト\n\nPHPワーカーのスクリプトはループで実行され、生メッセージとHTTPリクエストの両方を処理できます。\n\n```php\n<?php\n// 同じループで生のメッセージとHTTPリクエストの両方を処理する\n$handler = function ($payload = null) {\n    // ケース1: メッセージモード\n    if ($payload !== null) {\n        return \"Received payload: \" . $payload;\n    }\n\n    // ケース2: HTTPモード（標準のPHPスーパーグローバルが設定される）\n    echo \"Hello from page: \" . $_SERVER['REQUEST_URI'];\n};\n\nwhile (frankenphp_handle_request($handler)) {\n    gc_collect_cycles();\n}\n```\n\n## ライフサイクルフック\n\nFrankenPHPは、ライフサイクルの特定の時点でGoコードを実行するためのフックを提供します。\n\n| フックタイプ | オプション名                 | シグネチャ           | コンテキストと使用例                                                     |\n| :--------- | :-------------------------- | :------------------ | :--------------------------------------------------------------------- |\n| **サーバー** | `WithWorkerOnServerStartup` | `func()`            | グローバルなセットアップ。**一度だけ**実行されます。例: NATS/Redisへの接続。            |\n| **サーバー** | `WithWorkerOnServerShutdown` | `func()`            | グローバルなクリーンアップ。**一度だけ**実行されます。例: 共有接続のクローズ。       |\n| **スレッド** | `WithWorkerOnReady`         | `func(threadID int)` | スレッドごとのセットアップ。スレッドが開始したときに呼び出されます。スレッドIDを受け取ります。 |\n| **スレッド** | `WithWorkerOnShutdown`      | `func(threadID int)` | スレッドごとのクリーンアップ。スレッドIDを受け取ります。                            |\n\n### 例\n\n```go\npackage myextension\n\nimport (\n    \"fmt\"\n    \"github.com/dunglas/frankenphp\"\n    frankenphpCaddy \"github.com/dunglas/frankenphp/caddy\"\n)\n\nfunc init() {\n    workerHandle = frankenphpCaddy.RegisterWorkers(\n        \"my-worker\", \"worker.php\", 2,\n\n        // サーバー起動時 (グローバル)\n        frankenphp.WithWorkerOnServerStartup(func() {\n            fmt.Println(\"Extension: Server starting up...\")\n        }),\n\n        // スレッド準備完了時 (スレッドごと)\n        // 注: この関数はスレッドIDを表す整数を受け入れます\n        frankenphp.WithWorkerOnReady(func(id int) {\n            fmt.Printf(\"Extension: Worker thread #%d is ready.\\n\", id)\n        }),\n    )\n}\n```\n"
  },
  {
    "path": "docs/ja/extensions.md",
    "content": "# GoでPHP拡張モジュールを作成する\n\nFrankenPHPでは、**GoでPHP拡張モジュールを作成する**ことができます。これにより、PHPから直接呼び出せる**高パフォーマンスなネイティブ関数**を作成できます。アプリケーションは既存または新しいGoライブラリを活用でき、**PHPコードから直接goroutineの**強力な並行性モデルを使用できます。\n\nPHP拡張モジュールの記述は通常Cで行われますが、少しの追加作業で他の言語でも作成可能です。PHP拡張モジュールは低レベル言語の力を活用してPHPの機能を拡張することができます。例えば、ネイティブ関数を追加したり、特定の操作を最適化したりできます。\n\nCaddyモジュールのおかげで、GoでPHP拡張モジュールを書いてFrankenPHPに簡単に統合できます。\n\n## 2つのアプローチ\n\nFrankenPHPでは、GoでPHP拡張モジュールを作成する2つの方法を提供します：\n\n1. **拡張モジュールジェネレーターを使用** - ほとんどのユースケースに必要なボイラープレートを自動生成する推奨アプローチで、Goコードの記述に集中できます\n2. **手動実装** - 拡張モジュール構造を細かく制御したい高度なユースケース\n\n最初に始めやすいジェネレーター方式を紹介し、その後で完全な制御が必要な場合の手動実装方式を説明します。\n\n## 拡張モジュールジェネレーターを使用する\n\nFrankenPHPにはGoのみを使用して**PHP拡張モジュールを作成する**ツールが付属しています。**Cコードを書く必要がなく**、CGOを直接使用する必要もありません。FrankenPHPには**パブリック型API**も含まれており、**PHP/CとGo間の型変換**を心配することなくGoでPHP拡張を書くのに役立ちます。\n\n> [!TIP]\n> 拡張モジュールをGoで一から書く方法を理解したい場合は、ジェネレーターを使用せずにGoでPHP拡張モジュールを書く方法を紹介する後述の手動実装セクションを参照してください。\n\n注意すべきことは、このツールは**完全な拡張モジュールジェネレーター**ではないことです。GoでシンプルなPHP拡張モジュールを書くのには十分役立ちますが、高度なPHP拡張モジュールの機能には対応していません。より**複雑で最適化された**拡張モジュールを書く必要がある場合は、Cコードを書いたり、CGOを直接使用したりする必要があるかもしれません。\n\n### 前提条件\n\n以下の手動実装セクションでも説明しているように、[PHPのソースを取得](https://www.php.net/downloads.php)し、新しいGoモジュールを作成する必要があります。\n\n#### 新しいモジュールの作成とPHPソースの取得\n\nGoでPHP拡張モジュールを書く最初のステップは、新しいGoモジュールの作成です。以下のコマンドを使用できます：\n\n```console\ngo mod init github.com/my-account/my-module\n```\n\n2番目のステップは、次のステップのために[PHPのソースを取得](https://www.php.net/downloads.php)することです。取得したら、Goモジュールのディレクトリ内ではなく、任意のディレクトリに展開します：\n\n```console\ntar xf php-*\n```\n\n### 拡張モジュールの記述\n\nこれでGoでネイティブ関数を書く準備が整いました。`stringext.go`という名前の新しいファイルを作成します。最初の関数は文字列を引数として取り、それを指定された回数だけ繰り返し、文字列を逆転するかどうかを示すブール値を受け取り、結果の文字列を返します。これは以下のようになります：\n\n```go\nimport (\n    \"C\"\n    \"github.com/dunglas/frankenphp\"\n    \"strings\"\n)\n\n//export_php:function repeat_this(string $str, int $count, bool $reverse): string\nfunc repeat_this(s *C.zend_string, count int64, reverse bool) unsafe.Pointer {\n    str := frankenphp.GoString(unsafe.Pointer(s))\n\n    result := strings.Repeat(str, int(count))\n    if reverse {\n        runes := []rune(result)\n        for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {\n            runes[i], runes[j] = runes[j], runes[i]\n        }\n        result = string(runes)\n    }\n\n    return frankenphp.PHPString(result, false)\n}\n```\n\nここで重要なポイントが2つあります：\n\n- ディレクティブコメント`//export_php:function`はPHPでの関数シグネチャを定義します。これにより、ジェネレーターは適切なパラメータと戻り値の型でPHP関数を生成する方法を知ることができます。\n- 関数は`unsafe.Pointer`を返さなければなりません。FrankenPHPはCとGo間の型変換を支援するAPIを提供しています。\n\n前者は理解しやすいですが、後者は少し複雑かもしれません。次のセクションで型変換について詳しく説明します。\n\n### 型変換\n\nC/PHPとGoの間でメモリ表現が同じ変数型もありますが、直接使用するにはより多くのロジックが必要な型もあります。これは拡張モジュールを書く際の最も挑戦的な部分かもしれません。Zendエンジンの内部仕組みや、変数がPHP内でどのように格納されているかを理解する必要があるためです。以下の表は、知っておくべき重要な情報をまとめています：\n\n| PHP型              | Go型             | 直接変換 | CからGoヘルパー       | GoからCヘルパー        | クラスメソッドサポート |\n| ------------------ | ---------------- | -------- | --------------------- | ---------------------- | ---------------------- |\n| `int`              | `int64`          | ✅       | -                     | -                      | ✅                     |\n| `?int`             | `*int64`         | ✅       | -                     | -                      | ✅                     |\n| `float`            | `float64`        | ✅       | -                     | -                      | ✅                     |\n| `?float`           | `*float64`       | ✅       | -                     | -                      | ✅                     |\n| `bool`             | `bool`           | ✅       | -                     | -                      | ✅                     |\n| `?bool`            | `*bool`          | ✅       | -                     | -                      | ✅                     |\n| `string`/`?string` | `*C.zend_string` | ❌       | frankenphp.GoString() | frankenphp.PHPString() | ✅                     |\n| `array`            | `slice`/`map`    | ❌       | _未実装_              | _未実装_               | ❌                     |\n| `mixed`            | `any`            | ❌       | `GoValue()`           | `PHPValue()`           | ❌                     |\n| `object`           | `struct`         | ❌       | _未実装_              | _未実装_               | ❌                     |\n\n> [!NOTE]\n> この表はまだ完全ではなく、FrankenPHPの型APIがより完全になるにつれて完成されます。\n>\n> クラスメソッドについては、現在プリミティブ型のみがサポートされています。配列とオブジェクトはまだメソッドパラメータや戻り値の型として使用できません。\n\n前のセクションのコードスニペットを参照すると、最初のパラメータと戻り値の変換にヘルパーが使用されていることがわかります。 `repeat_this()`関数の2番目と3番目の引数は、基礎となる型のメモリ表現がCとGoで同じであるため、変換する必要がありません。\n\n### ネイティブPHPクラスの宣言\n\nジェネレーターは、PHPオブジェクトを作成するために使用できる**不透明クラス（opaque classes）**をGo構造体として宣言することをサポートしています。`//export_php:class`ディレクティブコメントを使用してPHPクラスを定義できます。例：\n\n```go\n//export_php:class User\ntype UserStruct struct {\n    Name string\n    Age  int\n}\n```\n\n#### 不透明クラスとは何ですか？\n\n**不透明クラス（opaque classes）**は、内部構造（プロパティ）がPHPコードから隠されているクラスです。これは以下を意味します：\n\n- **プロパティへの直接アクセス不可** ：PHPから直接プロパティを読み書きできません（`$user->name`は機能しません）\n- **メソッド経由のみで操作** - すべてのやりとりはGoで定義したメソッドを通じて行う必要があります\n- **より良いカプセル化** - 内部データ構造は完全にGoコードによって制御されます\n- **型安全性** - PHP側から誤った型で内部状態が破壊されるリスクがありません\n- **よりクリーンなAPI** - 適切な公開インターフェースを設計することを強制します\n\nこのアプローチは優れたカプセル化を実現し、PHPコードがGoオブジェクトの内部状態を意図せずに破壊してしまうことを防ぎます。オブジェクトとのすべてのやりとりは、明示的に定義したメソッドを通じて行う必要があります。\n\n#### クラスにメソッドを追加する\n\nプロパティは直接アクセスできないため、不透明クラスとやりとりするには **メソッドを定義する必要があります** 。`//export_php:method`ディレクティブを使用して動作を定義します：\n\n```go\n//export_php:class User\ntype UserStruct struct {\n    Name string\n    Age  int\n}\n\n//export_php:method User::getName(): string\nfunc (us *UserStruct) GetUserName() unsafe.Pointer {\n    return frankenphp.PHPString(us.Name, false)\n}\n\n//export_php:method User::setAge(int $age): void\nfunc (us *UserStruct) SetUserAge(age int64) {\n    us.Age = int(age)\n}\n\n//export_php:method User::getAge(): int\nfunc (us *UserStruct) GetUserAge() int64 {\n    return int64(us.Age)\n}\n\n//export_php:method User::setNamePrefix(string $prefix = \"User\"): void\nfunc (us *UserStruct) SetNamePrefix(prefix *C.zend_string) {\n    us.Name = frankenphp.GoString(unsafe.Pointer(prefix)) + \": \" + us.Name\n}\n```\n\n#### Nullableパラメータ\n\nジェネレーターは、PHPシグネチャにおける`?`プレフィックスを使用ったnullableパラメータをサポートしています。パラメータがnullableの場合、Go関数内ではポインタとして扱われ、PHP側で値が`null`だったかどうかを確認できます：\n\n```go\n//export_php:method User::updateInfo(?string $name, ?int $age, ?bool $active): void\nfunc (us *UserStruct) UpdateInfo(name *C.zend_string, age *int64, active *bool) {\n    // nameが渡された（nullではない）かチェック\n    if name != nil {\n        us.Name = frankenphp.GoString(unsafe.Pointer(name))\n    }\n\n    // ageが渡された（nullではない）かチェック\n    if age != nil {\n        us.Age = int(*age)\n    }\n\n    // activeが渡された（nullではない）かチェック\n    if active != nil {\n        us.Active = *active\n    }\n}\n```\n\n**Nullableパラメータの重要なポイント：**\n\n- **プリミティブ型のnullable** (`?int`, `?float`, `?bool`) はGoではそれぞれポインタ (`*int64`, `*float64`, `*bool`) になります\n- **nullable文字列** (`?string`) は `*C.zend_string` のままですが、`nil` になることがあります\n- ポインタ値を逆参照する前に **`nil`をチェック** してください\n- **PHPの`null`はGoの`nil`になります** - PHPが`null`を渡すと、Go関数は`nil`ポインタを受け取ります\n\n> [!WARNING]\n> 現在、クラスメソッドには次の制限があります。**配列とオブジェクトはパラメータ型や戻り値の型としてサポートされていません**。サポートされるのは`string`、`int`、`float`、`bool`、`void`（戻り値の型）といったスカラー型のみです。**nullableなスカラー型はすべてサポートされています** （`?string`、`?int`、`?float`、`?bool`）。\n\n拡張を生成した後、PHP側でクラスとそのメソッドを使用できるようになります。ただし**プロパティに直接アクセスできない**ことに注意してください：\n\n```php\n<?php\n\n$user = new User();\n\n// ✅ これは動作します - メソッドの使用\n$user->setAge(25);\necho $user->getName();           // 出力: (empty、デフォルト値)\necho $user->getAge();            // 出力: 25\n$user->setNamePrefix(\"Employee\");\n\n// ✅ これも動作します - nullableパラメータ\n$user->updateInfo(\"John\", 30, true);        // すべて指定\n$user->updateInfo(\"Jane\", null, false);     // Ageがnull\n$user->updateInfo(null, 25, null);          // Nameとactiveがnull\n\n// ❌ これは動作しません - プロパティへの直接アクセス\n// echo $user->name;             // エラー: privateプロパティにアクセスできません\n// $user->age = 30;              // エラー: privateプロパティにアクセスできません\n```\n\nこの設計により、Goコードがオブジェクトの状態へのアクセスと変更方法を完全に制御でき、より良いカプセル化と型安全性を提供します。\n\n### 定数の宣言\n\nジェネレーターは、2つのディレクティブを使用してGo定数をPHPにエクスポートすることをサポートしています：グローバル定数用の`//export_php:const`とクラス定数用の`//export_php:classconst`です。これにより、GoとPHPコード間で設定値、ステータスコード、その他の定数を共有できます。\n\n#### グローバル定数\n\n`//export_php:const`ディレクティブを使用してグローバルなPHP定数を作成できます：\n\n```go\n//export_php:const\nconst MAX_CONNECTIONS = 100\n\n//export_php:const\nconst API_VERSION = \"1.2.3\"\n\n//export_php:const\nconst STATUS_OK = iota\n\n//export_php:const\nconst STATUS_ERROR = iota\n```\n\n#### クラス定数\n\n`//export_php:classconst ClassName`ディレクティブを使用して、特定のPHPクラスに属する定数を作成できます：\n\n```go\n//export_php:classconst User\nconst STATUS_ACTIVE = 1\n\n//export_php:classconst User\nconst STATUS_INACTIVE = 0\n\n//export_php:classconst User\nconst ROLE_ADMIN = \"admin\"\n\n//export_php:classconst Order\nconst STATE_PENDING = iota\n\n//export_php:classconst Order\nconst STATE_PROCESSING = iota\n\n//export_php:classconst Order\nconst STATE_COMPLETED = iota\n```\n\nクラス定数は、PHPでクラス名スコープを使用してアクセスできます：\n\n```php\n<?php\n\n// グローバル定数\necho MAX_CONNECTIONS;    // 100\necho API_VERSION;        // \"1.2.3\"\n\n// クラス定数\necho User::STATUS_ACTIVE;    // 1\necho User::ROLE_ADMIN;       // \"admin\"\necho Order::STATE_PENDING;   // 0\n```\n\nディレクティブは、文字列、整数、ブール値、浮動小数点数、iota定数など、さまざまな値の型をサポートしています。`iota`を使用する場合、ジェネレーターは自動的に連続した値（0, 1, 2など）を割り当てます。グローバル定数はPHPコードでグローバル定数として利用可能になり、クラス定数はpublicとしてそれぞれのクラスにスコープされます。整数を使用する場合、異なる記法（バイナリ、16進数、8進数）がサポートされ、PHPのスタブファイルにそのまま出力されます。\n\nGo側のコードでは、いつも通り定数を使用できます。例えば、先ほど作成した`repeat_this()`関数を取り上げ、最後の引数を整数に変更してみましょう：\n\n```go\nimport (\n    \"C\"\n    \"github.com/dunglas/frankenphp\"\n    \"strings\"\n)\n\n//export_php:const\nconst STR_REVERSE = iota\n\n//export_php:const\nconst STR_NORMAL = iota\n\n//export_php:classconst StringProcessor\nconst MODE_LOWERCASE = 1\n\n//export_php:classconst StringProcessor\nconst MODE_UPPERCASE = 2\n\n//export_php:function repeat_this(string $str, int $count, int $mode): string\nfunc repeat_this(s *C.zend_string, count int64, mode int) unsafe.Pointer {\n    str := frankenphp.GoString(unsafe.Pointer(s))\n\n    result := strings.Repeat(str, int(count))\n    if mode == STR_REVERSE {\n        // 文字列を逆転\n    }\n\n    if mode == STR_NORMAL {\n        // 何もしない、定数を示すためのみ記載\n    }\n\n    return frankenphp.PHPString(result, false)\n}\n\n//export_php:class StringProcessor\ntype StringProcessorStruct struct {\n    // 内部フィールド\n}\n\n//export_php:method StringProcessor::process(string $input, int $mode): string\nfunc (sp *StringProcessorStruct) Process(input *C.zend_string, mode int64) unsafe.Pointer {\n    str := frankenphp.GoString(unsafe.Pointer(input))\n\n    switch mode {\n    case MODE_LOWERCASE:\n        str = strings.ToLower(str)\n    case MODE_UPPERCASE:\n        str = strings.ToUpper(str)\n    }\n\n    return frankenphp.PHPString(str, false)\n}\n```\n\n### 拡張モジュールの生成\n\nここでいよいよ、拡張モジュールを生成できるようになります。以下のコマンドでジェネレーターを実行できます：\n\n```console\nGEN_STUB_SCRIPT=php-src/build/gen_stub.php frankenphp extension-init my_extension.go\n```\n\n> [!NOTE] > `GEN_STUB_SCRIPT`環境変数に、先ほどダウンロードしたPHPソースの`gen_stub.php`ファイルのパスを設定するのを忘れないでください。これは手動実装セクションで言及されているのと同じ`gen_stub.php`スクリプトです。\n\nすべてがうまくいけば、`build`という名前の新しいディレクトリが作成されているはずです。このディレクトリには、生成されたPHP関数スタブを含む`my_extension.go`ファイルなど、拡張用の生成されたファイルが含まれています。\n\n### 生成された拡張モジュールをFrankenPHPへ統合する\n\n拡張モジュールがコンパイルされ、FrankenPHPに統合される準備が整いました。これを行うには、FrankenPHPのコンパイル方法を学ぶために、FrankenPHPの[コンパイルドキュメント](compile.md)を参照してください。`--with`フラグを使用してモジュールを追加し、モジュールのパスを指定します：\n\n```console\nCGO_ENABLED=1 \\\nXCADDY_GO_BUILD_FLAGS=\"-ldflags='-w -s' -tags=nobadger,nomysql,nopgx\" \\\nCGO_CFLAGS=$(php-config --includes) \\\nCGO_LDFLAGS=\"$(php-config --ldflags) $(php-config --libs)\" \\\nxcaddy build \\\n    --output frankenphp \\\n    --with github.com/my-account/my-module/build\n```\n\nこのとき、生成ステップで作成された`/build`サブディレクトリを指していることに注意してください。ただし、これは必須ではなく、生成されたファイルをモジュールのディレクトリにコピーして、直接それを指定することも可能です。\n\n### 生成された拡張モジュールのテスト\n\n作成した関数とクラスをテストするPHPファイルを作成しましょう。例えば、以下の内容で`index.php`ファイルを作成します：\n\n```php\n<?php\n\n// グローバル定数を使用\nvar_dump(repeat_this('Hello World', 5, STR_REVERSE));\n\n// クラス定数を使用\n$processor = new StringProcessor();\necho $processor->process('Hello World', StringProcessor::MODE_LOWERCASE);  // \"hello world\"\necho $processor->process('Hello World', StringProcessor::MODE_UPPERCASE);  // \"HELLO WORLD\"\n```\n\n前のセクションで示したように拡張モジュールをFrankenPHPに統合し、`./frankenphp php-server`を使用してこのテストファイルを実行することで、拡張モジュールが動作しているのを確認できるはずです。\n\n## 手動実装\n\n拡張モジュールの仕組みを理解したい、または拡張モジュールを完全に制御したい場合は、手動で書くこともできます。このアプローチは完全な制御を実現できますが、より多くのボイラープレートコードが必要になります。\n\n### 基本的な関数\n\nここでは、新しいネイティブ関数を定義するシンプルなPHP拡張モジュールをGoで手動実装する方法を紹介します。この関数はPHPから呼び出され、その関数がgoroutineを使ってCaddyのログにメッセージ出力するという処理を行います。この関数は引数を取らず、戻り値もありません。\n\n#### Go関数の定義\n\nモジュール内で、PHPから呼び出される新しいネイティブ関数を定義する必要があります。これを行うには、例えば`extension.go`のように任意の名前でファイルを作成し、以下のコードを追加します：\n\n```go\npackage ext_go\n\n//#include \"extension.h\"\nimport \"C\"\nimport (\n    \"unsafe\"\n    \"github.com/caddyserver/caddy/v2\"\n    \"github.com/dunglas/frankenphp\"\n)\n\nfunc init() {\n    frankenphp.RegisterExtension(unsafe.Pointer(&C.ext_module_entry))\n}\n\n//export go_print_something\nfunc go_print_something() {\n    go func() {\n        caddy.Log().Info(\"Hello from a goroutine!\")\n    }()\n}\n```\n\n`frankenphp.RegisterExtension()`関数は、内部のPHP登録ロジックを処理することで拡張登録プロセスを簡素化します。`go_print_something`関数は`//export`ディレクティブを使用して、CGOのおかげで、これから書くCコードでアクセスできるようになることを示しています。\n\nこの例では、新しい関数がCaddyのログにメッセージ出力するgoroutineをトリガーします。\n\n#### PHP関数の定義\n\nPHPがGo関数を呼び出せるようにするには、対応するPHP関数を定義する必要があります。このために、例えば`extension.stub.php`のようにスタブファイルを作成し、以下のコードを記述します：\n\n```php\n<?php\n\n/** @generate-class-entries */\n\nfunction go_print(): void {}\n```\n\nこのファイルはPHPから呼び出される`go_print()`関数のシグネチャを定義します。`@generate-class-entries`ディレクティブは、PHPがこの拡張モジュールのために関数エントリを自動生成することを可能にします。\n\nこれは手動ではなく、PHPソースで提供されるスクリプトを使用して行います（PHPソースが置かれている場所に基づいて`gen_stub.php`スクリプトのパスを調整してください）：\n\n```bash\nphp ../php-src/build/gen_stub.php extension.stub.php\n```\n\nこのスクリプトは、PHPがこの関数の定義および呼び出し方法を知るのに必要な情報を含む`extension_arginfo.h`という名前のファイルを生成します。\n\n#### GoとC間のブリッジの作成\n\n今度は、GoとC間をつなぐブリッジを書く必要があります。モジュールディレクトリに`extension.h`という名前のファイルを作成し、以下の内容を書きます：\n\n```c\n#ifndef _EXTENSION_H\n#define _EXTENSION_H\n\n#include <php.h>\n\nextern zend_module_entry ext_module_entry;\n\n#endif\n```\n\n次に、以下のステップを実行する`extension.c`という名前のファイルを作成します：\n\n- PHPヘッダーをインクルードする\n- 新しいネイティブPHP関数`go_print()`を宣言する\n- 拡張モジュールのメタデータを宣言する\n\nまずは必要なヘッダーのインクルードから始めましょう：\n\n```c\n#include <php.h>\n#include \"extension.h\"\n#include \"extension_arginfo.h\"\n\n// Goによってエクスポートされたシンボルを含みます\n#include \"_cgo_export.h\"\n```\n\n次に、PHP関数をネイティブ言語関数として定義します：\n\n```c\nPHP_FUNCTION(go_print)\n{\n    ZEND_PARSE_PARAMETERS_NONE();\n\n    go_print_something();\n}\n\nzend_module_entry ext_module_entry = {\n    STANDARD_MODULE_HEADER,\n    \"ext_go\",\n    ext_functions, /* Functions */\n    NULL,          /* MINIT */\n    NULL,          /* MSHUTDOWN */\n    NULL,          /* RINIT */\n    NULL,          /* RSHUTDOWN */\n    NULL,          /* MINFO */\n    \"0.1.1\",\n    STANDARD_MODULE_PROPERTIES\n};\n```\n\nこの場合、関数はパラメータを取らず、何も返しません。単に`//export`ディレクティブを使用してエクスポートした、先ほど定義したGo関数を呼び出します。\n\n最後に、名前、バージョン、プロパティなど、拡張のメタデータを`zend_module_entry`構造体で定義します。この情報はPHPが私たちの拡張モジュールを認識してロードするために必要です。`ext_functions`は定義したPHP関数へのポインタの配列であり、`gen_stub.php`スクリプトによって自動生成された`extension_arginfo.h`ファイル内に定義されています。\n\n拡張モジュールの登録は、Goコード内で呼び出しているFrankenPHPの`RegisterExtension()`関数によって自動的に処理されます。\n\n### 高度な使用方法\n\n基本的なPHP拡張をGoで作成する方法が分かったところで、少し例を複雑にしてみましょう。今度は文字列を引数として受け取り、その大文字版を返すPHP関数を作成します。\n\n#### PHP関数スタブの定義\n\n新しいPHP関数を定義するために、`extension.stub.php`ファイルを修正し、次の関数シグネチャを含めます：\n\n```php\n<?php\n\n/** @generate-class-entries */\n\n/**\n * Converts a string to uppercase.\n *\n * @param string $string The string to convert.\n * @return string The uppercase version of the string.\n */\nfunction go_upper(string $string): string {}\n```\n\n> [!TIP]\n> 関数のドキュメントを軽視しないでください！拡張スタブを他の開発者と共有する際、拡張機能の使い方や提供している機能を伝えるための重要な手段になります。\n\n`gen_stub.php`スクリプトでスタブファイルを再生成すると、`extension_arginfo.h`ファイルは以下のようになるはずです：\n\n```c\nZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_go_upper, 0, 1, IS_STRING, 0)\n    ZEND_ARG_TYPE_INFO(0, string, IS_STRING, 0)\nZEND_END_ARG_INFO()\n\nZEND_FUNCTION(go_upper);\n\nstatic const zend_function_entry ext_functions[] = {\n    ZEND_FE(go_upper, arginfo_go_upper)\n    ZEND_FE_END\n};\n```\n\nこの出力から、`go_upper`関数が`string`型の引数を1つ受け取り、`string`型の戻り値を返すことが定義されていのがわかります。\n\n#### GoとPHP/C間の型変換（Type Juggling）\n\nGo関数はPHPの文字列を引数として直接受け取ることはできません。そのためPHPの文字列をGoの文字列へ変換する必要があります。幸いなことに、FrankenPHPは、ジェネレーターアプローチで見たものと同様に、PHP文字列とGo文字列間の変換を処理するヘルパー関数を提供しています。\n\nヘッダーファイルはシンプルなままです：\n\n```c\n#ifndef _EXTENSION_H\n#define _EXTENSION_H\n\n#include <php.h>\n\nextern zend_module_entry ext_module_entry;\n\n#endif\n```\n\n次に、`extension.c`ファイルにGoとC間のブリッジを書きます。ここではPHPの文字列を直接Go関数に渡します：\n\n```c\nPHP_FUNCTION(go_upper)\n{\n    zend_string *str;\n\n    ZEND_PARSE_PARAMETERS_START(1, 1)\n        Z_PARAM_STR(str)\n    ZEND_PARSE_PARAMETERS_END();\n\n    zend_string *result = go_upper(str);\n    RETVAL_STR(result);\n}\n```\n\n`ZEND_PARSE_PARAMETERS_START`や引数のパースについては、[PHP Internals Book](https://www.phpinternalsbook.com/php7/extensions_design/php_functions.html#parsing-parameters-zend-parse-parameters)の該当ページで詳しく学ぶことができます。この例では、関数が`zend_string`として`string`型の必須引数を1つ取ることをPHPに伝えています。その後、この文字列を直接Go関数に渡し、`RETVAL_STR`を使用して結果を返します。\n\n残るはただ一つ、Go側で`go_upper`関数を実装するだけです。\n\n#### Go関数の実装\n\nGo側の関数では`*C.zend_string`を引数として受け取り、FrankenPHPのヘルパー関数を使用してGoの文字列に変換し、処理を行ったうえで、結果を新たな`*C.zend_string`として返します。メモリ管理と変換の複雑さは、ヘルパー関数がすべて対応してくれます。\n\n```go\nimport \"strings\"\n\n//export go_upper\nfunc go_upper(s *C.zend_string) *C.zend_string {\n    str := frankenphp.GoString(unsafe.Pointer(s))\n\n    upper := strings.ToUpper(str)\n\n    return (*C.zend_string)(frankenphp.PHPString(upper, false))\n}\n```\n\nこのアプローチは、手動メモリ管理よりもはるかにクリーンで安全です。FrankenPHPのヘルパー関数は、PHPの`zend_string`形式とGoの文字列間の変換を自動的に処理してくれます。`PHPString()`に`false`引数を指定していることで、新しい非永続文字列（リクエストの終了時に解放される）を作成したいことを示しています。\n\n> [!TIP]\n> この例ではエラーハンドリングを省略していますが、Go関数内でポインタが`nil`ではないこと、渡されたデータが有効であることを常に確認するべきです。\n\n### 拡張モジュールのFrankenPHPへの統合\n\n拡張モジュールがコンパイルされ、FrankenPHPに統合される準備が整いました。手順についてはFrankenPHPのコンパイル方法を学ぶために、FrankenPHPの[コンパイルドキュメント](compile.md)を参照してください。`--with`フラグを使用してモジュールを追加し、モジュールのパスを指定します：\n\n```console\nCGO_ENABLED=1 \\\nXCADDY_GO_BUILD_FLAGS=\"-ldflags='-w -s' -tags=nobadger,nomysql,nopgx\" \\\nCGO_CFLAGS=$(php-config --includes) \\\nCGO_LDFLAGS=\"$(php-config --ldflags) $(php-config --libs)\" \\\nxcaddy build \\\n    --output frankenphp \\\n    --with github.com/my-account/my-module\n```\n\nこれで完了です！拡張モジュールがFrankenPHPに統合され、PHPコードで利用できるようになりました。\n\n### 拡張モジュールのテスト\n\n拡張モジュールをFrankenPHPに統合したら、実装した関数を試すための`index.php`ファイルを作成します：\n\n```php\n<?php\n\n// 基本関数のテスト\ngo_print();\n\n// 高度な関数のテスト\necho go_upper(\"hello world\") . \"\\n\";\n```\n\nこのファイルを使用して`./frankenphp php-server`でFrankenPHPを実行でき、拡張モジュールが動作しているのを確認できるはずです。\n"
  },
  {
    "path": "docs/ja/github-actions.md",
    "content": "# GitHub Actionsの使用\n\nこのリポジトリでは、承認されたプルリクエストごと、またはセットアップ後のあなた自身のフォークで、\nDockerイメージをビルドして[Docker Hub](https://hub.docker.com/r/dunglas/frankenphp)にデプロイします。\n\n## GitHub Actionsのセットアップ\n\nリポジトリ設定のシークレットで、以下のシークレットを追加してください：\n\n- `REGISTRY_LOGIN_SERVER`: 使用するDockerレジストリ（例：`docker.io`）\n- `REGISTRY_USERNAME`: レジストリログイン用のユーザー名（例：`dunglas`）\n- `REGISTRY_PASSWORD`: レジストリログイン用のパスワード（例：アクセスキー）\n- `IMAGE_NAME`: イメージの名前（例：`dunglas/frankenphp`）\n\n## イメージのビルドとプッシュ\n\n1. プルリクエストを作成するか、フォークにプッシュします\n2. GitHub Actionsがイメージをビルドし、テストを実行します\n3. ビルドが成功した場合、イメージは`pr-x`（`x`はPR番号）をタグとしてレジストリにプッシュされます\n\n## イメージのデプロイ\n\n1. プルリクエストがマージされると、GitHub Actionsが再度テストを実行し、新しいイメージをビルドします\n2. ビルドが成功した場合、Dockerレジストリの`main`タグが更新されます\n\n## リリース\n\n1. リポジトリで新しいタグを作成します\n2. GitHub Actionsがイメージをビルドし、テストを実行します\n3. ビルドが成功した場合、イメージはタグ名をタグとしてレジストリにプッシュされます（例：`v1.2.3`と`v1.2`が作成されます）\n4. `latest`タグも更新されます\n"
  },
  {
    "path": "docs/ja/hot-reload.md",
    "content": "# ホットリロード\n\nFrankenPHPには、開発者のエクスペリエンスを大幅に向上させるために設計された組み込みの**ホットリロード**機能が含まれています。\n\n![Hot Reload](hot-reload.png)\n\nこの機能は、ViteやwebpackなどのモダンなJavaScriptツールにおける**ホットモジュールリプレースメント (HMR)** に似たワークフローを提供します。\nファイルの変更（PHPコード、テンプレート、JavaScript、CSSファイルなど）のたびに手動でブラウザをリフレッシュする代わりに、\nFrankenPHPはページの内容をリアルタイムで更新します。\n\nホットリロードは、WordPress、Laravel、Symfony、その他すべてのPHPアプリケーションやフレームワークでネイティブに動作します。\n\n有効にすると、FrankenPHPは現在の作業ディレクトリにおけるファイルシステムの変更を監視します。\nファイルが変更されると、[Mercure](mercure.md)の更新をブラウザにプッシュします。\n\n設定に応じて、ブラウザは以下のいずれかを実行します。\n\n- [Idiomorph](https://github.com/bigskysoftware/idiomorph)がロードされている場合、**DOMをモーフィング**します（スクロール位置と入力状態を保持）。\n- Idiomorphが存在しない場合、**ページをリロード**します（標準のライブリロード）。\n\n## 設定\n\nホットリロードを有効にするには、Mercureを有効にしてから、`Caddyfile`の`php_server`ディレクティブに`hot_reload`サブディレクティブを追加します。\n\n> [!WARNING]\n>\n> この機能は**開発環境のみ**を対象としています。\n> `hot_reload`を本番環境で有効にしないでください。この機能は安全ではなく（機密性の高い内部詳細を公開します）、アプリケーションの速度を低下させます。\n>\n```caddyfile\nlocalhost\n\nmercure {\n    anonymous\n}\n\nroot public/\nphp_server {\n    hot_reload\n}\n```\n\nデフォルトでは、FrankenPHPは現在の作業ディレクトリ内の以下のグロブパターンに一致するすべてのファイルを監視します: `./**/*.{css,env,gif,htm,html,jpg,jpeg,js,mjs,php,png,svg,twig,webp,xml,yaml,yml}`\n\nグロブ構文を使用して、監視するファイルを明示的に設定することも可能です。\n\n```caddyfile\nlocalhost\n\nmercure {\n    anonymous\n}\n\nroot public/\nphp_server {\n    hot_reload src/**/*{.php,.js} config/**/*.yaml\n}\n```\n\n使用するMercureトピック、および監視するディレクトリまたはファイルを指定するには、`hot_reload`のロングフォームを使用します。\n\n```caddyfile\nlocalhost\n\nmercure {\n    anonymous\n}\n\nroot public/\nphp_server {\n    hot_reload {\n        topic hot-reload-topic\n        watch src/**/*.php\n        watch assets/**/*.{ts,json}\n        watch templates/\n        watch public/css/\n    }\n}\n```\n\n## クライアントサイドの統合\n\nサーバーは変更を検出しますが、ブラウザはこれらのイベントを購読してページを更新する必要があります。\nFrankenPHPは、ファイル変更を購読するために使用するMercure HubのURLを、`$_SERVER['FRANKENPHP_HOT_RELOAD']`環境変数を通じて公開します。\n\nクライアントサイドのロジックを処理するための便利なJavaScriptライブラリ、[frankenphp-hot-reload](https://www.npmjs.com/package/frankenphp-hot-reload)も利用可能です。\nこれを使用するには、メインレイアウトに以下を追加します。\n\n```php\n<!DOCTYPE html>\n<title>FrankenPHP Hot Reload</title>\n<?php if (isset($_SERVER['FRANKENPHP_HOT_RELOAD'])): ?>\n<meta name=\"frankenphp-hot-reload:url\" content=\"<?=$_SERVER['FRANKENPHP_HOT_RELOAD']?>\">\n<script src=\"https://cdn.jsdelivr.net/npm/idiomorph\"></script>\n<script src=\"https://cdn.jsdelivr.net/npm/frankenphp-hot-reload/+esm\" type=\"module\"></script>\n<?php endif ?>\n```\n\nこのライブラリは自動的にMercureハブを購読し、ファイル変更が検出されるとバックグラウンドで現在のURLをフェッチし、DOMをモーフィングします。\n[npm](https://www.npmjs.com/package/frankenphp-hot-reload)パッケージとして、また[GitHub](https://github.com/dunglas/frankenphp-hot-reload)で利用できます。\n\nまたは、`EventSource`ネイティブJavaScriptクラスを使用してMercureハブに直接購読することで、独自のクライアントサイドロジックを実装することもできます。\n\n### 既存のDOMノードを保持する\n\nまれに、[Symfonyのウェブデバッグツールバーなどの開発ツール](https://github.com/symfony/symfony/pull/62970)を使用している場合など、特定のDOMノードを保持したいことがあります。\nそのためには、関連するHTML要素に`data-frankenphp-hot-reload-preserve`属性を追加します。\n\n```html\n<div data-frankenphp-hot-reload-preserve><!-- My debug bar --></div>\n```\n\n## ワーカーモード\n\nアプリケーションを[ワーカーモード](https://frankenphp.dev/docs/worker/)で実行している場合、アプリケーションスクリプトはメモリに常駐します。\nこれは、ブラウザがリロードされても、PHPコードの変更がすぐに反映されないことを意味します。\n\n最高の開発者エクスペリエンスのためには、`hot_reload`を[ワーカーディレクティブ内の`watch`サブディレクティブ](config.md#watching-for-file-changes)と組み合わせるべきです。\n\n- `hot_reload`: ファイルが変更されたときに**ブラウザ**をリフレッシュします\n- `worker.watch`: ファイルが変更されたときにワーカーを再起動します\n\n```caddy\nlocalhost\n\nmercure {\n    anonymous\n}\n\nroot public/\nphp_server {\n    hot_reload\n    worker {\n        file /path/to/my_worker.php\n        watch\n    }\n}\n```\n\n## 仕組み\n\n1. **監視**: FrankenPHPは、内部で[`e-dant/watcher`ライブラリ](https://github.com/e-dant/watcher)を使用してファイルシステムの変更を監視します（私たちはGoバインディングに貢献しました）。\n2. **再起動 (ワーカーモード)**: ワーカー設定で`watch`が有効になっている場合、新しいコードをロードするためにPHPワーカーが再起動されます。\n3. **プッシュ**: 変更されたファイルのリストを含むJSONペイロードが、組み込みの[Mercureハブ](https://mercure.rocks)に送信されます。\n4. **受信**: JavaScriptライブラリを介してリッスンしているブラウザがMercureイベントを受信します。\n5. **更新**:\n\n    - **Idiomorph**が検出された場合、更新されたコンテンツをフェッチし、現在のHTMLを新しい状態に合わせてモーフィングし、状態を失うことなく即座に変更を適用します。\n    - それ以外の場合、`window.location.reload()`が呼び出されてページがリフレッシュされます。\n"
  },
  {
    "path": "docs/ja/known-issues.md",
    "content": "# 既知の問題\n\n## 未対応のPHP拡張モジュール\n\n以下の拡張モジュールはFrankenPHPと互換性がないことが確認されています：\n\n| 名前                                                                                                        | 理由                 | 代替手段                                                                                                             |\n| ----------------------------------------------------------------------------------------------------------- | -------------------- | -------------------------------------------------------------------------------------------------------------------- |\n| [imap](https://www.php.net/manual/en/imap.installation.php)                                                 | スレッドセーフでない | [javanile/php-imap2](https://github.com/javanile/php-imap2), [webklex/php-imap](https://github.com/Webklex/php-imap) |\n| [newrelic](https://docs.newrelic.com/docs/apm/agents/php-agent/getting-started/introduction-new-relic-php/) | スレッドセーフでない | -                                                                                                                    |\n\n## バグのあるPHP拡張モジュール\n\n以下の拡張モジュールはFrankenPHPとの組み合わせで既知のバグや予期しない動作が確認されています：\n\n| 名前                                                          | 問題                                                                                                                                                                                                                                                                                 |\n| ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| [ext-openssl](https://www.php.net/manual/en/book.openssl.php) | FrankenPHPの静的ビルド（musl libcでビルド）を使用した場合、高負荷時にOpenSSL拡張がクラッシュすることがあります。回避策として動的リンクのビルド（Dockerイメージで使用されているもの）を使用してください。このバグは[PHP側で追跡中](https://github.com/php/php-src/issues/13648)です。 |\n\n## get_browser\n\n[get_browser()](https://www.php.net/manual/en/function.get-browser.php)関数は継続使用するとパフォーマンスが悪化することが確認されています。回避策として、User Agentごとの結果をキャッシュ（例：[APCu](https://www.php.net/manual/en/book.apcu.php)を利用）してください。User Agentごとの結果は静的なためです。\n\n## スタンドアロンバイナリおよびAlpineベースのDockerイメージ\n\nスタンドアロンバイナリおよびAlpineベースのDockerイメージ（`dunglas/frankenphp:*-alpine`）は、バイナリサイズを小さく保つために[glibc and friends](https://www.etalabs.net/compare_libcs.html)ではなく[musl libc](https://musl.libc.org/)を使用しています。これによりいくつかの互換性問題が発生する可能性があります。特に、globフラグ`GLOB_BRACE`は [サポートされていません](https://www.php.net/manual/en/function.glob.php) 。\n\n## Dockerで`https://127.0.0.1`を使用する\n\nデフォルトでは、FrankenPHPは`localhost`用のTLS証明書を生成します。\nこれはローカル開発における最も簡単かつ推奨される方法です。\n\nどうしても`127.0.0.1`をホストとして使用したい場合は、サーバー名を`127.0.0.1`に設定してその証明書を生成させることが可能です。\n\nただし、[Dockerのネットワークシステム](https://docs.docker.com/network/)の仕組みにより、Dockerを使用する場合はこれだけでは不十分です。\nこの場合、`curl: (35) LibreSSL/3.3.6: error:1404B438:SSL routines:ST_CONNECT:tlsv1 alert internal error`のようなTLSエラーが発生します。\n\nLinuxを使用している場合、[ホストネットワークドライバー](https://docs.docker.com/network/network-tutorial-host/)を使用することで、この問題を解決できます：\n\n```console\ndocker run \\\n    -e SERVER_NAME=\"127.0.0.1\" \\\n    -v $PWD:/app/public \\\n    --network host \\\n    dunglas/frankenphp\n```\n\nホストネットワークドライバーはMacとWindowsではサポートされていません。これらのプラットフォームでは、コンテナのIPアドレスを推測してサーバー名に含める必要があります。\n\n`docker network inspect bridge`を実行し、`Containers`キーを確認して`IPv4Address`にある現在割り当てられている最後のIPアドレスを特定し、それに1を加えます。コンテナがまだ実行されていない場合、最初に割り当てられるIPアドレスは通常`172.17.0.2`です。\n\nそして、これを`SERVER_NAME`環境変数に含めます：\n\n```console\ndocker run \\\n    -e SERVER_NAME=\"127.0.0.1, 172.17.0.3\" \\\n    -v $PWD:/app/public \\\n    -p 80:80 -p 443:443 -p 443:443/udp \\\n    dunglas/frankenphp\n```\n\n> [!CAUTION]\n>\n> `172.17.0.3`の部分は、実際にコンテナに割り当てられるIPに置き換えてください。\n\nこれでホストマシンから`https://127.0.0.1`へアクセスできるはずです。\n\nうまくいかない場合は、FrankenPHPをデバッグモードで起動して問題を特定してみてください：\n\n```console\ndocker run \\\n    -e CADDY_GLOBAL_OPTIONS=\"debug\" \\\n    -e SERVER_NAME=\"127.0.0.1\" \\\n    -v $PWD:/app/public \\\n    -p 80:80 -p 443:443 -p 443:443/udp \\\n    dunglas/frankenphp\n```\n\n## `@php` を参照するComposerスクリプト\n\n[Composerスクリプト](https://getcomposer.org/doc/articles/scripts.md)では、いくつかのタスクでPHPバイナリを実行したい場合があります。例えば、[Laravelプロジェクト](laravel.md)で`@php artisan package:discover --ansi`を実行する場合です。しかし現在これは以下の2つの理由で[失敗します](https://github.com/dunglas/frankenphp/issues/483#issuecomment-1899890915)：\n\n- ComposerはFrankenPHPバイナリを呼び出す方法を知りません\n- Composerはコマンドで`-d`フラグを使用してPHP設定を追加する場合があり、FrankenPHPはまだサポートしていません\n\n回避策として、未サポートのパラメータを削除してFrankenPHPを呼び出すシェルスクリプトを`/usr/local/bin/php`に作成できます：\n\n```bash\n#!/usr/bin/env bash\nargs=(\"$@\")\nindex=0\nfor i in \"$@\"\ndo\n    if [ \"$i\" == \"-d\" ]; then\n        unset 'args[$index]'\n        unset 'args[$index+1]'\n    fi\n    index=$((index+1))\ndone\n\n/usr/local/bin/frankenphp php-cli ${args[@]}\n```\n\n次に、環境変数`PHP_BINARY`にこの`php`スクリプトのパスを設定してComposerを実行します：\n\n```console\nexport PHP_BINARY=/usr/local/bin/php\ncomposer install\n```\n\n## 静的バイナリでのTLS/SSL問題のトラブルシューティング\n\n静的バイナリを使用する場合、例えばSTARTTLSを使用してメールを送信する際に以下のTLS関連エラーが発生する可能性があります：\n\n```text\nUnable to connect with STARTTLS: stream_socket_enable_crypto(): SSL operation failed with code 5. OpenSSL Error messages:\nerror:80000002:system library::No such file or directory\nerror:80000002:system library::No such file or directory\nerror:80000002:system library::No such file or directory\nerror:0A000086:SSL routines::certificate verify failed\n```\n\n静的バイナリにはTLS証明書がバンドルされていないため、OpenSSLにローカルのCA証明書の位置を明示する必要があります。\n\n[`openssl_get_cert_locations()`](https://www.php.net/manual/en/function.openssl-get-cert-locations.php)の出力を調べて、\nCA証明書をどこにインストールすべきか確認し、その場所に保存してください。\n\n> [!WARNING]\n>\n> WebとCLIコンテキストでは設定が異なる場合があります。\n> 適切なコンテキストで`openssl_get_cert_locations()`を実行してください。\n\n[Mozillaから抽出されたCA証明書はcurlのサイトでダウンロードできます](https://curl.se/docs/caextract.html)。\n\nまたは、Debian、Ubuntu、Alpineなどのディストリビューションでも、これらの証明書を含む`ca-certificates`というパッケージを提供しています。\n\n`SSL_CERT_FILE`および`SSL_CERT_DIR`を使用してOpenSSLにCA証明書を探す場所をヒントとして与えることも可能です：\n\n```console\n# TLS 証明書の環境変数を設定\nexport SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt\nexport SSL_CERT_DIR=/etc/ssl/certs\n```\n"
  },
  {
    "path": "docs/ja/laravel.md",
    "content": "# Laravel\n\n## Docker\n\nFrankenPHPを使用して[Laravel](https://laravel.com)のWebアプリケーションを配信するのは簡単で、公式Dockerイメージの`/app`ディレクトリにプロジェクトをマウントするだけです。\n\nLaravelアプリのメインディレクトリからこのコマンドを実行してください：\n\n```console\ndocker run -p 80:80 -p 443:443 -p 443:443/udp -v $PWD:/app dunglas/frankenphp\n```\n\nお楽しみください！\n\n## ローカルインストール\n\nまたは、ローカルマシンでFrankenPHPを使用してLaravelプロジェクトを実行することもできます：\n\n1. [使用しているシステムに対応するバイナリをダウンロードします](../#standalone-binary)\n2. Laravelプロジェクトのルートディレクトリに`Caddyfile`という名前のファイルを作成し、以下の設定を追加します：\n\n   ```caddyfile\n   {\n   \tfrankenphp\n   }\n\n   # サーバーのドメイン名\n   localhost {\n   \t# webroot を public/ ディレクトリに設定\n   \troot public/\n   \t# 圧縮を有効にする（任意）\n   \tencode zstd br gzip\n   \t# public/ ディレクトリ内の PHP ファイルを実行し、アセットを提供\n   \tphp_server {\n   \t\ttry_files {path} index.php\n   \t}\n   }\n   ```\n\n3. LaravelプロジェクトのルートディレクトリからFrankenPHPを起動します： `frankenphp run`\n\n## Laravel Octane\n\nOctaneはComposerパッケージマネージャーを使用してインストールできます：\n\n```console\ncomposer require laravel/octane\n```\n\nOctaneをインストールした後、`octane:install` Artisanコマンドを実行すると、Octaneの設定ファイルがアプリケーションにインストールされます：\n\n```console\nphp artisan octane:install --server=frankenphp\n```\n\nOctaneサーバーは`octane:frankenphp` Artisanコマンドで開始できます。\n\n```console\nphp artisan octane:frankenphp\n```\n\n`octane:frankenphp`コマンドは以下のオプションが利用可能です：\n\n- `--host`: サーバーがバインドするIPアドレス（デフォルト：`127.0.0.1`）\n- `--port`: サーバーが使用するポート（デフォルト： `8000`）\n- `--admin-port`: 管理サーバーが使用するポート（デフォルト： `2019`）\n- `--workers`: リクエスト処理に使うワーカー数（デフォルト： `auto`）\n- `--max-requests`: サーバーを再起動するまでに処理するリクエスト数（デフォルト： `500`）\n- `--caddyfile`: FrankenPHPの`Caddyfile`ファイルのパス（デフォルト： [Laravel OctaneのスタブCaddyfile](https://github.com/laravel/octane/blob/2.x/src/Commands/stubs/Caddyfile)）\n- `--https`: HTTPS、HTTP/2、HTTP/3を有効にし、証明書を自動的に生成・更新する\n- `--http-redirect`: HTTPからHTTPSへのリダイレクトを有効にする（--httpsオプション指定時のみ有効）\n- `--watch`: アプリケーションが変更されたときに自動的にサーバーをリロードする\n- `--poll`: ネットワーク越しのファイル監視のためにファイルシステムポーリングを使用する\n- `--log-level`: ネイティブCaddyロガーを使用して、指定されたログレベル以上でログメッセージを記録する\n\n> [!TIP]\n> 構造化されたJSONログ（ログ分析ソリューションを使用する際に便利）を取得するには、明示的に`--log-level`オプションを指定してください。\n\n詳しくは[Laravel Octaneの公式ドキュメント](https://laravel.com/docs/octane)をご覧ください。\n\n## Laravelアプリのスタンドアロンバイナリ化\n\n[FrankenPHPのアプリケーション埋め込み機能](embed.md)を使用して、Laravelアプリをスタンドアロンバイナリとして\n配布することが可能です。\n\nLaravelアプリをLinux用のスタンドアロンバイナリとしてパッケージ化するには、以下の手順に従ってください：\n\n1. アプリのリポジトリに`static-build.Dockerfile`という名前のファイルを作成します：\n\n   ```dockerfile\n   FROM --platform=linux/amd64 dunglas/frankenphp:static-builder-gnu\n   # バイナリをmusl-libcシステムで実行する場合は、static-builder-musl を使用してください\n\n   # アプリをコピー\n   WORKDIR /go/src/app/dist/app\n   COPY . .\n\n   # スペースを節約するためにテストやその他の不要なファイルを削除\n   # 代わりに .dockerignore に記述して除外することも可能\n   RUN rm -Rf tests/\n\n   # .envファイルをコピー\n   RUN cp .env.example .env\n   # APP_ENV と APP_DEBUG を本番用に変更\n   RUN sed -i'' -e 's/^APP_ENV=.*/APP_ENV=production/' -e 's/^APP_DEBUG=.*/APP_DEBUG=false/' .env\n\n   # 必要に応じて .env ファイルにさらに変更を加える\n\n   # 依存関係をインストール\n   RUN composer install --ignore-platform-reqs --no-dev -a\n\n   # 静的バイナリをビルド\n   WORKDIR /go/src/app/\n   RUN EMBED=dist/app/ ./build-static.sh\n   ```\n\n   > [!CAUTION]\n   >\n   > 一部の`.dockerignore`ファイルは\n   > `vendor/`ディレクトリや`.env`ファイルを無視します。ビルド前に`.dockerignore`ファイルを調整または削除してください。\n\n2. ビルドします：\n\n   ```console\n   docker build -t static-laravel-app -f static-build.Dockerfile .\n   ```\n\n3. バイナリを取り出します：\n\n   ```console\n   docker cp $(docker create --name static-laravel-app-tmp static-laravel-app):/go/src/app/dist/frankenphp-linux-x86_64 frankenphp ; docker rm static-laravel-app-tmp\n   ```\n\n4. キャッシュを構築します：\n\n   ```console\n   frankenphp php-cli artisan optimize\n   ```\n\n5. データベースマイグレーションを実行します（ある場合）：\n\n   ```console\n   frankenphp php-cli artisan migrate\n   ```\n\n6. アプリの秘密鍵を生成します：\n\n   ```console\n   frankenphp php-cli artisan key:generate\n   ```\n\n7. サーバーを起動します：\n\n   ```console\n   frankenphp php-server\n   ```\n\nこれで、アプリの準備は完了です！\n\n利用可能なオプションや他のOSでバイナリをビルドする方法については、[アプリケーション埋め込み](embed.md)ドキュメントをご覧ください。\n\n### ストレージパスの変更\n\nLaravelはアップロードされたファイルやキャッシュ、ログなどをデフォルトでアプリケーションの`storage/`ディレクトリに保存します。\nしかし、これは埋め込みアプリケーションには適していません。なぜなら、アプリの新しいバージョンごとに異なる一時ディレクトリに展開されるためです。\n\nこの問題を回避するには、`LARAVEL_STORAGE_PATH`環境変数を設定（例：`.env`ファイル内）するか、 `Illuminate\\Foundation\\Application::useStoragePath()`メソッドを呼び出して、一時ディレクトリの外にある任意のディレクトリを使用してください。\n\n### スタンドアロンバイナリでOctaneを実行する\n\nLaravel Octaneアプリもスタンドアロンバイナリとしてパッケージ化することが可能です！\n\nそのためには、[Octaneを正しくインストール](#laravel-octane)し、[前のセクション](#laravelアプリのスタンドアロンバイナリ化)で説明した手順に従ってください。\n\n次に、Octaneを通じてワーカーモードでFrankenPHPを起動するには、以下を実行してください：\n\n```console\nPATH=\"$PWD:$PATH\" frankenphp php-cli artisan octane:frankenphp\n```\n\n> [!CAUTION]\n>\n> コマンドを動作させるためには、スタンドアロンバイナリのファイル名が**必ず**`frankenphp`でなければなりません。\n> Octaneは`frankenphp`という名前の実行ファイルがパス上に存在することを前提としています。\n"
  },
  {
    "path": "docs/ja/mercure.md",
    "content": "# リアルタイム\n\nFrankenPHPには組み込みの[Mercure](https://mercure.rocks)ハブが付属しています！\nMercureを使用すると、接続されているすべてのデバイスにリアルタイムイベントをプッシュでき、各デバイスは即座にJavaScriptイベントを受信します。\n\nJSライブラリやSDKは必要ありません！\n\n![Mercure](mercure-hub.png)\n\nMercureハブを有効にするには、[Mercureのサイト](https://mercure.rocks/docs/hub/config)で説明されているように`Caddyfile`を更新してください。\n\nMercureハブのパスは`/.well-known/mercure`です。\nFrankenPHPをDocker内で実行している場合、完全な送信URLは`http://php/.well-known/mercure`のようになります。ここでの`php`はFrankenPHPを実行するコンテナの名前です。\n\nコードからMercureの更新をプッシュするには、[Symfony Mercure Component](https://symfony.com/components/Mercure)をお勧めします。なお、Symfonyのフルスタックフレームワークは必要ありません。\n"
  },
  {
    "path": "docs/ja/metrics.md",
    "content": "# メトリクス\n\n[Caddyのメトリクス](https://caddyserver.com/docs/metrics)が有効になっていると、FrankenPHPは以下のメトリクスを公開します：\n\n- `frankenphp_total_threads`: PHPスレッドの総数\n- `frankenphp_busy_threads`: 現在リクエストを処理中のPHPスレッド数。なお、実行中のワーカーは常にスレッドを消費します\n- `frankenphp_queue_depth`: 通常のキューに入っているリクエストの数\n- `frankenphp_total_workers{worker=\"[worker_name]\"}`: ワーカーの総数\n- `frankenphp_busy_workers{worker=\"[worker_name]\"}`: 現在リクエストを処理中のワーカーの数\n- `frankenphp_worker_request_time{worker=\"[worker_name]\"}`: すべてのワーカーがリクエスト処理に費やした時間\n- `frankenphp_worker_request_count{worker=\"[worker_name]\"}`: すべてのワーカーが処理したリクエスト数\n- `frankenphp_ready_workers{worker=\"[worker_name]\"}`: 少なくとも一度は `frankenphp_handle_request` を呼び出したワーカーの数\n- `frankenphp_worker_crashes{worker=\"[worker_name]\"}`: ワーカーが予期せず終了した回数\n- `frankenphp_worker_restarts{worker=\"[worker_name]\"}`: ワーカーが意図的に再起動された回数\n- `frankenphp_worker_queue_depth{worker=\"[worker_name]\"}`: キューに入っているリクエストの数\n\nワーカーメトリクスの`[worker_name]`プレースホルダーは、Caddyfileに指定されたワーカー名に置き換えられます。ワーカー名が指定されていない場合は、ワーカーファイルの絶対パスが使用されます。\n"
  },
  {
    "path": "docs/ja/performance.md",
    "content": "# パフォーマンス\n\nデフォルトでは、FrankenPHPはパフォーマンスと使いやすさのバランスが取れた構成を提供するよう設計されています。\nただし、適切な設定により、パフォーマンスを大幅に向上させることが可能です。\n\n## スレッド数とワーカー数\n\nデフォルトでは、FrankenPHPは利用可能なCPU数の2倍のスレッドとワーカー（ワーカーモードで）を開始します。\n\n適切な値は、アプリケーションの書き方、機能、ハードウェアに大きく依存します。\nこれらの値を調整することを強く推奨します。最適なシステムの安定性のためには、`num_threads` x `memory_limit` < `available_memory`とすることをお勧めします。\n\n適切な値を見つけるには、実際のトラフィックをシミュレートした負荷テストを実行するのが最も効果的です。\nそのためのツールとして、[k6](https://k6.io)や[Gatling](https://gatling.io)が有用です。\n\nスレッド数を設定するには、`php_server`や`php`ディレクティブ内の`num_threads`オプションを使用してください。\nワーカー数を変更するには、`frankenphp`ディレクティブ内の`worker`セクションにある`num`オプションを使用してください。\n\n### `max_threads`\n\n実際のトラフィックがどのようなものになるかを正確に把握できれば理想ですが、現実のアプリケーションでは\n予測困難な挙動が多いものです。`max_threads`[設定](config.md#caddyfile-config) により、FrankenPHPは指定された制限まで実行時に追加スレッドを自動的に生成できます。\n`max_threads`はトラフィックを処理するために必要なスレッド数を把握するのに役立ち、レイテンシのスパイクに対してサーバーをより回復力のあるものにできます。\n`auto`に設定すると、制限は`php.ini`の`memory_limit`に基づいて推定されます。推定できない場合、\n`auto`は代わりに`num_threads`の2倍がデフォルトになります。`auto`は必要なスレッド数を大幅に過小評価する可能性があることに留意してください。\n`max_threads`はPHP FPMの[pm.max_children](https://www.php.net/manual/en/install.fpm.configuration.php#pm.max-children)に似ています。主な違いは、FrankenPHPがプロセスではなくスレッドを使用し、\n必要に応じて異なるワーカースクリプトと「クラシックモード」間で自動的に委譲することです。\n\n## ワーカーモード\n\n[ワーカーモード](worker.md)を有効にするとパフォーマンスが劇的に向上しますが、\nアプリがこのモードと互換性があるように適応する必要があります：\nワーカースクリプトを作成し、アプリがメモリリークしていないことを確認する必要があります。\n\n## muslを使用しない\n\n公式Dockerイメージと私たちが提供するデフォルトバイナリのAlpine Linuxバリアントは、[musl libc](https://musl.libc.org)を使用しています。\n\nPHPは、従来のGNUライブラリの代わりにこの代替Cライブラリを使用すると[遅くなる](https://gitlab.alpinelinux.org/alpine/aports/-/issues/14381)ことが知られており、\n特にFrankenPHPに必要なZTSモード（スレッドセーフ）でコンパイルされた場合です。高度にスレッド化された環境では、差が大きくなる可能性があります。\n\nまた、[一部のバグはmuslを使用した場合にのみ発生します](https://github.com/php/php-src/issues?q=sort%3Aupdated-desc+is%3Aissue+is%3Aopen+label%3ABug+musl)。\n\n本番環境では、glibcにリンクされたFrankenPHPを使用することをお勧めします。\n\nこれは、Debian Dockerイメージを使用するか、[公式パッケージ (.deb, .rpm, .apk)](https://pkgs.henderkes.com) を使用するか、あるいは[FrankenPHPをソースからコンパイル](compile.md)することで実現できます。\n\nより軽量で安全なコンテナのためには、Alpineよりも[強化されたDebianイメージ](docker.md#hardening-images)を検討することをお勧めします。\n\n## Go Runtime設定\n\nFrankenPHPはGoで書かれています。\n\n一般的に、Go runtimeは特別な設定を必要としませんが、特定の状況では、\n特定の設定でパフォーマンスが向上する場合があります。\n\nおそらく`GODEBUG`環境変数を`cgocheck=0`に設定したいでしょう（FrankenPHP Dockerイメージのデフォルト）。\n\nFrankenPHPをコンテナ（Docker、Kubernetes、LXC...）で実行しており、コンテナで利用可能なメモリを制限している場合は、\n`GOMEMLIMIT`環境変数に利用可能なメモリ量を設定してください。\n\n詳細については、Go ランタイムを最大限に活用するために、[この主題に特化したGoドキュメントページ](https://pkg.go.dev/runtime#hdr-Environment_Variables)を読むことを強く推奨します。\n\n## `file_server`\n\nデフォルトでは、`php_server`ディレクティブは自動的にファイルサーバーを設定して\nルートディレクトリに保存された静的ファイル（アセット）を配信します。\n\nこの機能は便利ですが、コストがかかります。\n無効にするには、以下の設定を使用してください：\n\n```caddyfile\nphp_server {\n    file_server off\n}\n```\n\n## `try_files`\n\n`php_server`は、静的ファイルとPHPファイルに加えて、アプリケーションのインデックスファイル\nおよびディレクトリインデックスファイル（`/path/` -> `/path/index.php`）も試行します。ディレクトリインデックスが不要な場合、\n次のように`try_files`を明示的に定義して無効にできます：\n\n```caddyfile\nphp_server {\n    try_files {path} index.php\n    root /root/to/your/app # ここで root を明示的に追加すると、キャッシュの効率が向上します\n}\n```\n\nこれにより、不要なファイルの操作の回数を大幅に削減できます。\n上記の構成をワーカーモードに相当させると、次のようになります:\n\n```caddyfile\nroute {\n    php_server { # file server が全く不要な場合は \"php_server\" の代わりに \"php\" を使用します\n        root /root/to/your/app\n        worker /path/to/worker.php {\n            match * # すべてのリクエストを直接ワーカーに送信します\n        }\n    }\n}\n```\n\nファイルシステムへの不要な操作を完全にゼロにする代替アプローチとして、`php`ディレクティブを使用し、\nパスによってPHPファイルとそれ以外を分ける方法があります。アプリケーション全体が1つのエントリーファイルで提供される場合、この方法は有効です。\nたとえば`/assets`フォルダの背後で静的ファイルを提供する[設定](config.md#caddyfile-config)は次のようになります：\n\n```caddyfile\nroute {\n    @assets {\n        path /assets/*\n    }\n\n    # /assets 以下のリクエストはファイルサーバーが処理する\n    file_server @assets {\n        root /root/to/your/app\n    }\n\n    # /assets 以外のすべてのリクエストは index または worker の PHP ファイルで処理する\n    rewrite index.php\n    php {\n        root /root/to/your/app # ここで root を明示的に追加すると、キャッシュの効率が向上します\n    }\n}\n```\n\n## プレースホルダー\n\n`root`および`env`ディレクティブ内では、[プレースホルダー](https://caddyserver.com/docs/conventions#placeholders)を使用できます。\nただし、これによりこれらの値をキャッシュすることができなくなり、大幅なパフォーマンスコストが発生します。\n\n可能であれば、これらのディレクティブではプレースホルダーの使用を避けてください。\n\n## `resolve_root_symlink`\n\nデフォルトでは、ドキュメントルートがシンボリックリンクである場合、FrankenPHP はそれを自動的に解決します（これは PHP が正しく動作するために必要です）。\nドキュメントルートがシンボリックリンクでない場合、この機能を無効にできます。\n\n```caddyfile\nphp_server {\n    resolve_root_symlink false\n}\n```\n\nこの設定は、`root`ディレクティブに[プレースホルダー](https://caddyserver.com/docs/conventions#placeholders)が含まれている場合にパフォーマンスを向上させます。\nそれ以外の場合の効果はごくわずかです。\n\n## ログ\n\nログ出力は当然ながら非常に有用ですが、その性質上、\nI/O操作およびメモリ確保が必要となり、パフォーマンスを大幅に低下させます。\n[ログレベルを正しく設定](https://caddyserver.com/docs/caddyfile/options#log)し、\n必要なもののみをログに記録するようにしてください。\n\n## PHPパフォーマンス\n\nFrankenPHPは公式のPHPインタープリターを使用しています。\n通常のPHPに関するパフォーマンス最適化はすべてFrankenPHPでも有効です。\n\n特に以下の点を確認してください：\n\n- [OPcache](https://www.php.net/manual/en/book.opcache.php)がインストールされ、有効化され、適切に設定されていること\n- [Composer autoloader optimizations](https://getcomposer.org/doc/articles/autoloader-optimization.md)を有効にすること\n- `realpath`キャッシュがアプリケーションのニーズに合わせて十分な大きさであること\n- [preloading](https://www.php.net/manual/en/opcache.preloading.php)を使用すること\n\n詳細については、[Symfonyの専用ドキュメントエントリ](https://symfony.com/doc/current/performance.html)をお読みください\n（Symfonyを使用していなくても、多くのヒントが役立ちます）。\n\n## スレッドプールの分割\n\nアプリケーションが、高負荷時に不安定になったり、常に10秒以上応答にかかるAPIのような遅い外部サービスと連携することはよくあります。\nこのような場合、スレッドプールを分割して専用の「遅い」プールを持つことが有益です。\nこれにより、遅いエンドポイントがすべてのサーバーリソース/スレッドを消費するのを防ぎ、コネクションプールと同様に、遅いエンドポイントへのリクエストの同時実行数を制限できます。\n\n```caddyfile\nexample.com {\n    php_server {\n        root /app/public # アプリケーションのルート\n        worker index.php {\n            match /slow-endpoint/* # パスが /slow-endpoint/* のすべてリクエストはこのスレッドプールによって処理されます\n            num 1 # /slow-endpoint/* に一致するリクエストに対しては最低1スレッド\n            max_threads 20 # 必要に応じて、/slow-endpoint/* に一致するリクエストに対して最大20スレッドまで許可します\n        }\n        worker index.php {\n            match * # 他のすべてのリクエストは個別に処理されます\n            num 1 # 遅いエンドポイントがハングし始めても、他のリクエストには最低1スレッド\n            max_threads 20 # 必要に応じて、他のリクエストに対して最大20スレッドまで許可します\n        }\n    }\n}\n```\n\n一般的に、メッセージキューなどの適切なメカニズムを使用して、非常に遅いエンドポイントを非同期的に処理することも推奨されます。\n"
  },
  {
    "path": "docs/ja/production.md",
    "content": "# 本番環境でのデプロイ\n\nこのチュートリアルでは、Docker Composeを使用して単一サーバーにPHPアプリケーションをデプロイする方法を学びます。\n\nSymfonyを使用している場合は、Symfony Dockerプロジェクトの「[本番環境へのデプロイ](https://github.com/dunglas/symfony-docker/blob/main/docs/production.md)」ドキュメントを参照してください。\n\nAPI Platformを使用している場合は、[フレームワークのデプロイドキュメント](https://api-platform.com/docs/deployment/)を参照してください。\n\n## アプリの準備\n\nまず、PHPプロジェクトのルートディレクトリに`Dockerfile`を作成します：\n\n```dockerfile\nFROM dunglas/frankenphp\n\n# \"your-domain-name.example.com\" を実際のドメイン名に置き換えてください\nENV SERVER_NAME=your-domain-name.example.com\n# HTTPSを無効にしたい場合は、次の値を代わりに使用してください：\n#ENV SERVER_NAME=:80\n\n# プロジェクトで \"public\" ディレクトリをWebルートとして使用していない場合、ここで設定できます:\n# ENV SERVER_ROOT=web/\n\n# PHPの本番設定を有効化\nRUN mv \"$PHP_INI_DIR/php.ini-production\" \"$PHP_INI_DIR/php.ini\"\n\n# プロジェクトのPHPファイルをpublicディレクトリにコピー\nCOPY . /app/public\n# Symfony や Laravel を使用している場合は、代わりにプロジェクト全体をコピーする必要があります：\n#COPY . /app\n```\n\nより詳細な情報やカスタマイズ方法、PHP拡張モジュールやCaddyモジュールのインストール方法については、\n「[カスタムDockerイメージのビルド](docker.md)」を参照してください。\n\nプロジェクトでComposerを使用している場合は、\nDockerイメージにComposerを含め、依存関係をインストールしてください。\n\n次に、 `compose.yaml` ファイルを追加します：\n\n```yaml\nservices:\n  php:\n    image: dunglas/frankenphp\n    restart: always\n    ports:\n      - \"80:80\" # HTTP\n      - \"443:443\" # HTTPS\n      - \"443:443/udp\" # HTTP/3\n    volumes:\n      - caddy_data:/data\n      - caddy_config:/config\n\n# Caddyの証明書と設定に必要なボリューム\nvolumes:\n  caddy_data:\n  caddy_config:\n```\n\n> [!NOTE]\n>\n> 上記の例は本番環境向けです。\n> 開発環境では、ボリューム、異なるPHP設定、`SERVER_NAME`環境変数の異なる値を使用したい場合があります。\n>\n> [Symfony Docker](https://github.com/dunglas/symfony-docker)プロジェクト（FrankenPHPを使用）では、\n> マルチステージイメージ、Composer、追加のPHP拡張モジュールなどを活用した、\n> より高度な例を見ることができます。\n\n最後に、Gitを使用している場合は、これらのファイルをコミットしてプッシュします。\n\n## サーバーの準備\n\n本番環境にアプリケーションをデプロイするには、サーバーが必要です。\nこのチュートリアルではDigitalOceanの仮想マシンを使用しますが、他のLinuxサーバーでも同様に動作します。\nDockerがインストールされたLinuxサーバーが既にある場合は、[次のセクション](#ドメイン名の設定)に進んでください。\n\nまだサーバーがない場合は、[このアフィリエイトリンク](https://m.do.co/c/5d8aabe3ab80)を使用して$200の無料クレジットを取得し、アカウントを作成してください。その後、「Create a Droplet」をクリックします。\n次に、「Choose an image」セクションの下の「Marketplace」タブをクリックし、「Docker」という名前のアプリを検索します。\nこれにより、DockerとDocker Composeの最新バージョンが既にインストールされたUbuntuサーバーがプロビジョニングされます！\n\nテスト目的であれば、最安のプランで十分です。\n実際の本番使用では、おそらくニーズに合わせて「general purpose」セクションのプランを選びたいでしょう。\n\n![FrankenPHPをDockerでDigitalOceanにデプロイ](digitalocean-droplet.png)\n\n他の設定はデフォルトのままにするか、必要に応じて調整も可能です。\nSSHキーを追加するかパスワードを作成することを忘れずに行い、「Finalize and create」ボタンをクリックしてください。\n\n次に、Dropletがプロビジョニングされるまで数秒待ちます。\nDropletの準備ができたら、SSHを使用して接続します：\n\n```console\nssh root@<droplet-ip>\n```\n\n## ドメイン名の設定\n\nほとんどの場合、サイトにドメイン名を関連付けたいでしょう。\nまだドメイン名を所有していない場合は、レジストラーを通じて購入する必要があります。\n\n次に、サーバーのIPアドレスを指すドメイン名のタイプ`A`のDNSレコードを作成します：\n\n```dns\nyour-domain-name.example.com.  IN  A     207.154.233.113\n```\n\nDigitalOceanのドメインサービス（「Networking」 > 「Domains」）での例：\n\n![DigitalOceanでのDNS設定](digitalocean-dns.png)\n\n> [!NOTE]\n>\n> FrankenPHPがデフォルトで使用しているTLS証明書の自動生成サービスLet's Encryptは、IPアドレスの直接使用をサポートしていません。Let's Encryptを使用するにはドメイン名の使用が必須です。\n\n## デプロイ\n\n`git clone`や`scp`など、目的に合ったツールを使用してプロジェクトをサーバーにコピーします。\nGitHubを使用している場合は、[deploy key](https://docs.github.com/en/free-pro-team@latest/developers/overview/managing-deploy-keys#deploy-keys)の使用を検討してください。\ndeploy keyは[GitLabでもサポートされています](https://docs.gitlab.com/ee/user/project/deploy_keys/)。\n\nGitでの例：\n\n```console\ngit clone git@github.com:<username>/<project-name>.git\n```\n\nプロジェクトディレクトリ（`<project-name>`）に移動し、本番モードでアプリを開始します：\n\n```console\ndocker compose up --wait\n```\n\nサーバーが起動し、HTTPS証明書が自動的に生成されます。\n`https://your-domain-name.example.com`にアクセスしてお楽しみください！\n\n> [!CAUTION]\n>\n> Dockerはキャッシュレイヤーを持つ可能性があるため、各デプロイメントで正しいビルドを持っているか確認するか、キャッシュの問題を避けるために`--no-cache`オプションでプロジェクトを再ビルドしてください。\n\n## 複数ノードへのデプロイ\n\n複数のマシンクラスターにアプリをデプロイしたい場合は、提供されるComposeファイルと互換性のある[Docker Swarm](https://docs.docker.com/engine/swarm/stack-deploy/)を\n使用できます。\nKubernetesでデプロイするには、FrankenPHPを使用する[API Platformで提供されるHelmチャート](https://api-platform.com/docs/deployment/kubernetes/)をご覧ください。\n"
  },
  {
    "path": "docs/ja/static.md",
    "content": "# 静的ビルドの作成\n\nPHPライブラリのローカルインストールを使用する代わりに、\n[static-php-cli プロジェクト](https://github.com/crazywhalecc/static-php-cli)を利用して、FrankenPHPの静的またはほぼ静的なビルドを作成することが可能です（プロジェクト名に「CLI」とありますが、CLIだけでなく全てのSAPIをサポートしています）。\n\nこの方法を使えば、PHPインタープリター、Caddy Webサーバー、FrankenPHPをすべて含んだ単一でポータブルなバイナリを作成できます！\n\n完全に静的なネイティブ実行ファイルは依存関係を全く必要とせず、[`scratch` Dockerイメージ](https://docs.docker.com/build/building/base-images/#create-a-minimal-base-image-using-scratch)上でも実行可能です。\nただし、動的PHP拡張モジュール（Xdebugなど）をロードできず、musl libcを使用しているため、いくつかの制限があります。\n\nほぼ静的なバイナリは`glibc`のみを必要とし、動的拡張モジュールをロードできます。\n\n可能であれば、glibcベースのほぼ静的ビルドの使用をお勧めします。\n\nまた、FrankenPHPは[静的バイナリへのPHPアプリの埋め込み](embed.md)もサポートしています。\n\n## Linux\n\n静的なLinuxバイナリをビルドするためのDockerイメージを提供しています：\n\n### muslベースの完全静的ビルド\n\n依存関係なしにあらゆるLinuxディストリビューションで動作する完全静的バイナリ（ただし拡張モジュールの動的ロードはサポートしない）を作成するには、以下を実行します：\n\n```console\ndocker buildx bake --load static-builder-musl\ndocker cp $(docker create --name static-builder-musl dunglas/frankenphp:static-builder-musl):/go/src/app/dist/frankenphp-linux-$(uname -m) frankenphp ; docker rm static-builder-musl\n```\n\n高い並行性が求められるシナリオでは、より良いパフォーマンスのため、[mimalloc](https://github.com/microsoft/mimalloc)アロケーターの使用を検討してください。\n\n```console\ndocker buildx bake --load --set static-builder-musl.args.MIMALLOC=1 static-builder-musl\n```\n\n### glibcベースのほぼ静的なビルド（動的拡張モジュールのサポートあり）\n\n選択した拡張モジュールを静的にコンパイルしながら、さらにPHP拡張モジュールを動的にロードできるバイナリを作成するには、以下を実行します：\n\n```console\ndocker buildx bake --load static-builder-gnu\ndocker cp $(docker create --name static-builder-gnu dunglas/frankenphp:static-builder-gnu):/go/src/app/dist/frankenphp-linux-$(uname -m) frankenphp ; docker rm static-builder-gnu\n```\n\nこのバイナリは、glibcバージョン2.17以上をすべてサポートしますが、muslベースシステム（Alpine Linuxなど）では動作しません。\n\n生成されたほぼ静的（`glibc`を除く）バイナリは`frankenphp`という名前で、カレントディレクトリに出力されます。\n\nDockerを使わずに静的バイナリをビルドしたい場合は、macOS向けの手順を参照してください。これらの手順はLinuxでも使用できます。\n\n### カスタム拡張モジュール\n\nデフォルトでは、よく使われるPHP拡張モジュールがコンパイルされます。\n\nバイナリのサイズを削減したり、攻撃対象領域（アタックサーフェス）を減らすために、`PHP_EXTENSIONS`というDocker引数を使用してビルドする拡張モジュールを明示的に指定できます。\n\n例えば、`opcache`と`pdo_sqlite`拡張モジュールのみをビルドするには、以下のように実行します：\n\n```console\ndocker buildx bake --load --set static-builder-musl.args.PHP_EXTENSIONS=opcache,pdo_sqlite static-builder-musl\n# ...\n```\n\n有効にした拡張に必要なライブラリを追加するには、`PHP_EXTENSION_LIBS`というDocker引数を渡すことができます：\n\n```console\ndocker buildx bake \\\n  --load \\\n  --set static-builder-musl.args.PHP_EXTENSIONS=gd \\\n  --set static-builder-musl.args.PHP_EXTENSION_LIBS=libjpeg,libwebp \\\n  static-builder-musl\n```\n\n### 追加のCaddyモジュール\n\nCaddyの拡張モジュールを追加したい場合は、`XCADDY_ARGS`というDocker引数を使用して、[xcaddy](https://github.com/caddyserver/xcaddy)に渡す引数を以下のように指定できます：\n\n```console\ndocker buildx bake \\\n  --load \\\n  --set static-builder-musl.args.XCADDY_ARGS=\"--with github.com/darkweak/souin/plugins/caddy --with github.com/dunglas/caddy-cbrotli --with github.com/dunglas/mercure/caddy --with github.com/dunglas/vulcain/caddy\" \\\n  static-builder-musl\n```\n\nこの例では、Caddy用の[Souin](https://souin.io)HTTPキャッシュモジュールと[cbrotli](https://github.com/dunglas/caddy-cbrotli)、[Mercure](https://mercure.rocks)、[Vulcain](https://vulcain.rocks)モジュールを追加しています。\n\n> [!TIP]\n>\n> cbrotli、Mercure、Vulcainモジュールは、`XCADDY_ARGS`が空または設定されていない場合はデフォルトで含まれます。\n> `XCADDY_ARGS`の値をカスタマイズする場合、デフォルトのモジュールは含まれなくなるため、必要なものは明示的に記述してください。\n\n[ビルドのカスタマイズ](#ビルドのカスタマイズ)も参照してください\n\n### GitHubトークン\n\nGitHub API レート制限に達した場合は、`GITHUB_TOKEN`という名前の環境変数にGitHub Personal Access Tokenを設定してください：\n\n```console\nGITHUB_TOKEN=\"xxx\" docker --load buildx bake static-builder-musl\n# ...\n```\n\n## macOS\n\nmacOS用の静的バイナリを作成するには以下のスクリプトを実行してください（[Homebrew](https://brew.sh/)がインストールされている必要があります）：\n\n```console\ngit clone https://github.com/php/frankenphp\ncd frankenphp\n./build-static.sh\n```\n\nなお、このスクリプトはLinux（おそらく他のUnix系OS）でも動作し、私たちが提供するDockerイメージ内部でも使用されています。\n\n## ビルドのカスタマイズ\n\n以下の環境変数を`docker build`や`build-static.sh`\nスクリプトに渡すことで、静的ビルドをカスタマイズできます：\n\n- `FRANKENPHP_VERSION`: 使用するFrankenPHPのバージョン\n- `PHP_VERSION`: 使用するPHPのバージョン\n- `PHP_EXTENSIONS`: ビルドするPHP拡張（[サポートされる拡張のリスト](https://static-php.dev/en/guide/extensions.html)）\n- `PHP_EXTENSION_LIBS`: 拡張モジュールに追加機能を持たせるためにビルドする追加ライブラリ\n- `XCADDY_ARGS`: 追加のCaddyモジュールを導入するなど[xcaddy](https://github.com/caddyserver/xcaddy)に渡す引数\n- `EMBED`: バイナリに埋め込むPHPアプリケーションのパス\n- `CLEAN`: 指定するとlibphpおよびそのすべての依存関係がスクラッチからビルドされます（キャッシュなし）\n- `NO_COMPRESS`: UPXを使用して結果のバイナリを圧縮しない\n- `DEBUG_SYMBOLS`: 指定すると、デバッグシンボルが除去されず、バイナリに含まれます\n- `MIMALLOC`: （実験的、Linuxのみ）パフォーマンス向上のためにmuslのmallocngを[mimalloc](https://github.com/microsoft/mimalloc)に置き換えます。muslをターゲットとするビルドにのみこれを使用することをお勧めします。glibcの場合は、このオプションを無効にして、代わりにバイナリを実行する際に[`LD_PRELOAD`](https://microsoft.github.io/mimalloc/overrides.html)を使用することをお勧めします。\n- `RELEASE`: （メンテナー用）指定すると、生成されたバイナリがGitHubにアップロードされます\n\n## 拡張モジュール\n\nglibcまたはmacOSベースのバイナリでは、PHP拡張モジュールを動的にロードできます。ただし、これらの拡張はZTSサポートでコンパイルされている必要があります。\nほとんどのパッケージマネージャーは現在、拡張のZTSバージョンを提供していないため、自分でコンパイルする必要があります。\n\nこのために、`static-builder-gnu`Dockerコンテナをビルドして実行し、リモートでアクセスし、`./configure --with-php-config=/go/src/app/dist/static-php-cli/buildroot/bin/php-config`で拡張をコンパイルできます。\n\n[Xdebug拡張モジュール](https://xdebug.org)の場合：\n\n```console\ndocker build -t gnu-ext -f static-builder-gnu.Dockerfile --build-arg FRANKENPHP_VERSION=1.0 .\ndocker create --name static-builder-gnu -it gnu-ext /bin/sh\ndocker start static-builder-gnu\ndocker exec -it static-builder-gnu /bin/sh\ncd /go/src/app/dist/static-php-cli/buildroot/bin\ngit clone https://github.com/xdebug/xdebug.git && cd xdebug\nsource scl_source enable devtoolset-10\n../phpize\n./configure --with-php-config=/go/src/app/dist/static-php-cli/buildroot/bin/php-config\nmake\nexit\ndocker cp static-builder-gnu:/go/src/app/dist/static-php-cli/buildroot/bin/xdebug/modules/xdebug.so xdebug-zts.so\ndocker cp static-builder-gnu:/go/src/app/dist/frankenphp-linux-$(uname -m) ./frankenphp\ndocker stop static-builder-gnu\ndocker rm static-builder-gnu\ndocker rmi gnu-ext\n```\n\nこれにより、現在のディレクトリに`frankenphp`と`xdebug-zts.so`が作成されます。\n`xdebug-zts.so`を拡張ディレクトリに移動し、php.iniに`zend_extension=xdebug-zts.so`を追加してFrankenPHPを実行すると、Xdebugがロードされます。\n"
  },
  {
    "path": "docs/ja/worker.md",
    "content": "# FrankenPHPワーカーの使用\n\nアプリケーションを一度起動してメモリに保持します。\nFrankenPHPは数ミリ秒で受信リクエストを処理します。\n\n## ワーカースクリプトの開始\n\n### Docker\n\n`FRANKENPHP_CONFIG`環境変数の値を`worker /path/to/your/worker/script.php`に設定します：\n\n```console\ndocker run \\\n    -e FRANKENPHP_CONFIG=\"worker /app/path/to/your/worker/script.php\" \\\n    -v $PWD:/app \\\n    -p 80:80 -p 443:443 -p 443:443/udp \\\n    dunglas/frankenphp\n```\n\n### スタンドアロンバイナリ\n\n`php-server`コマンドの`--worker`オプションを使って、現在のディレクトリのコンテンツをワーカーを通じて提供できます：\n\n```console\nfrankenphp php-server --worker /path/to/your/worker/script.php\n```\n\nPHPアプリが[バイナリに埋め込まれている](embed.md)場合は、アプリのルートディレクトリにカスタムの`Caddyfile`を追加することができます。\nこれが自動的に使用されます。\n\nまた、`--watch`オプションを使えば、[ファイルの変更に応じてワーカーを再起動](config.md#watching-for-file-changes)することも可能です。\n以下のコマンドは、`/path/to/your/app/`ディレクトリおよびそのサブディレクトリ内の`.php`で終わるファイルが変更された場合に再起動をトリガーします：\n\n```console\nfrankenphp php-server --worker /path/to/your/worker/script.php --watch=\"/path/to/your/app/**/*.php\"\n```\n\nこの機能は、[ホットリロード](hot-reload.md)と組み合わせてよく使用されます。\n\n## Symfonyランタイム\n\n> [!TIP]\n> 以下のセクションは、FrankenPHPワーカーモードのネイティブサポートが導入されたSymfony 7.4より前のバージョンでのみ必要です。\n\nFrankenPHPのワーカーモードは[Symfony Runtime Component](https://symfony.com/doc/current/components/runtime.html)によってサポートされています。\nワーカーでSymfonyアプリケーションを開始するには、FrankenPHP用の[PHP Runtime](https://github.com/php-runtime/runtime)パッケージをインストールします：\n\n```console\ncomposer require runtime/frankenphp-symfony\n```\n\nアプリケーションサーバーを起動するには、FrankenPHP Symfony Runtimeを使用するように`APP_RUNTIME`環境変数を定義します：\n\n```console\ndocker run \\\n    -e FRANKENPHP_CONFIG=\"worker ./public/index.php\" \\\n    -e APP_RUNTIME=Runtime\\\\FrankenPhpSymfony\\\\Runtime \\\n    -v $PWD:/app \\\n    -p 80:80 -p 443:443 -p 443:443/udp \\\n    dunglas/frankenphp\n```\n\n## Laravel Octane\n\n[専用ドキュメント](laravel.md#laravel-octane)を参照してください。\n\n## カスタムアプリ\n\n以下の例は、サードパーティライブラリに依存せずに独自のワーカースクリプトを作成する方法を示しています：\n\n```php\n<?php\n// public/index.php\n\n// アプリケーションを起動\nrequire __DIR__.'/vendor/autoload.php';\n\n$myApp = new \\App\\Kernel();\n$myApp->boot();\n\n// パフォーマンス向上のため、ループの外側にハンドラーを配置（処理を減らす）\n$handler = static function () use ($myApp) {\n    try {\n        // リクエストを受信すると呼び出され、\n        // スーパーグローバル、php://inputなどがリセットされます。\n        echo $myApp->handle($_GET, $_POST, $_COOKIE, $_FILES, $_SERVER);\n    } catch (\\Throwable $exception) {\n        // `set_exception_handler`はワーカースクリプトが終了するときにのみ呼び出されるため、\n        // 予期しない動作になる可能性があります。そのため、ここで例外をキャッチして処理します。\n        (new \\MyCustomExceptionHandler)->handleException($exception);\n    }\n};\n\n$maxRequests = (int)($_SERVER['MAX_REQUESTS'] ?? 0);\nfor ($nbRequests = 0; !$maxRequests || $nbRequests < $maxRequests; ++$nbRequests) {\n    $keepRunning = \\frankenphp_handle_request($handler);\n\n    // HTTPレスポンス送信後に何らかの処理を実行\n    $myApp->terminate();\n\n    // ページ生成中にガベージコレクタが起動する可能性を減らすため、ここでガベージコレクタを明示的に呼び出す\n    gc_collect_cycles();\n\n    if (!$keepRunning) break;\n}\n\n// クリーンアップ\n$myApp->shutdown();\n```\n\n次に、アプリを開始し、`FRANKENPHP_CONFIG`環境変数を使用してワーカーを設定します：\n\n```console\ndocker run \\\n    -e FRANKENPHP_CONFIG=\"worker ./public/index.php\" \\\n    -v $PWD:/app \\\n    -p 80:80 -p 443:443 -p 443:443/udp \\\n    dunglas/frankenphp\n```\n\nデフォルトでは、CPU当たり2つのワーカーが開始されます。\n開始するワーカー数を設定することもできます：\n\n```console\ndocker run \\\n    -e FRANKENPHP_CONFIG=\"worker ./public/index.php 42\" \\\n    -v $PWD:/app \\\n    -p 80:80 -p 443:443 -p 443:443/udp \\\n    dunglas/frankenphp\n```\n\n### 一定数のリクエスト処理後にワーカーを再起動する\n\nPHPはもともと長時間実行されるプロセス向けに設計されていなかったため、メモリリークを引き起こすライブラリやレガシーコードがいまだに多く存在します。\nこうしたコードをワーカーモードで利用するための回避策として、一定数のリクエストを処理した後にワーカースクリプトを再起動する方法があります：\n\n前述のワーカー用スニペットでは、`MAX_REQUESTS`という名前の環境変数を設定することで、処理する最大リクエスト数を設定できます。\n\n### ワーカーの手動再起動\n\n[ファイルの変更を監視](config.md#watching-for-file-changes)してワーカーを再起動することも可能ですが、\n[Caddy admin API](https://caddyserver.com/docs/api)を使用してすべてのワーカーをグレースフルに（安全に）再起動することも可能です。adminが\n[Caddyfile](config.md#caddyfile-config)で有効になっている場合、次のような単純なPOSTリクエストで再起動エンドポイントにpingできます：\n\n```console\ncurl -X POST http://localhost:2019/frankenphp/workers/restart\n```\n\n### ワーカーの失敗\n\nワーカースクリプトがゼロ以外の終了コードでクラッシュした場合、FrankenPHP は指数的バックオフ戦略を用いて再起動を行います。\nワーカースクリプトが最後のバックオフ時間 × 2 より長く稼働し続けた場合、\nそれ以降の再起動ではペナルティを科しません。\nしかし、スクリプトにタイプミスがあるなど短時間で何度もゼロ以外の終了コードで失敗し続ける場合、\nFrankenPHP は`too many consecutive failures`というエラーとともにクラッシュします。\n\n連続失敗の回数上限は、[Caddyfile](config.md#caddyfile-config)の`max_consecutive_failures`オプションで設定できます:\n\n```caddyfile\nfrankenphp {\n    worker {\n        # ...\n        max_consecutive_failures 10\n    }\n}\n```\n\n## スーパーグローバルの動作\n\n[PHPのスーパーグローバル](https://www.php.net/manual/en/language.variables.superglobals.php)（`$_SERVER`、`$_ENV`、`$_GET`など）\nは以下のように動作します：\n\n- `frankenphp_handle_request()`が最初に呼び出される前は、スーパーグローバルにはワーカースクリプト自体にバインドされた値が格納されています\n- `frankenphp_handle_request()`の呼び出し中および呼び出し後は、スーパーグローバルには処理されたHTTPリクエストから生成された値が格納され、`frankenphp_handle_request()`を呼び出すたびにスーパーグローバルの値が変更されます\n\nコールバック内でワーカースクリプトのスーパーグローバルにアクセスするには、それらをコピーしてコールバックのスコープにコピーをインポートする必要があります：\n\n```php\n<?php\n// frankenphp_handle_request()を最初に呼び出す前に、ワーカーの $_SERVER スーパーグローバルをコピー\n$workerServer = $_SERVER;\n\n$handler = static function () use ($workerServer) {\n    var_dump($_SERVER); // リクエストにバインドされた $_SERVER\n    var_dump($workerServer); // ワーカースクリプトの $_SERVER\n};\n\n// ...\n"
  },
  {
    "path": "docs/ja/x-sendfile.md",
    "content": "# 大きな静的ファイルを効率的に配信する （`X-Sendfile`/`X-Accel-Redirect`）\n\n通常、静的ファイルはウェブサーバーによって直接配信されますが、\n時にはファイルを送信する前にPHPコードを実行する必要があります。\n例えば、アクセス制御、統計、カスタムHTTPヘッダーなど\n\n残念ながら、PHPを使用して大きな静的ファイルを配信することは、\nウェブサーバーを直接使うより非効率的です（メモリ過負荷、パフォーマンス低下など）。\n\nFrankenPHPでは、カスタマイズされたPHPコードを実行した**後**に、\n静的ファイルの送信をウェブサーバーに委譲できます。\n\nこの機能を使うには、PHPアプリケーションは提供するファイルのパスを含む\nカスタムHTTPヘッダーを定義するだけです。残りの処理はFrankenPHPが行います。\n\nこの機能は、Apacheでは **`X-Sendfile`** 、NGINXでは **`X-Accel-Redirect`** として知られています。\n\n以下の例では、プロジェクトのドキュメントルートが`public/`ディレクトリであり、\n`public/`ディレクトリの外部に保存されたファイルを\n`private-files/`ディレクトリからPHPで提供したいと仮定します。\n\n## 設定方法\n\nまず、この機能を有効にするために以下の設定を`Caddyfile`に追加します：\n\n```patch\n\troot public/\n\t# ...\n\n+\t# Symfony や Laravel など、Symfony HttpFoundation コンポーネントを使用するプロジェクトに必要\n+\trequest_header X-Sendfile-Type x-accel-redirect\n+\trequest_header X-Accel-Mapping ../private-files=/private-files\n+\n+\tintercept {\n+\t\t@accel header X-Accel-Redirect *\n+\t\thandle_response @accel {\n+\t\t\troot private-files/\n+\t\t\trewrite * {resp.header.X-Accel-Redirect}\n+\t\t\tmethod * GET\n+\n+\t\t\t# セキュリティ強化のため、 PHP によって設定された X-Accel-Redirect ヘッダーを削除\n+\t\t\theader -X-Accel-Redirect\n+\n+\t\t\tfile_server\n+\t\t}\n+\t}\n\n\tphp_server\n```\n\n## プレーンなPHPの場合\n\n`private-files/`からの相対パスを`X-Accel-Redirect`ヘッダーの値として設定します：\n\n```php\nheader('X-Accel-Redirect: file.txt');\n```\n\n## Symfony HttpFoundationコンポーネントを使用するプロジェクトの場合（Symfony、Laravel、Drupalなど）\n\nSymfonyのHttpFoundationは[この機能をネイティブサポート](https://symfony.com/doc/current/components/http_foundation.html#serving-files)しており、\n`X-Accel-Redirect`ヘッダーの正しい値を自動的に決定してレスポンスに追加します。\n\n```php\nuse Symfony\\Component\\HttpFoundation\\BinaryFileResponse;\n\nBinaryFileResponse::trustXSendfileTypeHeader();\n$response = new BinaryFileResponse(__DIR__.'/../private-files/file.txt');\n\n// ...\n```\n"
  },
  {
    "path": "docs/known-issues.md",
    "content": "# Known Issues\n\n## Unsupported PHP Extensions\n\nThe following extensions are known not to be compatible with FrankenPHP:\n\n| Name                                                                                                        | Reason          | Alternatives                                                                                                         |\n| ----------------------------------------------------------------------------------------------------------- | --------------- | -------------------------------------------------------------------------------------------------------------------- |\n| [imap](https://www.php.net/manual/en/imap.installation.php)                                                 | Not thread-safe | [javanile/php-imap2](https://github.com/javanile/php-imap2), [webklex/php-imap](https://github.com/Webklex/php-imap) |\n| [newrelic](https://docs.newrelic.com/docs/apm/agents/php-agent/getting-started/introduction-new-relic-php/) | Not thread-safe | -                                                                                                                    |\n\n## Buggy PHP Extensions\n\nThe following extensions have known bugs and unexpected behaviors when used with FrankenPHP:\n\n| Name                                                          | Problem                                                                                                                                                                                                                   |\n| ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [ext-openssl](https://www.php.net/manual/en/book.openssl.php) | When using musl libc, the OpenSSL extension may crash under heavy loads. The problem doesn't occur when using the more popular GNU libc. This bug is [being tracked by PHP](https://github.com/php/php-src/issues/13648). |\n\n## get_browser\n\nThe [get_browser()](https://www.php.net/manual/en/function.get-browser.php) function seems to perform badly after a while. A workaround is to cache (e.g. with [APCu](https://www.php.net/manual/en/book.apcu.php)) the results per User Agent, as they are static.\n\n## Standalone Binary and Alpine-based Docker Images\n\nThe fully binary and Alpine-based Docker images (`dunglas/frankenphp:*-alpine`) use [musl libc](https://musl.libc.org/) instead of [glibc and friends](https://www.etalabs.net/compare_libcs.html), to keep a smaller binary size.\nThis may lead to some compatibility issues.\nIn particular, the glob flag `GLOB_BRACE` is [not available](https://www.php.net/manual/en/function.glob.php)\n\nPrefer using the GNU variant of the static binary and Debian-based Docker images if you encounter issues.\n\n## Using `https://127.0.0.1` with Docker\n\nBy default, FrankenPHP generates a TLS certificate for `localhost`.\nIt's the easiest and recommended option for local development.\n\nIf you really want to use `127.0.0.1` as a host instead, it's possible to configure it to generate a certificate for it by setting the server name to `127.0.0.1`.\n\nUnfortunately, this is not enough when using Docker because of [its networking system](https://docs.docker.com/network/).\nYou will get a TLS error similar to `curl: (35) LibreSSL/3.3.6: error:1404B438:SSL routines:ST_CONNECT:tlsv1 alert internal error`.\n\nIf you're using Linux, a solution is to use [the host networking driver](https://docs.docker.com/network/network-tutorial-host/):\n\n```console\ndocker run \\\n    -e SERVER_NAME=\"127.0.0.1\" \\\n    -v $PWD:/app/public \\\n    --network host \\\n    dunglas/frankenphp\n```\n\nThe host networking driver isn't supported on Mac and Windows. On these platforms, you will have to guess the IP address of the container and include it in the server names.\n\nRun the `docker network inspect bridge` and look at the `Containers` key to identify the last currently assigned IP address under the `IPv4Address` key, and increment it by one. If no container is running, the first assigned IP address is usually `172.17.0.2`.\n\nThen, include this in the `SERVER_NAME` environment variable:\n\n```console\ndocker run \\\n    -e SERVER_NAME=\"127.0.0.1, 172.17.0.3\" \\\n    -v $PWD:/app/public \\\n    -p 80:80 -p 443:443 -p 443:443/udp \\\n    dunglas/frankenphp\n```\n\n> [!CAUTION]\n>\n> Be sure to replace `172.17.0.3` with the IP that will be assigned to your container.\n\nYou should now be able to access `https://127.0.0.1` from the host machine.\n\nIf that's not the case, start FrankenPHP in debug mode to try to figure out the problem:\n\n```console\ndocker run \\\n    -e CADDY_GLOBAL_OPTIONS=\"debug\" \\\n    -e SERVER_NAME=\"127.0.0.1\" \\\n    -v $PWD:/app/public \\\n    -p 80:80 -p 443:443 -p 443:443/udp \\\n    dunglas/frankenphp\n```\n\n## Composer Scripts Referencing `@php`\n\n[Composer scripts](https://getcomposer.org/doc/articles/scripts.md) may want to execute a PHP binary for some tasks, e.g. in [a Laravel project](laravel.md) to run `@php artisan package:discover --ansi`. This [currently fails](https://github.com/php/frankenphp/issues/483#issuecomment-1899890915) for two reasons:\n\n- Composer does not know how to call the FrankenPHP binary;\n- Composer may add PHP settings using the `-d` flag in the command, which FrankenPHP does not yet support.\n\nAs a workaround, we can create a shell script in `/usr/local/bin/php` which strips the unsupported parameters and then calls FrankenPHP:\n\n```bash\n#!/usr/bin/env bash\nargs=(\"$@\")\nindex=0\nfor i in \"$@\"\ndo\n    if [ \"$i\" == \"-d\" ]; then\n        unset 'args[$index]'\n        unset 'args[$index+1]'\n    fi\n    index=$((index+1))\ndone\n\n/usr/local/bin/frankenphp php-cli ${args[@]}\n```\n\nThen set the environment variable `PHP_BINARY` to the path of our `php` script and run Composer:\n\n```console\nexport PHP_BINARY=/usr/local/bin/php\ncomposer install\n```\n\n## Troubleshooting TLS/SSL Issues with Static Binaries\n\nWhen using the static binaries, you may encounter the following TLS-related errors, for instance when sending emails using STARTTLS:\n\n```text\nUnable to connect with STARTTLS: stream_socket_enable_crypto(): SSL operation failed with code 5. OpenSSL Error messages:\nerror:80000002:system library::No such file or directory\nerror:80000002:system library::No such file or directory\nerror:80000002:system library::No such file or directory\nerror:0A000086:SSL routines::certificate verify failed\n```\n\nAs the static binary doesn't bundle TLS certificates, you need to point OpenSSL to your local CA certificates installation.\n\nInspect the output of [`openssl_get_cert_locations()`](https://www.php.net/manual/en/function.openssl-get-cert-locations.php),\nto find where CA certificates must be installed and store them at this location.\n\n> [!WARNING]\n>\n> Web and CLI contexts may have different settings.\n> Be sure to run `openssl_get_cert_locations()` in the proper context.\n\n[CA certificates extracted from Mozilla can be downloaded on the cURL site](https://curl.se/docs/caextract.html).\n\nAlternatively, many distributions, including Debian, Ubuntu, and Alpine provide packages named `ca-certificates` that contain these certificates.\n\nIt's also possible to use the `SSL_CERT_FILE` and `SSL_CERT_DIR` to hint OpenSSL where to look for CA certificates:\n\n```console\n# Set TLS certificates environment variables\nexport SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt\nexport SSL_CERT_DIR=/etc/ssl/certs\n```\n"
  },
  {
    "path": "docs/laravel.md",
    "content": "# Laravel\n\n## Docker\n\nServing a [Laravel](https://laravel.com) web application with FrankenPHP is as easy as mounting the project in the `/app` directory of the official Docker image.\n\nRun this command from the main directory of your Laravel app:\n\n```console\ndocker run -p 80:80 -p 443:443 -p 443:443/udp -v $PWD:/app dunglas/frankenphp\n```\n\nAnd enjoy!\n\n## Local Installation\n\nAlternatively, you can run your Laravel projects with FrankenPHP from your local machine:\n\n1. [Download the binary corresponding to your system](../#standalone-binary)\n2. Add the following configuration to a file named `Caddyfile` in the root directory of your Laravel project:\n\n   ```caddyfile\n   {\n   \tfrankenphp\n   }\n\n   # The domain name of your server\n   localhost {\n   \t# Set the webroot to the public/ directory\n   \troot public/\n   \t# Enable compression (optional)\n   \tencode zstd br gzip\n   \t# Execute PHP files from the public/ directory and serve assets\n   \tphp_server {\n   \t\ttry_files {path} index.php\n   \t}\n   }\n   ```\n\n3. Start FrankenPHP from the root directory of your Laravel project: `frankenphp run`\n\n## Laravel Octane\n\nOctane may be installed via the Composer package manager:\n\n```console\ncomposer require laravel/octane\n```\n\nAfter installing Octane, you may execute the `octane:install` Artisan command, which will install Octane's configuration file into your application:\n\n```console\nphp artisan octane:install --server=frankenphp\n```\n\nThe Octane server can be started via the `octane:frankenphp` Artisan command.\n\n```console\nphp artisan octane:frankenphp\n```\n\nThe `octane:frankenphp` command can take the following options:\n\n- `--host`: The IP address the server should bind to (default: `127.0.0.1`)\n- `--port`: The port the server should be available on (default: `8000`)\n- `--admin-port`: The port the admin server should be available on (default: `2019`)\n- `--workers`: The number of workers that should be available to handle requests (default: `auto`)\n- `--max-requests`: The number of requests to process before reloading the server (default: `500`)\n- `--caddyfile`: The path to the FrankenPHP `Caddyfile` file (default: [stubbed `Caddyfile` in Laravel Octane](https://github.com/laravel/octane/blob/2.x/src/Commands/stubs/Caddyfile))\n- `--https`: Enable HTTPS, HTTP/2, and HTTP/3, and automatically generate and renew certificates\n- `--http-redirect`: Enable HTTP to HTTPS redirection (only enabled if --https is passed)\n- `--watch`: Automatically reload the server when the application is modified\n- `--poll`: Use file system polling while watching in order to watch files over a network\n- `--log-level`: Log messages at or above the specified log level, using the native Caddy logger\n\n> [!TIP]\n> To get structured JSON logs (useful when using log analytics solutions), explicitly the pass `--log-level` option.\n\nSee also [how to use Mercure with Octane](#mercure-support).\n\nLearn more about [Laravel Octane in its official documentation](https://laravel.com/docs/octane).\n\n## Laravel Apps As Standalone Binaries\n\nUsing [FrankenPHP's application embedding feature](embed.md), it's possible to distribute Laravel\napps as standalone binaries.\n\nFollow these steps to package your Laravel app as a standalone binary for Linux:\n\n1. Create a file named `static-build.Dockerfile` in the repository of your app:\n\n   ```dockerfile\n   FROM --platform=linux/amd64 dunglas/frankenphp:static-builder-gnu\n   # If you intend to run the binary on musl-libc systems, use static-builder-musl instead\n\n   # Copy your app\n   WORKDIR /go/src/app/dist/app\n   COPY . .\n\n   # Remove the tests and other unneeded files to save space\n   # Alternatively, add these files to a .dockerignore file\n   RUN rm -Rf tests/\n\n   # Copy .env file\n   RUN cp .env.example .env\n   # Change APP_ENV and APP_DEBUG to be production ready\n   RUN sed -i'' -e 's/^APP_ENV=.*/APP_ENV=production/' -e 's/^APP_DEBUG=.*/APP_DEBUG=false/' .env\n\n   # Make other changes to your .env file if needed\n\n   # Install the dependencies\n   RUN composer install --ignore-platform-reqs --no-dev -a\n\n   # Build the static binary\n   WORKDIR /go/src/app/\n   RUN EMBED=dist/app/ ./build-static.sh\n   ```\n\n   > [!CAUTION]\n   >\n   > Some `.dockerignore` files\n   > will ignore the `vendor/` directory and `.env` files. Be sure to adjust or remove the `.dockerignore` file before the build.\n\n2. Build:\n\n   ```console\n   docker build -t static-laravel-app -f static-build.Dockerfile .\n   ```\n\n3. Extract the binary:\n\n   ```console\n   docker cp $(docker create --name static-laravel-app-tmp static-laravel-app):/go/src/app/dist/frankenphp-linux-x86_64 frankenphp ; docker rm static-laravel-app-tmp\n   ```\n\n4. Populate caches:\n\n   ```console\n   frankenphp php-cli artisan optimize\n   ```\n\n5. Run database migrations (if any):\n\n   ```console\n   frankenphp php-cli artisan migrate\n   ```\n\n6. Generate app's secret key:\n\n   ```console\n   frankenphp php-cli artisan key:generate\n   ```\n\n7. Start the server:\n\n   ```console\n   frankenphp php-server\n   ```\n\nYour app is now ready!\n\nLearn more about the options available and how to build binaries for other OSes in the [applications embedding](embed.md)\ndocumentation.\n\n### Changing The Storage Path\n\nBy default, Laravel stores uploaded files, caches, logs, etc. in the application's `storage/` directory.\nThis is not suitable for embedded applications, as each new version will be extracted into a different temporary directory.\n\nSet the `LARAVEL_STORAGE_PATH` environment variable (for example, in your `.env` file) or call the `Illuminate\\Foundation\\Application::useStoragePath()` method to use a directory outside the temporary directory.\n\n### Mercure Support\n\n[Mercure](https://mercure.rocks) is a great way to add real-time capabilities to your Laravel apps.\nFrankenPHP includes [Mercure support out of the box](mercure.md).\n\nIf you are not using [Octane](#laravel-octane), see [the Mercure documentation entry](mercure.md).\n\nIf you are using Octane, you can use enable Mercure support by adding the following lines to your `config/octane.php` file:\n\n```php\n// ...\n\nreturn [\n    // ...\n\n    'mercure' => [\n        'anonymous' => true,\n        'publisher_jwt' => '!ChangeThisMercureHubJWTSecretKey!',\n        'subscriber_jwt' => '!ChangeThisMercureHubJWTSecretKey!',\n    ],\n];\n```\n\nYou can use [all directives supported by Mercure](https://mercure.rocks/docs/hub/config#directives) in this array.\n\nTo publish and subscribe to updates, we recommend using the [Laravel Mercure Broadcaster](https://github.com/mvanduijker/laravel-mercure-broadcaster) library.\nAlternatively, see [the Mercure documentation](mercure.md) to do it in pure PHP and JavaScript.\n\n### Running Octane With Standalone Binaries\n\nIt's even possible to package Laravel Octane apps as standalone binaries!\n\nTo do so, [install Octane properly](#laravel-octane) and follow the steps described in [the previous section](#laravel-apps-as-standalone-binaries).\n\nThen, to start FrankenPHP in worker mode through Octane, run:\n\n```console\nPATH=\"$PWD:$PATH\" frankenphp php-cli artisan octane:frankenphp\n```\n\n> [!CAUTION]\n>\n> For the command to work, the standalone binary **must** be named `frankenphp`\n> because Octane needs a program named `frankenphp` available in the path.\n"
  },
  {
    "path": "docs/logging.md",
    "content": "# Logging\n\nFrankenPHP integrates seamlessly with [Caddy's logging system](https://caddyserver.com/docs/logging).\nYou can log messages using standard PHP functions or leverage the dedicated `frankenphp_log()` function for advanced\nstructured logging capabilities.\n\n## `frankenphp_log()`\n\nThe `frankenphp_log()` function allows you to emit structured logs directly from your PHP application,\nmaking ingestion into platforms like Datadog, Grafana Loki, or Elastic, as well as OpenTelemetry support, much easier.\n\nUnder the hood, `frankenphp_log()` wraps [Go's `log/slog` package](https://pkg.go.dev/log/slog) to provide rich logging\nfeatures.\n\nThese logs include the severity level and optional context data.\n\n```php\nfunction frankenphp_log(string $message, int $level = FRANKENPHP_LOG_LEVEL_INFO, array $context = []): void\n```\n\n### Parameters\n\n- **`message`**: The log message string.\n- **`level`**: The severity level of the log. Can be any arbitrary integer. Convenience constants are provided for common levels: `FRANKENPHP_LOG_LEVEL_DEBUG` (`-4`), `FRANKENPHP_LOG_LEVEL_INFO` (`0`), `FRANKENPHP_LOG_LEVEL_WARN` (`4`) and `FRANKENPHP_LOG_LEVEL_ERROR` (`8`)). Default is `FRANKENPHP_LOG_LEVEL_INFO`.\n- **`context`**: An associative array of additional data to include in the log entry.\n\n### Example\n\n```php\n<?php\n\n// Log a simple informational message\nfrankenphp_log(\"Hello from FrankenPHP!\");\n\n// Log a warning with context data\nfrankenphp_log(\n    \"Memory usage high\",\n    FRANKENPHP_LOG_LEVEL_WARN,\n    [\n        'current_usage' => memory_get_usage(),\n        'peak_usage' => memory_get_peak_usage(),\n    ],\n);\n\n```\n\nWhen viewing the logs (e.g., via `docker compose logs`), the output will appear as structured JSON:\n\n```json\n{\"level\":\"info\",\"ts\":1704067200,\"logger\":\"frankenphp\",\"msg\":\"Hello from FrankenPHP!\"}\n{\"level\":\"warn\",\"ts\":1704067200,\"logger\":\"frankenphp\",\"msg\":\"Memory usage high\",\"current_usage\":10485760,\"peak_usage\":12582912}\n```\n\n## `error_log()`\n\nFrankenPHP also allows logging using the standard `error_log()` function. If the `$message_type` parameter is `4` (SAPI),\nthese messages are routed to the Caddy logger.\n\nBy default, messages sent via `error_log()` are treated as unstructured text.\nThey are useful for compatibility with existing applications or libraries that rely on the standard PHP library.\n\n### Example with error_log()\n\n```php\nerror_log(\"Database connection failed\", 4);\n```\n\nThis will appear in the Caddy logs, often prefixed to indicate it originated from PHP.\n\n> [!TIP]\n> For better observability in production environments, prefer `frankenphp_log()`\n> as it allows you to filter logs by level (Debug, Error, etc.)\n> and query specific fields in your logging infrastructure.\n"
  },
  {
    "path": "docs/mercure.md",
    "content": "# Real-time\n\nFrankenPHP comes with a built-in [Mercure](https://mercure.rocks) hub!\nMercure allows you to push real-time events to all the connected devices: they will receive a JavaScript event instantly.\n\nIt's a convenient alternative to WebSockets that is simple to use and is natively supported by all modern web browsers!\n\n![Mercure](mercure-hub.png)\n\n## Enabling Mercure\n\nMercure support is disabled by default.\nHere is a minimal example of a `Caddyfile` enabling both FrankenPHP and the Mercure hub:\n\n```caddyfile\n# The hostname to respond to\nlocalhost\n\nmercure {\n    # The secret key used to sign the JWT tokens for publishers\n    publisher_jwt !ChangeThisMercureHubJWTSecretKey!\n    # When publisher_jwt is set, you must set subscriber_jwt too!\n    subscriber_jwt !ChangeThisMercureHubJWTSecretKey!\n    # Allows anonymous subscribers (without JWT)\n    anonymous\n}\n\nroot public/\nphp_server\n```\n\n> [!TIP]\n>\n> The [sample `Caddyfile`](https://github.com/php/frankenphp/blob/main/caddy/frankenphp/Caddyfile)\n> provided by [the Docker images](docker.md) already includes a commented Mercure configuration\n> with convenient environment variables to configure it.\n>\n> Uncomment the Mercure section in `/etc/frankenphp/Caddyfile` to enable it.\n\n## Subscribing to Updates\n\nBy default, the Mercure hub is available on the `/.well-known/mercure` path of your FrankenPHP server.\nTo subscribe to updates, use the native [`EventSource`](https://developer.mozilla.org/docs/Web/API/EventSource) JavaScript class:\n\n```html\n<!-- public/index.html -->\n<!doctype html>\n<title>Mercure Example</title>\n<script>\n  const eventSource = new EventSource(\"/.well-known/mercure?topic=my-topic\");\n  eventSource.onmessage = function (event) {\n    console.log(\"New message:\", event.data);\n  };\n</script>\n```\n\n## Publishing Updates\n\n### Using `mercure_publish()`\n\nFrankenPHP provides a convenient `mercure_publish()` function to publish updates to the built-in Mercure hub:\n\n```php\n<?php\n// public/publish.php\n\n$updateID = mercure_publish('my-topic',  json_encode(['key' => 'value']));\n\n// Write to FrankenPHP's logs\nerror_log(\"update $updateID published\", 4);\n```\n\nThe full function signature is:\n\n```php\n/**\n * @param string|string[] $topics\n */\nfunction mercure_publish(string|array $topics, string $data = '', bool $private = false, ?string $id = null, ?string $type = null, ?int $retry = null): string {}\n```\n\n### Using `file_get_contents()`\n\nTo dispatch an update to connected subscribers, send an authenticated POST request to the Mercure hub with the `topic` and `data` parameters:\n\n```php\n<?php\n// public/publish.php\n\nconst JWT = 'eyJhbGciOiJIUzI1NiJ9.eyJtZXJjdXJlIjp7InB1Ymxpc2giOlsiKiJdfX0.PXwpfIGng6KObfZlcOXvcnWCJOWTFLtswGI5DZuWSK4';\n\n$updateID = file_get_contents('https://localhost/.well-known/mercure', context: stream_context_create(['http' => [\n    'method'  => 'POST',\n    'header'  => \"Content-type: application/x-www-form-urlencoded\\r\\nAuthorization: Bearer \" . JWT,\n    'content' => http_build_query([\n        'topic' => 'my-topic',\n        'data' => json_encode(['key' => 'value']),\n    ]),\n]]));\n\n// Write to FrankenPHP's logs\nerror_log(\"update $updateID published\", 4);\n```\n\nThe key passed as parameter of the `mercure.publisher_jwt` option in the `Caddyfile` must used to sign the JWT token used in the `Authorization` header.\n\nThe JWT must include a `mercure` claim with a `publish` permission for the topics you want to publish to.\nSee [the Mercure documentation](https://mercure.rocks/spec#publishers) about authorization.\n\nTo generate your own tokens, you can use [this jwt.io link](https://www.jwt.io/#token=eyJhbGciOiJIUzI1NiJ9.eyJtZXJjdXJlIjp7InB1Ymxpc2giOlsiKiJdfX0.PXwpfIGng6KObfZlcOXvcnWCJOWTFLtswGI5DZuWSK4),\nbut for production apps, it's recommended to use short-lived tokens generated aerodynamically using with a trusted [JWT library](https://www.jwt.io/libraries?programming_language=php).\n\n### Using Symfony Mercure\n\nAlternatively, you can use the [Symfony Mercure Component](https://symfony.com/components/Mercure), a standalone PHP library.\n\nThis library handled the JWT generation, update publishing as well as cookie-based authorization for subscribers.\n\nFirst, install the library using Composer:\n\n```console\ncomposer require symfony/mercure lcobucci/jwt\n```\n\nThen, you can use it like this:\n\n```php\n<?php\n// public/publish.php\n\nrequire __DIR__ . '/../vendor/autoload.php';\n\nconst JWT_SECRET = '!ChangeThisMercureHubJWTSecretKey!'; // Must be the same as mercure.publisher_jwt in Caddyfile\n\n// Set up the JWT token provider\n$jwFactory = new \\Symfony\\Component\\Mercure\\Jwt\\LcobucciFactory(JWT_SECRET);\n$provider = new \\Symfony\\Component\\Mercure\\Jwt\\FactoryTokenProvider($jwFactory, publish: ['*']);\n\n$hub = new \\Symfony\\Component\\Mercure\\Hub('https://localhost/.well-known/mercure', $provider);\n// Serialize the update, and dispatch it to the hub, that will broadcast it to the clients\n$updateID = $hub->publish(new \\Symfony\\Component\\Mercure\\Update('my-topic', json_encode(['key' => 'value'])));\n\n// Write to FrankenPHP's logs\nerror_log(\"update $updateID published\", 4);\n```\n\nMercure is also natively supported by:\n\n- [Laravel](laravel.md#mercure-support)\n- [Symfony](https://symfony.com/doc/current/mercure.html)\n- [API Platform](https://api-platform.com/docs/core/mercure/)\n"
  },
  {
    "path": "docs/metrics.md",
    "content": "# Metrics\n\nWhen [Caddy metrics](https://caddyserver.com/docs/metrics) are enabled, FrankenPHP exposes the following metrics:\n\n- `frankenphp_total_threads`: The total number of PHP threads.\n- `frankenphp_busy_threads`: The number of PHP threads currently processing a request (running workers always consume a thread).\n- `frankenphp_queue_depth`: The number of regular queued requests\n- `frankenphp_total_workers{worker=\"[worker_name]\"}`: The total number of workers.\n- `frankenphp_busy_workers{worker=\"[worker_name]\"}`: The number of workers currently processing a request.\n- `frankenphp_worker_request_time{worker=\"[worker_name]\"}`: The time spent processing requests by all workers.\n- `frankenphp_worker_request_count{worker=\"[worker_name]\"}`: The number of requests processed by all workers.\n- `frankenphp_ready_workers{worker=\"[worker_name]\"}`: The number of workers that have called `frankenphp_handle_request` at least once.\n- `frankenphp_worker_crashes{worker=\"[worker_name]\"}`: The number of times a worker has unexpectedly terminated.\n- `frankenphp_worker_restarts{worker=\"[worker_name]\"}`: The number of times a worker has been deliberately restarted.\n- `frankenphp_worker_queue_depth{worker=\"[worker_name]\"}`: The number of queued requests.\n\nFor worker metrics, the `[worker_name]` placeholder is replaced by the worker name in the Caddyfile, otherwise absolute path of worker file will be used.\n"
  },
  {
    "path": "docs/performance.md",
    "content": "# Performance\n\nBy default, FrankenPHP tries to offer a good compromise between performance and ease of use.\nHowever, it is possible to substantially improve performance using an appropriate configuration.\n\n## Number of Threads and Workers\n\nBy default, FrankenPHP starts 2 times more threads and workers (in worker mode) than the available number of CPU cores.\n\nThe appropriate values depend heavily on how your application is written, what it does, and your hardware.\nWe strongly recommend changing these values. For best system stability, it is recommended to have `num_threads` x `memory_limit` < `available_memory`.\n\nTo find the right values, it's best to run load tests simulating real traffic.\n[k6](https://k6.io) and [Gatling](https://gatling.io) are good tools for this.\n\nTo configure the number of threads, use the `num_threads` option of the `php_server` and `php` directives.\nTo change the number of workers, use the `num` option of the `worker` section of the `frankenphp` directive.\n\n### `max_threads`\n\nWhile it's always better to know exactly what your traffic will look like, real-life applications tend to be more\nunpredictable. The `max_threads` [configuration](config.md#caddyfile-config) allows FrankenPHP to automatically spawn additional threads at runtime up to the specified limit.\n`max_threads` can help you figure out how many threads you need to handle your traffic and can make the server more resilient to latency spikes.\nIf set to `auto`, the limit will be estimated based on the `memory_limit` in your `php.ini`. If not able to do so,\n`auto` will instead default to 2x `num_threads`. Keep in mind that `auto` might strongly underestimate the number of threads needed.\n`max_threads` is similar to PHP FPM's [pm.max_children](https://www.php.net/manual/en/install.fpm.configuration.php#pm.max-children). The main difference is that FrankenPHP uses threads instead of\nprocesses and automatically delegates them across different worker scripts and 'classic mode' as needed.\n\n## Worker Mode\n\nEnabling [the worker mode](worker.md) dramatically improves performance,\nbut your app must be adapted to be compatible with this mode:\nyou need to create a worker script and to be sure that the app is not leaking memory.\n\n## Don't Use musl\n\nThe Alpine Linux variant of the official Docker images and the default binaries we provide are using [the musl libc](https://musl.libc.org).\n\nPHP is known to be [slower](https://gitlab.alpinelinux.org/alpine/aports/-/issues/14381) when using this alternative C library instead of the traditional GNU library,\nespecially when compiled in ZTS mode (thread-safe), which is required for FrankenPHP. The difference can be significant in a heavily threaded environment.\n\nAlso, [some bugs only happen when using musl](https://github.com/php/php-src/issues?q=sort%3Aupdated-desc+is%3Aissue+is%3Aopen+label%3ABug+musl).\n\nIn production environments, we recommend using FrankenPHP linked against glibc, compiled with an appropriate optimization level.\n\nThis can be achieved by using the Debian Docker images, using [our maintainers .deb, .rpm, or .apk packages](https://pkgs.henderkes.com), or by [compiling FrankenPHP from sources](compile.md).\n\nFor leaner or more secure containers, you may want to consider [a hardened Debian image](docker.md#hardening-images) rather than Alpine.\n\n## Go Runtime Configuration\n\nFrankenPHP is written in Go.\n\nIn general, the Go runtime doesn't require any special configuration, but in certain circumstances,\nspecific configuration improves performance.\n\nYou likely want to set the `GODEBUG` environment variable to `cgocheck=0` (the default in the FrankenPHP Docker images).\n\nIf you run FrankenPHP in containers (Docker, Kubernetes, LXC...) and limit the memory available for the containers,\nset the `GOMEMLIMIT` environment variable to the available amount of memory.\n\nFor more details, [the Go documentation page dedicated to this subject](https://pkg.go.dev/runtime#hdr-Environment_Variables) is a must-read to get the most out of the runtime.\n\n## `file_server`\n\nBy default, the `php_server` directive automatically sets up a file server to\nserve static files (assets) stored in the root directory.\n\nThis feature is convenient, but comes with a cost.\nTo disable it, use the following config:\n\n```caddyfile\nphp_server {\n    file_server off\n}\n```\n\n## `try_files`\n\nBesides static files and PHP files, `php_server` will also try to serve your application's index\nand directory index files (`/path/` -> `/path/index.php`). If you don't need directory indices,\nyou can disable them by explicitly defining `try_files` like this:\n\n```caddyfile\nphp_server {\n    try_files {path} index.php\n    root /root/to/your/app # explicitly adding the root here allows for better caching\n}\n```\n\nThis can significantly reduce the number of unnecessary file operations.\nA worker equivalent of the previous configuration would be:\n\n```caddyfile\nroute {\n    php_server { # use \"php\" instead of \"php_server\" if you don't need the file server at all\n        root /root/to/your/app\n        worker /path/to/worker.php {\n            match * # send all requests directly to the worker\n        }\n    }\n}\n```\n\nAn alternate approach with 0 unnecessary file system operations would be to instead use the `php` directive and split\nfiles from PHP by path. This approach works well if your entire application is served by one entry file.\nAn example [configuration](config.md#caddyfile-config) that serves static files behind an `/assets` folder could look like this:\n\n```caddyfile\nroute {\n    @assets {\n        path /assets/*\n    }\n\n    # everything behind /assets is handled by the file server\n    file_server @assets {\n        root /root/to/your/app\n    }\n\n    # everything that is not in /assets is handled by your index or worker PHP file\n    rewrite index.php\n    php {\n        root /root/to/your/app # explicitly adding the root here allows for better caching\n    }\n}\n```\n\n## Placeholders\n\nYou can use [placeholders](https://caddyserver.com/docs/conventions#placeholders) in the `root` and `env` directives.\nHowever, this prevents caching these values, and comes with a significant performance cost.\n\nIf possible, avoid placeholders in these directives.\n\n## `resolve_root_symlink`\n\nBy default, if the document root is a symbolic link, it is automatically resolved by FrankenPHP (this is necessary for PHP to work properly).\nIf the document root is not a symlink, you can disable this feature.\n\n```caddyfile\nphp_server {\n    resolve_root_symlink false\n}\n```\n\nThis will improve performance if the `root` directive contains [placeholders](https://caddyserver.com/docs/conventions#placeholders).\nThe gain will be negligible in other cases.\n\n## Logs\n\nLogging is obviously very useful, but, by definition,\nit requires I/O operations and memory allocations, which considerably reduces performance.\nMake sure you [set the logging level](https://caddyserver.com/docs/caddyfile/options#log) correctly,\nand only log what's necessary.\n\n## PHP Performance\n\nFrankenPHP uses the official PHP interpreter.\nAll usual PHP-related performance optimizations apply with FrankenPHP.\n\nIn particular:\n\n- check that [OPcache](https://www.php.net/manual/en/book.opcache.php) is installed, enabled, and properly configured\n- enable [Composer autoloader optimizations](https://getcomposer.org/doc/articles/autoloader-optimization.md)\n- ensure that the `realpath` cache is big enough for the needs of your application\n- use [preloading](https://www.php.net/manual/en/opcache.preloading.php)\n\nFor more details, read [the dedicated Symfony documentation entry](https://symfony.com/doc/current/performance.html)\n(most tips are useful even if you don't use Symfony).\n\n## Splitting The Thread Pool\n\nIt is common for applications to interact with slow external services, like an\nAPI that tends to be unreliable under high load or consistently takes 10+ seconds to respond.\nIn such cases, it can be beneficial to split the thread pool to have dedicated \"slow\" pools.\nThis prevents the slow endpoints from consuming all server resources/threads and\nlimits the concurrency of requests going towards the slow endpoint, similar to a\nconnection pool.\n\n```caddyfile\nexample.com {\n    php_server {\n        root /app/public # the root of your application\n        worker index.php {\n            match /slow-endpoint/* # all requests with path /slow-endpoint/* are handled by this thread pool\n            num 1 # minimum 1 threads for requests matching /slow-endpoint/*\n            max_threads 20 # allow up to 20 threads for requests matching /slow-endpoint/*, if needed\n        }\n        worker index.php {\n            match * # all other requests are handled separately\n            num 1 # minimum 1 threads for other requests, even if the slow endpoints start hanging\n            max_threads 20 # allow up to 20 threads for other requests, if needed\n        }\n    }\n}\n```\n\nGenerally it's also advisable to handle very slow endpoints asynchronously, by using relevant mechanisms such as message queues.\n"
  },
  {
    "path": "docs/production.md",
    "content": "# Deploying in Production\n\nIn this tutorial, we will learn how to deploy a PHP application on a single server using Docker Compose.\n\nIf you're using Symfony, prefer reading the \"[Deploy in production](https://github.com/dunglas/symfony-docker/blob/main/docs/production.md)\" documentation entry of the Symfony Docker project (which uses FrankenPHP).\n\nIf you're using API Platform (which also uses FrankenPHP), refer to [the deployment documentation of the framework](https://api-platform.com/docs/deployment/).\n\n## Preparing Your App\n\nFirst, create a `Dockerfile` in the root directory of your PHP project:\n\n```dockerfile\nFROM dunglas/frankenphp\n\n# Be sure to replace \"your-domain-name.example.com\" by your domain name\nENV SERVER_NAME=your-domain-name.example.com\n# If you want to disable HTTPS, use this value instead:\n#ENV SERVER_NAME=:80\n\n# If your project is not using the \"public\" directory as the web root, you can set it here:\n# ENV SERVER_ROOT=web/\n\n# Enable PHP production settings\nRUN mv \"$PHP_INI_DIR/php.ini-production\" \"$PHP_INI_DIR/php.ini\"\n\n# Copy the PHP files of your project in the public directory\nCOPY . /app/public\n# If you use Symfony or Laravel, you need to copy the whole project instead:\n#COPY . /app\n```\n\nRefer to \"[Building Custom Docker Image](docker.md)\" for more details and options,\nand to learn how to customize the configuration, install PHP extensions and Caddy modules.\n\nIf your project uses Composer,\nbe sure to include it in the Docker image and to install your dependencies.\n\nThen, add a `compose.yaml` file:\n\n```yaml\nservices:\n  php:\n    image: dunglas/frankenphp\n    restart: always\n    ports:\n      - \"80:80\" # HTTP\n      - \"443:443\" # HTTPS\n      - \"443:443/udp\" # HTTP/3\n    volumes:\n      - caddy_data:/data\n      - caddy_config:/config\n\n# Volumes needed for Caddy certificates and configuration\nvolumes:\n  caddy_data:\n  caddy_config:\n```\n\n> [!NOTE]\n>\n> The previous examples are intended for production usage.\n> In development, you may want to use a volume, a different PHP configuration and a different value for the `SERVER_NAME` environment variable.\n>\n> Take a look to the [Symfony Docker](https://github.com/dunglas/symfony-docker) project\n> (which uses FrankenPHP) for a more advanced example using multi-stage images,\n> Composer, extra PHP extensions, etc.\n\nFinally, if you use Git, commit these files and push.\n\n## Preparing a Server\n\nTo deploy your application in production, you need a server.\nIn this tutorial, we will use a virtual machine provided by DigitalOcean, but any Linux server can work.\nIf you already have a Linux server with Docker installed, you can skip straight to [the next section](#configuring-a-domain-name).\n\nOtherwise, use [this affiliate link](https://m.do.co/c/5d8aabe3ab80) to get $200 of free credit, create an account, then click on \"Create a Droplet\".\nThen, click on the \"Marketplace\" tab under the \"Choose an image\" section and search for the app named \"Docker\".\nThis will provision an Ubuntu server with the latest versions of Docker and Docker Compose already installed!\n\nFor test purposes, the cheapest plans will be enough.\nFor real production usage, you'll probably want to pick a plan in the \"general purpose\" section to fit your needs.\n\n![Deploying FrankenPHP on DigitalOcean with Docker](digitalocean-droplet.png)\n\nYou can keep the defaults for other settings, or tweak them according to your needs.\nDon't forget to add your SSH key or create a password then press the \"Finalize and create\" button.\n\nThen, wait a few seconds while your Droplet is provisioning.\nWhen your Droplet is ready, use SSH to connect:\n\n```console\nssh root@<droplet-ip>\n```\n\n## Configuring a Domain Name\n\nIn most cases, you'll want to associate a domain name with your site.\nIf you don't own a domain name yet, you'll have to buy one through a registrar.\n\nThen create a DNS record of type `A` for your domain name pointing to the IP address of your server:\n\n```dns\nyour-domain-name.example.com.  IN  A     207.154.233.113\n```\n\nExample with the DigitalOcean Domains service (\"Networking\" > \"Domains\"):\n\n![Configuring DNS on DigitalOcean](digitalocean-dns.png)\n\n> [!NOTE]\n>\n> Let's Encrypt, the service used by default by FrankenPHP to automatically generate a TLS certificate doesn't support using bare IP addresses. Using a domain name is mandatory to use Let's Encrypt.\n\n## Deploying\n\nCopy your project on the server using `git clone`, `scp`, or any other tool that may fit your need.\nIf you use GitHub, you may want to use [a deploy key](https://docs.github.com/en/free-pro-team@latest/developers/overview/managing-deploy-keys#deploy-keys).\nDeploy keys are also [supported by GitLab](https://docs.gitlab.com/ee/user/project/deploy_keys/).\n\nExample with Git:\n\n```console\ngit clone git@github.com:<username>/<project-name>.git\n```\n\nGo into the directory containing your project (`<project-name>`), and start the app in production mode:\n\n```console\ndocker compose up --wait\n```\n\nYour server is up and running, and an HTTPS certificate has been automatically generated for you.\nGo to `https://your-domain-name.example.com` and enjoy!\n\n> [!CAUTION]\n>\n> Docker can have a cache layer, make sure you have the right build for each deployment or rebuild your project with `--no-cache` option to avoid cache issue.\n\n## Deploying on Multiple Nodes\n\nIf you want to deploy your app on a cluster of machines, you can use [Docker Swarm](https://docs.docker.com/engine/swarm/stack-deploy/),\nwhich is compatible with the provided Compose files.\nTo deploy on Kubernetes, take a look at [the Helm chart provided with API Platform](https://api-platform.com/docs/deployment/kubernetes/), which uses FrankenPHP.\n"
  },
  {
    "path": "docs/pt-br/CONTRIBUTING.md",
    "content": "# Contribuindo\n\n## Compilando o PHP\n\n### Com Docker (Linux)\n\nCrie a imagem Docker de desenvolvimento:\n\n```console\ndocker build -t frankenphp-dev -f dev.Dockerfile .\ndocker run --cap-add=SYS_PTRACE --security-opt seccomp=unconfined -p 8080:8080 -p 443:443 -p 443:443/udp -v $PWD:/go/src/app -it frankenphp-dev\n```\n\nA imagem contém as ferramentas de desenvolvimento usuais (Go, GDB, Valgrind,\nNeovim...) e usa os seguintes locais de configuração do PHP:\n\n- php.ini: `/etc/frankenphp/php.ini`.\n  Um arquivo `php.ini` com configurações de desenvolvimento é fornecido por\n  padrão.\n- Arquivos de configuração adicionais: `/etc/frankenphp/php.d/*.ini`.\n- Extensões PHP: `/usr/lib/frankenphp/modules/`.\n\nSe a sua versão do Docker for anterior à 23.0, a compilação falhará devido ao\n[problema de padrão do `.dockerignore`](https://github.com/moby/moby/pull/42676).\nAdicione diretórios ao `.dockerignore`.\n\n```patch\n !testdata/*.php\n !testdata/*.txt\n+!caddy\n+!internal\n```\n\n### Sem Docker (Linux e macOS)\n\n[Siga as instruções para compilar a partir do código-fonte](compile.md) e passe\na flag de configuração `--debug`.\n\n## Executando a suite de testes\n\n```console\ngo test -race -v ./...\n```\n\n## Módulo Caddy\n\nConstrua o Caddy com o módulo Caddy FrankenPHP:\n\n```console\ncd caddy/frankenphp/\ngo build -tags nobadger,nomysql,nopgx\ncd ../../\n```\n\nExecute o Caddy com o módulo Caddy FrankenPHP:\n\n```console\ncd testdata/\n../caddy/frankenphp/frankenphp run\n```\n\nO servidor está escutando em `127.0.0.1:80`:\n\n> [!NOTE]\n> Se você estiver usando o Docker, terá que vincular a porta 80 do contêiner ou\n> executar de dentro do contêiner.\n\n```console\ncurl -vk http://127.0.0.1/phpinfo.php\n```\n\n## Servidor de teste mínimo\n\nConstrua o servidor de teste mínimo:\n\n```console\ncd internal/testserver/\ngo build\ncd ../../\n```\n\nExecute o servidor de teste:\n\n```console\ncd testdata/\n../internal/testserver/testserver\n```\n\nO servidor está escutando em `127.0.0.1:8080`:\n\n```console\ncurl -v http://127.0.0.1:8080/phpinfo.php\n```\n\n## Construindo imagens Docker localmente\n\nImprima o plano do bake:\n\n```console\ndocker buildx bake -f docker-bake.hcl --print\n```\n\nConstrua imagens FrankenPHP para amd64 localmente:\n\n```console\ndocker buildx bake -f docker-bake.hcl --pull --load --set \"*.platform=linux/amd64\"\n```\n\nConstrua imagens FrankenPHP para arm64 localmente:\n\n```console\ndocker buildx bake -f docker-bake.hcl --pull --load --set \"*.platform=linux/arm64\"\n```\n\nConstrua imagens FrankenPHP do zero para arm64 e amd64 e envie para o Docker\nHub:\n\n```console\ndocker buildx bake -f docker-bake.hcl --pull --no-cache --push\n```\n\n## Depurando falhas de segmentação com compilações estáticas\n\n1. Baixe a versão de depuração do binário do FrankenPHP do GitHub ou crie sua\n   própria compilação estática personalizada, incluindo símbolos de depuração:\n\n   ```console\n   docker buildx bake \\\n       --load \\\n       --set static-builder.args.DEBUG_SYMBOLS=1 \\\n       --set \"static-builder.platform=linux/amd64\" \\\n       static-builder\n   docker cp $(docker create --name static-builder-musl dunglas/frankenphp:static-builder-musl):/go/src/app/dist/frankenphp-linux-$(uname -m) frankenphp\n   ```\n\n2. Substitua sua versão atual do `frankenphp` pelo executável de depuração do\n   FrankenPHP.\n3. Inicie o FrankenPHP normalmente (alternativamente, você pode iniciar o\n   FrankenPHP diretamente com o GDB: `gdb --args frankenphp run`).\n4. Anexe ao processo com o GDB:\n\n   ```console\n   gdb -p `pidof frankenphp`\n   ```\n\n5. Se necessário, digite `continue` no shell do GDB.\n6. Faça o FrankenPHP travar.\n7. Digite `bt` no shell do GDB.\n8. Copie a saída.\n\n## Depurando falhas de segmentação no GitHub Actions\n\n1. Abra o arquivo `.github/workflows/tests.yml`.\n2. Habilite os símbolos de depuração do PHP:\n\n   ```patch\n       - uses: shivammathur/setup-php@v2\n         # ...\n         env:\n           phpts: ts\n   +       debug: true\n   ```\n\n3. Habilite o `tmate` para se conectar ao contêiner:\n\n   ```patch\n       - name: Set CGO flags\n         run: echo \"CGO_CFLAGS=$(php-config --includes)\" >> \"$GITHUB_ENV\"\n   +   - run: |\n   +       sudo apt install gdb\n   +       mkdir -p /home/runner/.config/gdb/\n   +       printf \"set auto-load safe-path /\\nhandle SIG34 nostop noprint pass\" > /home/runner/.config/gdb/gdbinit\n   +   - uses: mxschmitt/action-tmate@v3\n   ```\n\n4. Conecte-se ao contêiner.\n5. Abra o `frankenphp.go`.\n6. Habilite o `cgosymbolizer`:\n\n   ```patch\n   -\t//_ \"github.com/ianlancetaylor/cgosymbolizer\"\n   +\t_ \"github.com/ianlancetaylor/cgosymbolizer\"\n   ```\n\n7. Baixe o módulo: `go get`.\n8. No contêiner, você pode usar o GDB e similares:\n\n   ```console\n   go test -c -ldflags=-w\n   gdb --args frankenphp.test -test.run ^MyTest$\n   ```\n\n9. Quando a falha for corrigida, reverta todas essas alterações.\n\n## Recursos diversos de desenvolvimento\n\n- [PHP embedding in uWSGI](https://github.com/unbit/uwsgi/blob/master/plugins/php/php_plugin.c)\n- [PHP embedding in NGINX Unit](https://github.com/nginx/unit/blob/master/src/nxt_php_sapi.c)\n- [PHP embedding in Go (go-php)](https://github.com/deuill/go-php)\n- [PHP embedding in Go (GoEmPHP)](https://github.com/mikespook/goemphp)\n- [PHP embedding in C++](https://gist.github.com/paresy/3cbd4c6a469511ac7479aa0e7c42fea7)\n- [Extending and Embedding PHP, por Sara Golemon](https://books.google.fr/books?id=zMbGvK17_tYC&pg=PA254&lpg=PA254#v=onepage&q&f=false)\n- [What the heck is TSRMLS_CC, anyway?](http://blog.golemon.com/2006/06/what-heck-is-tsrmlscc-anyway.html)\n- [SDL bindings](https://pkg.go.dev/github.com/veandco/go-sdl2@v0.4.21/sdl#Main)\n\n## Recursos relacionados ao Docker\n\n- [Definição do arquivo Bake](https://docs.docker.com/build/customize/bake/file-definition/)\n- [`docker buildx build`](https://docs.docker.com/engine/reference/commandline/buildx_build/)\n\n## Comando útil\n\n```console\napk add strace util-linux gdb\nstrace -e 'trace=!futex,epoll_ctl,epoll_pwait,tgkill,rt_sigreturn' -p 1\n```\n\n## Traduzindo a documentação\n\nPara traduzir a documentação e o site para um novo idioma, siga estes passos:\n\n1. Crie um novo diretório com o código ISO de 2 caracteres do idioma no\n   diretório `docs/` deste repositório.\n2. Copie todos os arquivos `.md` da raiz do diretório `docs/` para o novo\n   diretório (sempre use a versão em inglês como fonte para tradução, pois está\n   sempre atualizada).\n3. Copie os arquivos `README.md` e `CONTRIBUTING.md` do diretório raiz para o\n   novo diretório.\n4. Traduza o conteúdo dos arquivos, mas não altere os nomes dos arquivos, nem\n   traduza strings que comecem com `> [!` (é uma marcação especial para o\n   GitHub).\n5. Crie um pull request com as traduções.\n6. No\n   [repositório do site](https://github.com/dunglas/frankenphp-website/tree/main),\n   copie e traduza os arquivos de tradução nos diretórios `content/`, `data/` e\n   `i18n/`.\n7. Traduza os valores no arquivo YAML criado.\n8. Abra um pull request no repositório do site.\n"
  },
  {
    "path": "docs/pt-br/README.md",
    "content": "# FrankenPHP: um moderno servidor de aplicações para PHP\n\n<h1 align=\"center\"><a href=\"https://frankenphp.dev/pt-br\"><img src=\"frankenphp.png\" alt=\"FrankenPHP\" width=\"600\"></a></h1>\n\nO FrankenPHP é um moderno servidor de aplicações para PHP, construído sobre o\nservidor web [Caddy](https://caddyserver.com/).\n\nO FrankenPHP dá superpoderes às suas aplicações PHP graças aos seus recursos\nimpressionantes: [_Early Hints_](early-hints.md), [modo worker](worker.md),\n[recursos em tempo real](mercure.md), suporte automático a HTTPS, HTTP/2 e\nHTTP/3...\n\nO FrankenPHP funciona com qualquer aplicação PHP e torna seus projetos Laravel e\nSymfony mais rápidos do que nunca, graças às suas integrações oficiais com o\nmodo worker.\n\nO FrankenPHP também pode ser usado como uma biblioteca Go independente para\nincorporar PHP em qualquer aplicação usando `net/http`.\n\n[**Saiba mais** em _frankenphp.dev_](https://frankenphp.dev/pt-br) e neste\nconjunto de slides:\n\n<a href=\"https://dunglas.dev/2022/10/frankenphp-the-modern-php-app-server-written-in-go/\"><img src=\"https://dunglas.dev/wp-content/uploads/2022/10/frankenphp.png\" alt=\"Slides\" width=\"600\"></a>\n\n## Começando\n\nNo Windows, use [WSL](https://learn.microsoft.com/pt-br/windows/wsl/) para\nexecutar o FrankenPHP.\n\n### Script de instalação\n\nVocê pode copiar esta linha no seu terminal para instalar automaticamente a\nversão apropriada para sua plataforma:\n\n```console\ncurl https://frankenphp.dev/install.sh | sh\n```\n\n### Binário independente\n\nFornecemos binários estáticos do FrankenPHP para desenvolvimento em Linux e macOS contendo o\n[PHP 8.4](https://www.php.net/releases/8.4/pt_BR.php) e as extensões PHP mais populares.\n\n[Baixe o FrankenPHP](https://github.com/php/frankenphp/releases)\n\n**Instalação de extensões:** As extensões mais comuns já estão incluídas. Não é possível instalar mais extensões.\n\n### Pacotes rpm\n\nNossos mantenedores oferecem pacotes rpm para todos os sistemas que usam `dnf`. Para instalar, execute:\n\n```console\nsudo dnf install https://rpm.henderkes.com/static-php-1-0.noarch.rpm\nsudo dnf module enable php-zts:static-8.4 # 8.2-8.5 disponíveis\nsudo dnf install frankenphp\n```\n\n**Instalação de extensões:** `sudo dnf install php-zts-<extension>`\n\nPara extensões não disponíveis por padrão, use o [PIE](https://github.com/php/pie):\n\n```console\nsudo dnf install pie-zts\nsudo pie-zts install asgrim/example-pie-extension\n```\n\n### Pacotes deb\n\nNossos mantenedores oferecem pacotes deb para todos os sistemas que usam `apt`. Para instalar, execute:\n\n```console\nsudo curl -fsSL https://key.henderkes.com/static-php.gpg -o /usr/share/keyrings/static-php.gpg && \\\necho \"deb [signed-by=/usr/share/keyrings/static-php.gpg] https://deb.henderkes.com/ stable main\" | sudo tee /etc/apt/sources.list.d/static-php.list && \\\nsudo apt update\nsudo apt install frankenphp\n```\n\n**Instalação de extensões:** `sudo apt install php-zts-<extension>`\n\nPara extensões não disponíveis por padrão, use o [PIE](https://github.com/php/pie):\n\n```console\nsudo apt install pie-zts\nsudo pie-zts install asgrim/example-pie-extension\n```\n\nPara servir o conteúdo do diretório atual, execute:\n\n```console\nfrankenphp php-server\n```\n\nVocê também pode executar scripts de linha de comando com:\n\n```console\nfrankenphp php-cli /caminho/para/seu/script.php\n```\n\n### Docker\n\nAlternativamente, [imagens Docker](docker.md) estão disponíveis:\n\n```console\ndocker run -v .:/app/public \\\n    -p 80:80 -p 443:443 -p 443:443/udp \\\n    dunglas/frankenphp\n```\n\nAcesse `https://localhost` e divirta-se!\n\n> [!TIP]\n>\n> Não tente usar `https://127.0.0.1`.\n> Use `https://localhost` e aceite o certificado autoassinado.\n> Use a\n> [variável de ambiente `SERVER_NAME`](config.md#variaveis-de-ambiente)\n> para alterar o domínio a ser usado.\n\n### Homebrew\n\nO FrankenPHP também está disponível como um pacote [Homebrew](https://brew.sh)\npara macOS e Linux.\n\nPara instalá-lo:\n\n```console\nbrew install dunglas/frankenphp/frankenphp\n```\n\n**Instalação de extensões:** Use o [PIE](https://github.com/php/pie).\n\n### Uso\n\nPara servir o conteúdo do diretório atual, execute:\n\n```console\nfrankenphp php-server\n```\n\nVocê também pode executar scripts de linha de comando com:\n\n```console\nfrankenphp php-cli /caminho/para/seu/script.php\n```\n\nPara os pacotes deb e rpm, você também pode iniciar o serviço systemd:\n\n```console\nsudo systemctl start frankenphp\n```\n\n## Documentação\n\n- [Modo clássico](classic.md)\n- [Modo worker](worker.md)\n- [Suporte a Early Hints (código de status HTTP 103)](early-hints.md)\n- [Tempo real](mercure.md)\n- [Servindo grandes arquivos estáticos com eficiência](x-sendfile.md)\n- [Configuração](config.md)\n- [Escrevendo extensões PHP em Go](extensions.md)\n- [Imagens Docker](docker.md)\n- [Implantação em produção](production.md)\n- [Otimização de desempenho](performance.md)\n- [Crie aplicações PHP **independentes** e autoexecutáveis](embed.md)\n- [Crie binários estáticos](static.md)\n- [Compile a partir do código-fonte](compile.md)\n- [Monitorando o FrankenPHP](metrics.md)\n- [Integração com Laravel](laravel.md)\n- [Problemas conhecidos](known-issues.md)\n- [Aplicação de demonstração (Symfony) e benchmarks](https://github.com/dunglas/frankenphp-demo)\n- [Documentação da biblioteca Go](https://pkg.go.dev/github.com/php/frankenphp)\n- [Contribuindo e depurando](CONTRIBUTING.md)\n\n## Exemplos e esqueletos\n\n- [Symfony](https://github.com/dunglas/symfony-docker)\n- [API Platform](https://api-platform.com/docs/symfony)\n- [Laravel](laravel.md)\n- [Sulu](https://sulu.io/blog/running-sulu-with-frankenphp)\n- [WordPress](https://github.com/StephenMiracle/frankenwp)\n- [Drupal](https://github.com/dunglas/frankenphp-drupal)\n- [Joomla](https://github.com/alexandreelise/frankenphp-joomla)\n- [TYPO3](https://github.com/ochorocho/franken-typo3)\n- [Magento2](https://github.com/ekino/frankenphp-magento2)\n"
  },
  {
    "path": "docs/pt-br/classic.md",
    "content": "# Usando o modo clássico\n\nSem nenhuma configuração adicional, o FrankenPHP opera no modo clássico.\nNeste modo, o FrankenPHP funciona como um servidor PHP tradicional, servindo\ndiretamente arquivos PHP.\nIsso o torna um substituto perfeito para PHP-FPM ou Apache com mod_php.\n\nSemelhante ao Caddy, o FrankenPHP aceita um número ilimitado de conexões e usa\num [número fixo de threads](config.md#configuracao-do-caddyfile) para servi-las.\nO número de conexões aceitas e enfileiradas é limitado apenas pelos recursos\ndisponíveis no sistema.\nO pool de threads do PHP opera com um número fixo de threads inicializadas na\ninicialização, comparável ao modo estático do PHP-FPM.\nTambém é possível permitir que as threads\n[escalem automaticamente em tempo de execução](performance.md#max_threads),\nsemelhante ao modo dinâmico do PHP-FPM.\n\nAs conexões enfileiradas aguardarão indefinidamente até que uma thread PHP\nesteja disponível para servi-las.\nPara evitar isso, você pode usar a\n[configuração](config.md#configuracao-do-caddyfile) `max_wait_time` na\nconfiguração global do FrankenPHP para limitar o tempo que uma requisição pode\nesperar por uma thread PHP livre antes de ser rejeitada.\nAlém disso, você pode definir um\n[tempo limite de escrita razoável no Caddy](https://caddyserver.com/docs/caddyfile/options#timeouts).\n\nCada instância do Caddy ativará apenas um pool de threads do FrankenPHP, que\nserá compartilhado entre todos os blocos `php_server`.\n"
  },
  {
    "path": "docs/pt-br/compile.md",
    "content": "# Compilar a partir do código-fonte\n\nEste documento explica como criar um binário FrankenPHP que carregará o PHP como\numa biblioteca dinâmica.\nEste é o método recomendado.\n\nComo alternativa, [compilações totalmente e principalmente estáticas](static.md)\ntambém podem ser criadas.\n\n## Instalar o PHP\n\nO FrankenPHP é compatível com PHP 8.2 e versões superiores.\n\n### Com o Homebrew (Linux e Mac)\n\nA maneira mais fácil de instalar uma versão da `libphp` compatível com o\nFrankenPHP é usar os pacotes ZTS fornecidos pelo\n[Homebrew PHP](https://github.com/shivammathur/homebrew-php).\n\nPrimeiro, se ainda não o fez, instale o [Homebrew](https://brew.sh).\n\nEm seguida, instale a variante ZTS do PHP, o Brotli (opcional, para suporte à\ncompressão) e o watcher (opcional, para detecção de alterações em arquivos):\n\n```console\nbrew install shivammathur/php/php-zts brotli watcher\nbrew link --overwrite --force shivammathur/php/php-zts\n```\n\n### Compilando o PHP\n\nAlternativamente, você pode compilar o PHP a partir do código-fonte com as\nopções necessárias para o FrankenPHP seguindo estes passos.\n\nPrimeiro, [obtenha o código-fonte do PHP](https://www.php.net/downloads.php) e\nextraia-os:\n\n```console\ntar xf php-*\ncd php-*/\n```\n\nEm seguida, execute o script `configure` com as opções necessárias para sua\nplataforma.\nAs seguintes flags `./configure` são obrigatórias, mas você pode adicionar\noutras, por exemplo, para compilar extensões ou recursos adicionais.\n\n#### Linux\n\n```console\n./configure \\\n    --enable-embed \\\n    --enable-zts \\\n    --disable-zend-signals \\\n    --enable-zend-max-execution-timers\n```\n\n#### Mac\n\nUse o gerenciador de pacotes [Homebrew](https://brew.sh/) para instalar as\ndependências necessárias e opcionais:\n\n```console\nbrew install libiconv bison brotli re2c pkg-config watcher\necho 'export PATH=\"/opt/homebrew/opt/bison/bin:$PATH\"' >> ~/.zshrc\n```\n\nEm seguida, execute o script `configure`:\n\n```console\n./configure \\\n    --enable-embed \\\n    --enable-zts \\\n    --disable-zend-signals \\\n    --with-iconv=/opt/homebrew/opt/libiconv/\n```\n\n#### Compilar o PHP\n\nFinalmente, compile e instale o PHP:\n\n```console\nmake -j\"$(getconf _NPROCESSORS_ONLN)\"\nsudo make install\n```\n\n## Instalar dependências opcionais\n\nAlguns recursos do FrankenPHP dependem de dependências opcionais do sistema que\ndevem ser instaladas.\nAlternativamente, esses recursos podem ser desabilitados passando as tags de\ncompilação para o compilador Go.\n\n| Recurso                               | Dependência                                                           | Tag de compilação para desabilitá-lo |\n| ------------------------------------- | --------------------------------------------------------------------- | ------------------------------------ |\n| Compressão Brotli                     | [Brotli](https://github.com/google/brotli)                            | `nobrotli`                           |\n| Reiniciar workers ao alterar arquivos | [Watcher C](https://github.com/e-dant/watcher/tree/release/watcher-c) | `nowatcher`                          |\n\n## Compilando a aplicação Go\n\nAgora você pode compilar o binário final.\n\n### Usando o `xcaddy`\n\nA maneira recomendada é usar o [`xcaddy`](https://github.com/caddyserver/xcaddy)\npara compilar o FrankenPHP.\nO `xcaddy` também permite adicionar facilmente\n[módulos Caddy personalizados](https://caddyserver.com/docs/modules/) e\nextensões FrankenPHP:\n\n```console\nCGO_ENABLED=1 \\\nXCADDY_GO_BUILD_FLAGS=\"-ldflags='-w -s' -tags=nobadger,nomysql,nopgx\" \\\nCGO_CFLAGS=$(php-config --includes) \\\nCGO_LDFLAGS=\"$(php-config --ldflags) $(php-config --libs)\" \\\nxcaddy build \\\n    --output frankenphp \\\n    --with github.com/php/frankenphp/caddy \\\n    --with github.com/dunglas/mercure/caddy \\\n    --with github.com/dunglas/vulcain/caddy\n    # Adicione módulos Caddy e extensões FrankenPHP extras aqui\n```\n\n> [!TIP]\n>\n> Se você estiver usando a `libc` `musl` (o padrão no Alpine Linux) e Symfony,\n> pode ser necessário aumentar o tamanho da pilha padrão.\n> Caso contrário, você poderá receber erros como `PHP Fatal error: Maximum call\nstack size of 83360 bytes reached during compilation.\nTry splitting expression`.\n>\n> Para fazer isso, altere a variável de ambiente `XCADDY_GO_BUILD_FLAGS` para\n> algo como\n> `XCADDY_GO_BUILD_FLAGS=$'-ldflags \"-w -s -extldflags \\'-Wl,-z,stack-size=0x80000\\'\"'`\n> (altere o valor do tamanho da pilha de acordo com as necessidades da sua\n> aplicação).\n\n### Sem o `xcaddy`\n\nAlternativamente, é possível compilar o FrankenPHP sem o `xcaddy` usando o\ncomando `go` diretamente:\n\n```console\ncurl -L https://github.com/php/frankenphp/archive/refs/heads/main.tar.gz | tar xz\ncd frankenphp-main/caddy/frankenphp\nCGO_CFLAGS=$(php-config --includes) CGO_LDFLAGS=\"$(php-config --ldflags) $(php-config --libs)\" go build -tags=nobadger,nomysql,nopgx\n```\n"
  },
  {
    "path": "docs/pt-br/config.md",
    "content": "# Configuração\n\nFrankenPHP, Caddy, bem como os módulos [Mercure](mercure.md) e [Vulcain](https://vulcain.rocks), podem ser configurados usando [os formatos suportados pelo Caddy](https://caddyserver.com/docs/getting-started#your-first-config).\n\nO formato mais comum é o `Caddyfile`, que é um formato de texto simples e legível por humanos.\nPor padrão, o FrankenPHP procurará por um `Caddyfile` no diretório atual.\nVocê pode especificar um caminho personalizado com a opção `-c` ou `--config`.\n\nUm `Caddyfile` mínimo para servir uma aplicação PHP é mostrado abaixo:\n\n```caddyfile\n# O nome do host para responder\nlocalhost\n\n# Opcionalmente, o diretório para servir arquivos, caso contrário, o padrão é o diretório atual\n#root public/\nphp_server\n```\n\nUm `Caddyfile` mais avançado, que habilita mais recursos e fornece variáveis de ambiente convenientes, é disponibilizado [no repositório FrankenPHP](https://github.com/php/frankenphp/blob/main/caddy/frankenphp/Caddyfile),\ne com as imagens Docker.\n\nO próprio PHP pode ser configurado [usando um arquivo `php.ini`](https://www.php.net/manual/en/configuration.file.php).\n\nDependendo do seu método de instalação, o FrankenPHP e o interpretador PHP procurarão por arquivos de configuração nos locais descritos abaixo.\n\n## Docker\n\nFrankenPHP:\n\n- `/etc/frankenphp/Caddyfile`: o arquivo de configuração principal\n- `/etc/frankenphp/Caddyfile.d/*.caddyfile`: arquivos de configuração adicionais que são carregados automaticamente\n\nPHP:\n\n- `php.ini`: `/usr/local/etc/php/php.ini` (nenhum `php.ini` é fornecido por padrão)\n- arquivos de configuração adicionais: `/usr/local/etc/php/conf.d/*.ini`\n- extensões PHP: `/usr/local/lib/php/extensions/no-debug-zts-<YYYYMMDD>/`\n- Você deve copiar um modelo oficial fornecido pelo projeto PHP:\n\n```dockerfile\nFROM dunglas/frankenphp\n\n# Produção:\nRUN cp $PHP_INI_DIR/php.ini-production $PHP_INI_DIR/php.ini\n\n# Ou desenvolvimento:\nRUN cp $PHP_INI_DIR/php.ini-development $PHP_INI_DIR/php.ini\n```\n\n## Pacotes RPM e Debian\n\nFrankenPHP:\n\n- `/etc/frankenphp/Caddyfile`: o arquivo de configuração principal\n- `/etc/frankenphp/Caddyfile.d/*.caddyfile`: arquivos de configuração adicionais que são carregados automaticamente\n\nPHP:\n\n- `php.ini`: `/etc/php-zts/php.ini` (um arquivo `php.ini` com predefinições de produção é fornecido por padrão)\n- arquivos de configuração adicionais: `/etc/php-zts/conf.d/*.ini`\n\n## Binário estático\n\nFrankenPHP:\n\n- No diretório de trabalho atual: `Caddyfile`\n\nPHP:\n\n- `php.ini`: O diretório no qual `frankenphp run` ou `frankenphp php-server` é executado, e então `/etc/frankenphp/php.ini`\n- arquivos de configuração adicionais: `/etc/frankenphp/php.d/*.ini`\n- extensões PHP: não podem ser carregadas, inclua-as no próprio binário\n- copie um dos arquivos `php.ini-production` ou `php.ini-development` fornecidos [nas fontes do PHP](https://github.com/php/php-src/).\n\n## Configuração do Caddyfile\n\nAs [diretivas HTTP](https://caddyserver.com/docs/caddyfile/concepts#directives) `php_server` ou `php` podem ser usadas dentro dos blocos de site para servir sua aplicação PHP.\n\nExemplo mínimo:\n\n```caddyfile\nlocalhost {\n\t# Habilita compressão (opcional)\n\tencode zstd br gzip\n\t# Executa arquivos PHP no diretório atual e serve assets\n\tphp_server\n}\n```\n\nVocê também pode configurar explicitamente o FrankenPHP usando a [opção global](https://caddyserver.com/docs/caddyfile/concepts#global-options) `frankenphp`:\n\n```caddyfile\n{\n\tfrankenphp {\n\t\tnum_threads <num_threads> # Define o número de threads PHP a serem iniciadas. Padrão: 2x o número de CPUs disponíveis.\n\t\tmax_threads <num_threads> # Limita o número de threads PHP adicionais que podem ser iniciadas em tempo de execução. Padrão: num_threads. Pode ser definido como 'auto'.\n\t\tmax_wait_time <duration> # Define o tempo máximo que uma requisição pode esperar por uma thread PHP livre antes de atingir o tempo limite. Padrão: desabilitado.\n\t\tmax_idle_time <duration> # Define o tempo máximo que uma thread autoescalada pode ficar ociosa antes de ser desativada. Padrão: 5s.\n\t\tphp_ini <key> <value> # Define uma diretiva php.ini. Pode ser usada várias vezes para definir múltiplas diretivas.\n\t\tworker {\n\t\t\tfile <path> # Define o caminho para o script do worker.\n\t\t\tnum <num> # Define o número de threads PHP a serem iniciadas, o padrão é 2x o número de CPUs disponíveis.\n\t\t\tenv <key> <value> # Define uma variável de ambiente extra para o valor fornecido. Pode ser especificada mais de uma vez para múltiplas variáveis de ambiente.\n\t\t\twatch <path> # Define o caminho para monitorar alterações em arquivos. Pode ser especificada mais de uma vez para múltiplos caminhos.\n\t\t\tname <name> # Define o nome do worker, usado em logs e métricas. Padrão: caminho absoluto do arquivo do worker\n\t\t\tmax_consecutive_failures <num> # Define o número máximo de falhas consecutivas antes que o worker seja considerado não saudável. -1 significa que o worker sempre reiniciará. Padrão: 6.\n\t\t}\n\t}\n}\n\n# ...\n```\n\nAlternativamente, você pode usar a forma abreviada de uma linha da opção `worker`:\n\n```caddyfile\n{\n\tfrankenphp {\n\t\tworker <file> <num>\n\t}\n}\n\n# ...\n```\n\nVocê também pode definir múltiplos workers se servir múltiplas aplicações no mesmo servidor:\n\n```caddyfile\napp.example.com {\n    root /path/to/app/public\n\tphp_server {\n\t\troot /path/to/app/public # permite melhor cache\n\t\tworker index.php <num>\n\t}\n}\n\nother.example.com {\n    root /path/to/other/public\n\tphp_server {\n\t\troot /path/to/other/public\n\t\tworker index.php <num>\n\t}\n}\n\n# ...\n```\n\nUsar a diretiva `php_server` é geralmente o que você precisa,\nmas se precisar de controle total, você pode usar a diretiva `php` de mais baixo nível.\nA diretiva `php` passa toda a entrada para o PHP, em vez de primeiro verificar\nse é um arquivo PHP ou não. Leia mais sobre isso na [página de desempenho](performance.md#try_files).\n\nUsar a diretiva `php_server` é equivalente a esta configuração:\n\n```caddyfile\nroute {\n\t# Adiciona barra final para requisições de diretório\n\t@canonicalPath {\n\t\tfile {path}/index.php\n\t\tnot path */\n\t}\n\tredir @canonicalPath {path}/ 308\n\t# Se o arquivo requisitado não existir, tenta os arquivos index\n\t@indexFiles file {\n\t\ttry_files {path} {path}/index.php index.php\n\t\tsplit_path .php\n\t}\n\trewrite @indexFiles {http.matchers.file.relative}\n\t# FrankenPHP!\n\t@phpFiles path *.php\n\tphp @phpFiles\n\tfile_server\n}\n```\n\nAs diretivas `php_server` e `php` têm as seguintes opções:\n\n```caddyfile\nphp_server [<matcher>] {\n\troot <directory> # Define a pasta raiz para o site. Padrão: diretiva `root`.\n\tsplit_path <delim...> # Define as substrings para dividir o URI em duas partes. A primeira substring correspondente será usada para separar as \"informações de caminho\" do caminho. A primeira parte é sufixada com a substring correspondente e será assumida como o nome real do recurso (script CGI). A segunda parte será definida como PATH_INFO para o script usar. Padrão: `.php`\n\tresolve_root_symlink false # Desabilita a resolução do diretório `root` para seu valor real avaliando um link simbólico, se houver (habilitado por padrão).\n\tenv <key> <value> # Define uma variável de ambiente extra para o valor fornecido. Pode ser especificada mais de uma vez para múltiplas variáveis de ambiente.\n\tfile_server off # Desabilita a diretiva integrada file_server.\n\tworker { # Cria um worker específico para este servidor. Pode ser especificada mais de uma vez para múltiplos workers.\n\t\tfile <path> # Define o caminho para o script do worker, pode ser relativo à raiz do php_server\n\t\tnum <num> # Define o número de threads PHP a serem iniciadas, o padrão é 2x o número de CPUs disponíveis.\n\t\tname <name> # Define o nome para o worker, usado em logs e métricas. Padrão: caminho absoluto do arquivo do worker. Sempre começa com m# quando definido em um bloco php_server.\n\t\twatch <path> # Define o caminho para monitorar alterações em arquivos. Pode ser especificada mais de uma vez para múltiplos caminhos.\n\t\tenv <key> <value> # Define uma variável de ambiente extra para o valor fornecido. Pode ser especificada mais de uma vez para múltiplas variáveis de ambiente. As variáveis de ambiente para este worker também são herdadas do pai do php_server, mas podem ser sobrescritas aqui.\n\t\tmatch <path> # Corresponde o worker a um padrão de caminho. Sobrescreve try_files e só pode ser usada na diretiva php_server.\n\t}\n\tworker <other_file> <num> # Também pode usar a forma abreviada, como no bloco global frankenphp.\n}\n```\n\n### Monitorando alterações em arquivos\n\nComo os workers inicializam sua aplicação apenas uma vez e a mantêm na memória,\nquaisquer alterações nos seus arquivos PHP não serão refletidas imediatamente.\n\nOs workers podem ser reiniciados em caso de alterações em arquivos por meio da diretiva `watch`.\nIsso é útil para ambientes de desenvolvimento.\n\n```caddyfile\n{\n\tfrankenphp {\n\t\tworker {\n\t\t\tfile  /path/to/app/public/worker.php\n\t\t\twatch\n\t\t}\n\t}\n}\n```\n\nEsta funcionalidade é frequentemente usada em combinação com [recarregamento a quente (hot reload)](hot-reload.md).\n\nSe o diretório `watch` não for especificado, ele usará o valor padrão `./**/*.{env,php,twig,yaml,yml}`,\nque monitora todos os arquivos `.env`, `.php`, `.twig`, `.yaml` e `.yml` no diretório e subdiretórios\nonde o processo FrankenPHP foi iniciado. Você também pode especificar um ou mais diretórios por meio de um\n[padrão de nome de arquivo shell](https://pkg.go.dev/path/filepath#Match):\n\n```caddyfile\n{\n\tfrankenphp {\n\t\tworker {\n\t\t\tfile  /path/to/app/public/worker.php\n\t\t\twatch /path/to/app # monitora todos os arquivos em todos os subdiretórios de /path/to/app\n\t\t\twatch /path/to/app/*.php # monitora arquivos terminados em .php em /path/to/app\n\t\t\twatch /path/to/app/**/*.php # monitora arquivos PHP em /path/to/app e subdiretórios\n\t\t\twatch /path/to/app/**/*.{php,twig} # monitora arquivos PHP e Twig em /path/to/app e subdiretórios\n\t\t}\n\t}\n}\n```\n\n- O padrão `**` significa monitoramento recursivo\n- Diretórios também podem ser relativos (ao local de início do processo FrankenPHP)\n- Se você tiver múltiplos workers definidos, todos eles serão reiniciados quando um arquivo for alterado\n- Tenha cuidado ao monitorar arquivos que são criados em tempo de execução (como logs), pois eles podem causar reinicializações indesejadas de workers.\n\nO monitor de arquivos é baseado em [e-dant/watcher](https://github.com/e-dant/watcher).\n\n## Correspondendo o worker a um caminho\n\nEm aplicações PHP tradicionais, scripts são sempre colocados no diretório público.\nIsso também é verdade para worker scripts, que são tratados como qualquer outro script PHP.\nSe você quiser, em vez disso, colocar o worker script fora do diretório público, pode fazê-lo via a diretiva `match`.\n\nA diretiva `match` é uma alternativa otimizada para `try_files`, disponível apenas dentro de `php_server` e `php`.\nO exemplo a seguir sempre servirá um arquivo no diretório público, se presente,\ne, caso contrário, encaminhará a requisição para o worker que corresponde ao padrão de caminho.\n\n```caddyfile\n{\n\tfrankenphp {\n\t\tphp_server {\n\t\t\tworker {\n\t\t\t\tfile /path/to/worker.php # o arquivo pode estar fora do caminho público\n\t\t\t\tmatch /api/* # todas as requisições que começam com /api/ serão tratadas por este worker\n\t\t\t}\n\t\t}\n\t}\n}\n```\n\n## Variáveis de ambiente\n\nAs seguintes variáveis de ambiente podem ser usadas para injetar diretivas Caddy no `Caddyfile` sem modificá-lo:\n\n- `SERVER_NAME`: altera [os endereços nos quais escutar](https://caddyserver.com/docs/caddyfile/concepts#addresses), os nomes de host fornecidos também serão usados para o certificado TLS gerado\n- `SERVER_ROOT`: altera o diretório raiz do site, o padrão é `public/`\n- `CADDY_GLOBAL_OPTIONS`: injeta [opções globais](https://caddyserver.com/docs/caddyfile/options)\n- `FRANKENPHP_CONFIG`: injeta a configuração sob a diretiva `frankenphp`\n\nAssim como para as SAPIs FPM e CLI, as variáveis de ambiente são expostas por padrão na superglobal `$_SERVER`.\n\nO valor `S` da [diretiva `variables_order` do PHP](https://www.php.net/manual/en/ini.core.php#ini.variables-order) é sempre equivalente a `ES` independentemente da colocação de `E` em outra parte desta diretiva.\n\n## Configuração do PHP\n\nPara carregar [arquivos de configuração adicionais do PHP](https://www.php.net/manual/en/configuration.file.php#configuration.file.scan),\na variável de ambiente `PHP_INI_SCAN_DIR` pode ser usada.\nQuando definida, o PHP carregará todos os arquivos com a extensão `.ini` presentes nos diretórios fornecidos.\n\nVocê também pode alterar a configuração do PHP usando a diretiva `php_ini` no `Caddyfile`:\n\n```caddyfile\n{\n    frankenphp {\n        php_ini memory_limit 256M\n\n        # ou\n\n        php_ini {\n            memory_limit 256M\n            max_execution_time 15\n        }\n    }\n}\n```\n\n### Desabilitando HTTPS\n\nPor padrão, o FrankenPHP habilitará automaticamente o HTTPS para todos os nomes de host, incluindo `localhost`.\nSe você quiser desabilitar o HTTPS (por exemplo, em um ambiente de desenvolvimento), você pode definir a variável de ambiente `SERVER_NAME` para `http://` ou `:80`:\n\nAlternativamente, você pode usar todos os outros métodos descritos na [documentação do Caddy](https://caddyserver.com/docs/automatic-https#activation).\n\nSe você quiser usar HTTPS com o endereço IP `127.0.0.1` em vez do nome de host `localhost`, por favor, leia a seção de [problemas conhecidos](known-issues.md#using-https127001-with-docker).\n\n### Full Duplex (HTTP/1)\n\nAo usar HTTP/1.x, pode ser desejável habilitar o modo full-duplex para permitir a gravação de uma resposta antes que o corpo inteiro\ntenha sido lido. (por exemplo: [Mercure](mercure.md), WebSocket, Server-Sent Events, etc.)\n\nEsta é uma configuração de adesão que precisa ser adicionada às opções globais no `Caddyfile`:\n\n```caddyfile\n{\n  servers {\n    enable_full_duplex\n  }\n}\n```\n\n> [!CAUTION]\n>\n> Habilitar esta opção pode causar deadlock em clientes HTTP/1.x antigos que não suportam full-duplex.\n> Isso também pode ser configurado usando a configuração de ambiente `CADDY_GLOBAL_OPTIONS`:\n\n```sh\nCADDY_GLOBAL_OPTIONS=\"servers {\n  enable_full_duplex\n}\"\n```\n\nVocê pode encontrar mais informações sobre esta configuração na [documentação do Caddy](https://caddyserver.com/docs/caddyfile/options#enable-full-duplex).\n\n## Habilitar o modo de depuração\n\nAo usar a imagem Docker, defina a variável de ambiente `CADDY_GLOBAL_OPTIONS` como `debug` para habilitar o modo de depuração:\n\n```console\ndocker run -v $PWD:/app/public \\\n    -e CADDY_GLOBAL_OPTIONS=debug \\\n    -p 80:80 -p 443:443 -p 443:443/udp \\\n    dunglas/frankenphp\n```\n\n## Autocompletar do Shell\n\nFrankenPHP oferece suporte integrado de autocompletar do shell para Bash, Zsh, Fish e PowerShell. Isso permite o preenchimento automático para todos os comandos (incluindo comandos personalizados como `php-server`, `php-cli` e `extension-init`) e suas flags.\n\n### Bash\n\nPara carregar o autocompletar na sua sessão atual do shell:\n\n```console\nsource <(frankenphp completion bash)\n```\n\nPara carregar o autocompletar para cada nova sessão, execute:\n\n**Linux:**\n\n```console\nfrankenphp completion bash > /usr/share/bash-completion/completions/frankenphp\n```\n\n**macOS:**\n\n```console\nfrankenphp completion bash > $(brew --prefix)/share/bash-completion/completions/frankenphp\n```\n\n### Zsh\n\nSe o autocompletar do shell ainda não estiver habilitado em seu ambiente, você precisará ativá-lo. Você pode executar o seguinte uma vez:\n\n```console\necho \"autoload -U compinit; compinit\" >> ~/.zshrc\n```\n\nPara carregar o autocompletar para cada sessão, execute uma vez:\n\n```console\nfrankenphp completion zsh > \"${fpath[1]}/_frankenphp\"\n```\n\nVocê precisará iniciar um novo shell para que esta configuração tenha efeito.\n\n### Fish\n\nPara carregar o autocompletar na sua sessão atual do shell:\n\n```console\nfrankenphp completion fish | source\n```\n\nPara carregar o autocompletar para cada nova sessão, execute uma vez:\n\n```console\nfrankenphp completion fish > ~/.config/fish/completions/frankenphp.fish\n```\n\n### PowerShell\n\nPara carregar o autocompletar na sua sessão atual do shell:\n\n```powershell\nfrankenphp completion powershell | Out-String | Invoke-Expression\n```\n\nPara carregar o autocompletar para cada nova sessão, execute uma vez:\n\n```powershell\nfrankenphp completion powershell | Out-File -FilePath (Join-Path (Split-Path $PROFILE) \"frankenphp.ps1\")\nAdd-Content -Path $PROFILE -Value '. (Join-Path (Split-Path $PROFILE) \"frankenphp.ps1\")'\n```\n\nVocê precisará iniciar um novo shell para que esta configuração tenha efeito.\n\nVocê precisará iniciar um novo shell para que esta configuração tenha efeito.\n"
  },
  {
    "path": "docs/pt-br/docker.md",
    "content": "# Construindo Imagens Docker Personalizadas\n\n[As imagens Docker do FrankenPHP](https://hub.docker.com/r/dunglas/frankenphp) são baseadas em [imagens oficiais do PHP](https://hub.docker.com/_/php/).\nVariantes do Debian e do Alpine Linux são fornecidas para arquiteturas populares.\nVariantes do Debian são recomendadas.\n\nVariantes para PHP 8.2, 8.3, 8.4 e 8.5 são fornecidas.\n\nAs tags seguem este padrão: `dunglas/frankenphp:<versao-do-frankenphp>-php<versao-do-php>-<os>`\n\n- `<versao-do-frankenphp>` e `<versao-do-php>` são números de versão do FrankenPHP e do PHP, respectivamente, variando de maior (ex.: `1`), menor (ex.: `1.2`) a versões de patch (ex.: `1.2.3`).\n- `<os>` é `trixie` (para Debian Trixie), `bookworm` (para Debian Bookworm) ou `alpine` (para a versão estável mais recente do Alpine).\n\n[Navegue pelas tags](https://hub.docker.com/r/dunglas/frankenphp/tags).\n\n## Como usar as imagens\n\nCrie um `Dockerfile` no seu projeto:\n\n```dockerfile\nFROM dunglas/frankenphp\n\nCOPY . /app/public\n```\n\nEm seguida, execute estes comandos para construir e executar a imagem Docker:\n\n```console\ndocker build -t minha-app-php .\ndocker run -it --rm --name minha-app-rodando minha-app-php\n```\n\n## Como ajustar a configuração\n\nPara sua conveniência, [um `Caddyfile` padrão](https://github.com/php/frankenphp/blob/main/caddy/frankenphp/Caddyfile) contendo\nvariáveis de ambiente úteis é fornecido na imagem.\n\n## Como instalar mais extensões PHP\n\nO script [`docker-php-extension-installer`](https://github.com/mlocati/docker-php-extension-installer) é fornecido na imagem base.\nAdicionar extensões PHP adicionais é simples:\n\n```dockerfile\nFROM dunglas/frankenphp\n\n# adicione extensões adicionais aqui:\nRUN install-php-extensions \\\n\tpdo_mysql \\\n\tgd \\\n\tintl \\\n\tzip \\\n\topcache\n```\n\n## Como instalar mais módulos Caddy\n\nO FrankenPHP é construído sobre o Caddy, e todos os [módulos Caddy](https://caddyserver.com/docs/modules/) podem ser usados com o FrankenPHP.\n\nA maneira mais fácil de instalar módulos Caddy personalizados é usar o [xcaddy](https://github.com/caddyserver/xcaddy):\n\n```dockerfile\nFROM dunglas/frankenphp:builder AS builder\n\n# Copia o xcaddy para a imagem do builder\nCOPY --from=caddy:builder /usr/bin/xcaddy /usr/bin/xcaddy\n\n# O CGO precisa estar habilitado para compilar o FrankenPHP\nRUN CGO_ENABLED=1 \\\n    XCADDY_SETCAP=1 \\\n    XCADDY_GO_BUILD_FLAGS=\"-ldflags='-w -s' -tags=nobadger,nomysql,nopgx\" \\\n    CGO_CFLAGS=$(php-config --includes) \\\n    CGO_LDFLAGS=\"$(php-config --ldflags) $(php-config --libs)\" \\\n    xcaddy build \\\n        --output /usr/local/bin/frankenphp \\\n        --with github.com/dunglas/frankenphp=./ \\\n        --with github.com/dunglas/frankenphp/caddy=./caddy/ \\\n        --with github.com/dunglas/caddy-cbrotli \\\n        # Mercure e Vulcain estão incluídos na compilação oficial, mas sinta-se\n        # à vontade para removê-los\n        --with github.com/dunglas/mercure/caddy \\\n        --with github.com/dunglas/vulcain/caddy\n        # Adicione módulos Caddy extras aqui\n\nFROM dunglas/frankenphp AS runner\n\n# Substitui o binário oficial pelo que contém seus módulos personalizados\nCOPY --from=builder /usr/local/bin/frankenphp /usr/local/bin/frankenphp\n```\n\nA imagem `builder` fornecida pelo FrankenPHP contém uma versão compilada da `libphp`.\n[Imagens de builder](https://hub.docker.com/r/dunglas/frankenphp/tags?name=builder) são fornecidas para todas as versões do FrankenPHP e do PHP, tanto para Debian quanto para Alpine.\n\n> [!TIP]\n>\n> Se você estiver usando Alpine Linux e Symfony, pode ser necessário\n> [aumentar o tamanho padrão da pilha](compile.md#using-xcaddy).\n\n## Habilitando o modo worker por padrão\n\nDefina a variável de ambiente `FRANKENPHP_CONFIG` para iniciar o FrankenPHP com um worker script:\n\n```dockerfile\nFROM dunglas/frankenphp\n\n# ...\n\nENV FRANKENPHP_CONFIG=\"worker ./public/index.php\"\n```\n\n## Usando um volume em desenvolvimento\n\nPara desenvolver facilmente com o FrankenPHP, monte o diretório do seu host que contém o código-fonte da aplicação como um volume no contêiner Docker:\n\n```console\ndocker run -v $PWD:/app/public -p 80:80 -p 443:443 -p 443:443/udp --tty minha-app-php\n```\n\n> [!TIP]\n>\n> A opção `--tty` permite ter logs legíveis por humanos em vez de logs JSON.\n\nCom o Docker Compose:\n\n```yaml\n# compose.yaml\n\nservices:\n  php:\n    image: dunglas/frankenphp\n    # descomente a linha a seguir se quiser usar um Dockerfile personalizado\n    #build: .\n    # descomente a linha a seguir se quiser executar isso em um ambiente de\n    # produção\n    # restart: always\n    ports:\n      - \"80:80\" # HTTP\n      - \"443:443\" # HTTPS\n      - \"443:443/udp\" # HTTP/3\n    volumes:\n      - ./:/app/public\n      - caddy_data:/data\n      - caddy_config:/config\n    # comente a linha a seguir em produção, isso permite ter logs legíveis em\n    # desenvolvimento\n    tty: true\n\n# Volumes necessários para certificados e configuração do Caddy\nvolumes:\n  caddy_data:\n  caddy_config:\n```\n\n## Executando como um usuário não root\n\nO FrankenPHP pode ser executado como um usuário não root no Docker.\n\nAqui está um exemplo de `Dockerfile` fazendo isso:\n\n```dockerfile\nFROM dunglas/frankenphp\n\nARG USER=appuser\n\nRUN \\\n\t# Use \"adduser -D ${USER}\" para distribuições baseadas em Alpine\n\tuseradd ${USER}; \\\n\t# Adiciona capacidade adicional para vincular às portas 80 e 443\n\tsetcap CAP_NET_BIND_SERVICE=+eip /usr/local/bin/frankenphp; \\\n\t# Concede acesso de escrita a /config/caddy e /data/caddy\n\tchown -R ${USER}:${USER} /config/caddy /data/caddy\n\nUSER ${USER}\n```\n\n### Executando sem capacidades\n\nMesmo executando sem root, o FrankenPHP precisa do recurso `CAP_NET_BIND_SERVICE` para vincular o\nservidor web em portas privilegiadas (80 e 443).\n\nSe você expor o FrankenPHP em uma porta não privilegiada (1024 e acima), é possível executar\no servidor web como um usuário não root e sem a necessidade de nenhuma capacidade:\n\n```dockerfile\nFROM dunglas/frankenphp\n\nARG USER=appuser\n\nRUN \\\n\t# Use \"adduser -D ${USER}\" para distribuições baseadas em Alpine\n\tuseradd ${USER}; \\\n\t# Remove a capacidade padrão\n\tsetcap -r /usr/local/bin/frankenphp; \\\n\t# Concede acesso de escrita a /config/caddy e /data/caddy\n\tchown -R ${USER}:${USER} /config/caddy /data/caddy\n\nUSER ${USER}\n```\n\nEm seguida, defina a variável de ambiente `SERVER_NAME` para usar uma porta sem privilégios.\nExemplo: `:8000`\n\n## Atualizações\n\nAs imagens Docker são construídas:\n\n- quando uma nova release é marcada (tagueada)\n- diariamente às 4h UTC, se novas versões das imagens oficiais do PHP estiverem disponíveis\n\n## Endurecendo Imagens\n\nPara reduzir ainda mais a superfície de ataque e o tamanho das suas imagens Docker do FrankenPHP, também é possível construí-las sobre uma\n[imagem Google distroless](https://github.com/GoogleContainerTools/distroless) ou\n[Docker hardened](https://www.docker.com/products/hardened-images).\n\n> [!WARNING]\n> Essas imagens base mínimas não incluem um shell ou gerenciador de pacotes, o que torna a depuração mais difícil.\n> Elas são, portanto, recomendadas apenas para produção se a segurança for uma alta prioridade.\n\nAo adicionar extensões PHP adicionais, você precisará de uma etapa de build intermediária:\n\n```dockerfile\nFROM dunglas/frankenphp AS builder\n\n# Adicione extensões PHP adicionais aqui\nRUN install-php-extensions pdo_mysql pdo_pgsql #...\n\n# Copia as bibliotecas compartilhadas do frankenphp e todas as extensões instaladas para um local temporário\n# Você também pode fazer esta etapa manualmente analisando a saída ldd do binário frankenphp e de cada arquivo .so da extensão\nRUN apt-get update && apt-get install -y libtree && \\\n    EXT_DIR=\"$(php -r 'echo ini_get(\"extension_dir\");')\" && \\\n    FRANKENPHP_BIN=\"$(which frankenphp)\"; \\\n    LIBS_TMP_DIR=\"/tmp/libs\"; \\\n    mkdir -p \"$LIBS_TMP_DIR\"; \\\n    for target in \"$FRANKENPHP_BIN\" $(find \"$EXT_DIR\" -maxdepth 2 -type f -name \"*.so\"); do \\\n        libtree -pv \"$target\" | sed 's/.*── \\(.*\\) \\[.*/\\1/' | grep -v \"^$target\" | while IFS= read -r lib; do \\\n            [ -z \"$lib\" ] && continue; \\\n            base=$(basename \"$lib\"); \\\n            destfile=\"$LIBS_TMP_DIR/$base\"; \\\n            if [ ! -f \"$destfile\" ]; then \\\n                cp \"$lib\" \"$destfile\"; \\\n            fi; \\\n        done; \\\n    done\n\n\n# Imagem base debian distroless, certifique-se de que esta é a mesma versão debian da imagem base\nFROM gcr.io/distroless/base-debian13\n# Alternativa de imagem Docker endurecida\n# FROM dhi.io/debian:13\n\n# Localização do seu aplicativo e Caddyfile a serem copiados para o contêiner\nARG PATH_TO_APP=\".\"\nARG PATH_TO_CADDYFILE=\"./Caddyfile\"\n\n# Copia seu aplicativo para /app\n# Para um endurecimento adicional, certifique-se de que apenas os caminhos graváveis são de propriedade do usuário não-root\nCOPY --chown=nonroot:nonroot \"$PATH_TO_APP\" /app\nCOPY \"$PATH_TO_CADDYFILE\" /etc/caddy/Caddyfile\n\n# Copia frankenphp e bibliotecas necessárias\nCOPY --from=builder /usr/local/bin/frankenphp /usr/local/bin/frankenphp\nCOPY --from=builder /usr/local/lib/php/extensions /usr/local/lib/php/extensions\nCOPY --from=builder /tmp/libs /usr/lib\n\n# Copia arquivos de configuração php.ini\nCOPY --from=builder /usr/local/etc/php/conf.d /usr/local/etc/php/conf.d\nCOPY --from=builder /usr/local/etc/php/php.ini-production /usr/local/etc/php/php.ini\n\n# Diretórios de dados do Caddy — devem ser graváveis para o usuário não-root, mesmo em um sistema de arquivos raiz somente leitura\nENV XDG_CONFIG_HOME=/config \\\n    XDG_DATA_HOME=/data\nCOPY --from=builder --chown=nonroot:nonroot /data/caddy /data/caddy\nCOPY --from=builder --chown=nonroot:nonroot /config/caddy /config/caddy\n\nUSER nonroot\n\nWORKDIR /app\n\n# Ponto de entrada para executar o frankenphp com o Caddyfile fornecido\nENTRYPOINT [\"/usr/local/bin/frankenphp\", \"run\", \"-c\", \"/etc/caddy/Caddyfile\"]\n```\n\n## Versões de Desenvolvimento\n\nAs versões de desenvolvimento estão disponíveis no repositório Docker [`dunglas/frankenphp-dev`](https://hub.docker.com/repository/docker/dunglas/frankenphp-dev).\nUma nova construção é acionada sempre que um commit é enviado para o branch principal do repositório do GitHub.\n\nAs tags `latest*` apontam para o HEAD do branch `main`.\nTags no formato `sha-<git-commit-hash>` também estão disponíveis.\n"
  },
  {
    "path": "docs/pt-br/early-hints.md",
    "content": "# Early Hints\n\nO FrankenPHP suporta nativamente o\n[código de status 103 Early Hints](https://developer.chrome.com/blog/early-hints/).\nUsar Early Hints pode melhorar o tempo de carregamento das suas páginas web em\n30%.\n\n```php\n<?php\n\nheader('Link: </style.css>; rel=preload; as=style');\nheaders_send(103);\n\n// seus algoritmos e consultas SQL lentos 🤪\n\necho <<<'HTML'\n<!DOCTYPE html>\n<title>Olá FrankenPHP</title>\n<link rel=\"stylesheet\" href=\"style.css\">\nHTML;\n```\n\nAs Early Hints são suportadas tanto pelo modo normal quanto pelo modo\n[worker](worker.md).\n"
  },
  {
    "path": "docs/pt-br/embed.md",
    "content": "# Aplicações PHP como binários independentes\n\nO FrankenPHP tem a capacidade de incorporar o código-fonte e os assets de\naplicações PHP em um binário estático e independente.\n\nGraças a esse recurso, aplicações PHP podem ser distribuídas como binários\nindependentes que incluem a própria aplicação, o interpretador PHP e o Caddy, um\nservidor web de nível de produção.\n\nSaiba mais sobre esse recurso\n[na apresentação feita por Kévin na SymfonyCon 2023](https://dunglas.dev/2023/12/php-and-symfony-apps-as-standalone-binaries/).\n\nPara incorporar aplicações Laravel,\n[leia esta entrada específica na documentação](laravel.md#aplicacoes-laravel-como-binarios-independentes).\n\n## Preparando sua aplicação\n\nAntes de criar o binário independente, certifique-se de que sua aplicação esteja\npronta para ser incorporada.\n\nPor exemplo, você provavelmente deseja:\n\n- Instalar as dependências de produção da aplicação;\n- Fazer o dump do carregador automático;\n- Habilitar o modo de produção da sua aplicação (se houver);\n- Remover arquivos desnecessários, como `.git` ou testes, para reduzir o tamanho\n  do seu binário final.\n\nPor exemplo, para uma aplicação Symfony, você pode usar os seguintes comandos:\n\n```console\n# Exporta o projeto para se livrar de .git/, etc.\nmkdir $TMPDIR/minha-aplicacao-preparada\ngit archive HEAD | tar -x -C $TMPDIR/minha-aplicacao-preparada\ncd $TMPDIR/minha-aplicacao-preparada\n\n# Define as variáveis de ambiente adequadas\necho APP_ENV=prod > .env.local\necho APP_DEBUG=0 >> .env.local\n\n# Remove os testes e outros arquivos desnecessários para economizar espaço.\n# Como alternativa, adicione esses arquivos com o atributo export-ignore no seu\n# arquivo .gitattributes.\nrm -Rf tests/\n\n# Instala as dependências\ncomposer install --ignore-platform-reqs --no-dev -a\n\n# Otimiza o arquivo .env\ncomposer dump-env prod\n```\n\n### Personalizando a configuração\n\nPara personalizar\n[a configuração](config.md), você pode colocar um arquivo `Caddyfile` e um\narquivo `php.ini` no diretório principal da aplicação a ser incorporada\n(`$TMPDIR/minha-aplicacao-preparada` no exemplo anterior).\n\n## Criando um binário do Linux\n\nA maneira mais fácil de criar um binário do Linux é usar o builder baseado em\nDocker que fornecemos.\n\n1. Crie um arquivo chamado `static-build.Dockerfile` no repositório da sua\n   aplicação:\n\n   ```dockerfile\n   FROM --platform=linux/amd64 dunglas/frankenphp:static-builder-gnu\n   # Se você pretende executar o binário em sistemas musl-libc, use o static-builder-musl\n\n   # Copia sua aplicação\n   WORKDIR /go/src/app/dist/app\n   COPY . .\n\n   # Compila o binário estático\n   WORKDIR /go/src/app/\n   RUN EMBED=dist/app/ ./build-static.sh\n   ```\n\n   > [!CAUTION]\n   >\n   > Alguns arquivos `.dockerignore` (por exemplo, o\n   > [`.dockerignore` padrão do Docker do Symfony](https://github.com/dunglas/symfony-docker/blob/main/.dockerignore))\n   > ignorarão o diretório `vendor/` e os arquivos `.env`.\n   > Certifique-se de ajustar ou remover o arquivo `.dockerignore` antes da\n   > construção.\n\n2. Construa:\n\n   ```console\n   docker build -t aplicacao-estatica -f static-build.Dockerfile .\n   ```\n\n3. Extraia o binário:\n\n   ```console\n   docker cp $(docker create --name aplicacao-estatica-tmp aplicacao-estatica):/go/src/app/dist/frankenphp-linux-x86_64 minha-aplicacao ; docker rm aplicacao-estatica-tmp\n   ```\n\nO binário resultante é o arquivo `minha-aplicacao` no diretório atual.\n\n## Criando um binário para outros sistemas operacionais\n\nSe você não quiser usar o Docker ou quiser compilar um binário para macOS, use o\nscript de shell que fornecemos:\n\n```console\ngit clone https://github.com/php/frankenphp\ncd frankenphp\nEMBED=/caminho/para/sua/aplicacao ./build-static.sh\n```\n\nO binário resultante é o arquivo `frankenphp-<os>-<arch>` no diretório `dist/`.\n\n## Usando o binário\n\nÉ isso! O arquivo `minha-aplicacao` (ou `dist/frankenphp-<os>-<arch>` em outros\nsistemas operacionais) contém sua aplicação independente!\n\nPara iniciar a aplicação web, execute:\n\n```console\n./minha-aplicacao php-server\n```\n\nSe a sua aplicação contiver um [worker script](worker.md), inicie o worker com\nalgo como:\n\n```console\n./minha-aplicacao php-server --worker public/index.php\n```\n\nPara habilitar HTTPS (um certificado Let's Encrypt é criado automaticamente),\nHTTP/2 e HTTP/3, especifique o nome de domínio a ser usado:\n\n```console\n./minha-aplicacao php-server --domain localhost\n```\n\nVocê também pode executar os scripts PHP CLI incorporados ao seu binário:\n\n```console\n./minha-aplicacao php-cli bin/console\n```\n\n## Extensões PHP\n\nPor padrão, o script compilará as extensões requeridas pelo arquivo\n`composer.json` do seu projeto, se houver.\nSe o arquivo `composer.json` não existir, as extensões padrão serão compiladas,\nconforme documentado na [entrada de compilações estáticas](static.md).\n\nPara personalizar as extensões, use a variável de ambiente `PHP_EXTENSIONS`.\n\n## Personalizando a compilação\n\n[Leia a documentação da compilação estática](static.md) para ver como\npersonalizar o binário (extensões, versão do PHP...).\n\n## Distribuindo o binário\n\nNo Linux, o binário criado é compactado usando [UPX](https://upx.github.io).\n\nNo Mac, para reduzir o tamanho do arquivo antes de enviá-lo, você pode\ncompactá-lo.\nRecomendamos usar `xz`.\n"
  },
  {
    "path": "docs/pt-br/extension-workers.md",
    "content": "# Workers de Extensão\n\nOs Workers de Extensão permitem que sua [extensão FrankenPHP](https://frankenphp.dev/docs/extensions/) gerencie um pool dedicado de threads PHP para executar tarefas em segundo plano, lidar com eventos assíncronos ou implementar protocolos personalizados. Útil para sistemas de fila, listeners de eventos, agendadores, etc.\n\n## Registrando o Worker\n\n### Registro Estático\n\nSe você não precisa que o worker seja configurável pelo usuário (caminho de script fixo, número de threads fixo), você pode simplesmente registrar o worker na função `init()`.\n\n```go\npackage myextension\n\nimport (\n\t\"github.com/dunglas/frankenphp\"\n\t\"github.com/dunglas/frankenphp/caddy\"\n)\n\n// Handle global para comunicar com o pool de workers\nvar worker frankenphp.Workers\n\nfunc init() {\n\t// Registra o worker quando o módulo é carregado.\n\tworker = caddy.RegisterWorkers(\n\t\t\"my-internal-worker\", // Nome único\n\t\t\"worker.php\",         // Caminho do script (relativo à execução ou absoluto)\n\t\t2,                    // Contagem fixa de threads\n\t\t// Hooks de ciclo de vida opcionais\n\t\tfrankenphp.WithWorkerOnServerStartup(func() {\n\t\t\t// Lógica de configuração global...\n\t\t}),\n\t)\n}\n```\n\n### Em um Módulo Caddy (Configurável pelo usuário)\n\nSe você planeja compartilhar sua extensão (como uma fila genérica ou um listener de eventos), você deve encapsulá-la em um módulo Caddy. Isso permite que os usuários configurem o caminho do script e a contagem de threads através do seu `Caddyfile`. Isso exige a implementação da interface `caddy.Provisioner` e a análise do Caddyfile ([veja um exemplo](https://github.com/dunglas/frankenphp-queue/blob/989120d394d66dd6c8e2101cac73dd622fade334/caddy.go)).\n\n### Em uma Aplicação Go Pura (Embedagem)\n\nSe você está [embedando o FrankenPHP em uma aplicação Go padrão sem caddy](https://pkg.go.dev/github.com/dunglas/frankenphp#example-ServeHTTP), você pode registrar workers de extensão usando `frankenphp.WithExtensionWorkers` ao inicializar as opções.\n\n## Interagindo com Workers\n\nAssim que o pool de workers estiver ativo, você pode despachar tarefas para ele. Isso pode ser feito dentro de [funções nativas exportadas para PHP](https://frankenphp.dev/docs/extensions/#writing-the-extension), ou de qualquer lógica Go, como um agendador cron, um listener de eventos (MQTT, Kafka), ou qualquer outra goroutine.\n\n### Modo Headless: `SendMessage`\n\nUse `SendMessage` para passar dados brutos diretamente para o seu script worker. Isso é ideal para filas ou comandos simples.\n\n#### Exemplo: Uma Extensão de Fila Assíncrona\n\n```go\n// #include <Zend/zend_types.h>\nimport \"C\"\nimport (\n\t\"context\"\n\t\"unsafe\"\n\t\"github.com/dunglas/frankenphp\"\n)\n\n//export_php:function my_queue_push(mixed $data): bool\nfunc my_queue_push(data *C.zval) bool {\n\t// 1. Garante que o worker esteja pronto\n\tif worker == nil {\n\t\treturn false\n\t}\n\n\t// 2. Despacha para o worker em segundo plano\n\t_, err := worker.SendMessage(\n\t\tcontext.Background(), // Contexto Go padrão\n\t\tunsafe.Pointer(data), // Dados a serem passados para o worker\n\t\tnil, // http.ResponseWriter opcional\n\t)\n\n\treturn err == nil\n}\n```\n\n### Emulação HTTP: `SendRequest`\n\nUse `SendRequest` se sua extensão precisar invocar um script PHP que espera um ambiente web padrão (populando `$_SERVER`, `$_GET`, etc.).\n\n```go\n// #include <Zend/zend_types.h>\nimport \"C\"\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"unsafe\"\n\t\"github.com/dunglas/frankenphp\"\n)\n\n//export_php:function my_worker_http_request(string $path): string\nfunc my_worker_http_request(path *C.zend_string) unsafe.Pointer {\n\t// 1. Prepara a requisição e o gravador\n\turl := frankenphp.GoString(unsafe.Pointer(path))\n\treq, _ := http.NewRequest(\"GET\", url, http.NoBody)\n\trr := httptest.NewRecorder()\n\n\t// 2. Despacha para o worker\n\tif err := worker.SendRequest(rr, req); err != nil {\n\t\treturn nil\n\t}\n\n\t// 3. Retorna a resposta capturada\n\treturn frankenphp.PHPString(rr.Body.String(), false)\n}\n```\n\n## Script do Worker\n\nO script PHP do worker é executado em um loop e pode lidar tanto com mensagens brutas quanto com requisições HTTP.\n\n```php\n<?php\n// Lida tanto com mensagens brutas quanto com requisições HTTP no mesmo loop\n$handler = function ($payload = null) {\n    // Caso 1: Modo de Mensagem\n    if ($payload !== null) {\n        return \"Received payload: \" . $payload;\n    }\n\n    // Caso 2: Modo HTTP (superglobais PHP padrão são populadas)\n    echo \"Hello from page: \" . $_SERVER['REQUEST_URI'];\n};\n\nwhile (frankenphp_handle_request($handler)) {\n    gc_collect_cycles();\n}\n```\n\n## Hooks de Ciclo de Vida\n\nFrankenPHP oferece hooks para executar código Go em pontos específicos do ciclo de vida.\n\n| Tipo de Hook | Nome da Opção                  | Assinatura           | Contexto e Caso de Uso                                                     |\n| :--------- | :--------------------------- | :------------------- | :------------------------------------------------------------------------- |\n| **Servidor** | `WithWorkerOnServerStartup`  | `func()`             | Configuração global. Executado **Uma Vez**. Exemplo: Conectar ao NATS/Redis. |\n| **Servidor** | `WithWorkerOnServerShutdown` | `func()`             | Limpeza global. Executado **Uma Vez**. Exemplo: Fechar conexões compartilhadas. |\n| **Thread** | `WithWorkerOnReady`          | `func(threadID int)` | Configuração por thread. Chamado quando um thread inicia. Recebe o ID do Thread. |\n| **Thread** | `WithWorkerOnShutdown`       | `func(threadID int)` | Limpeza por thread. Recebe o ID do Thread.                                  |\n\n### Exemplo\n\n```go\npackage myextension\n\nimport (\n    \"fmt\"\n    \"github.com/dunglas/frankenphp\"\n    frankenphpCaddy \"github.com/dunglas/frankenphp/caddy\"\n)\n\nfunc init() {\n    workerHandle = frankenphpCaddy.RegisterWorkers(\n        \"my-worker\", \"worker.php\", 2,\n\n        // Inicialização do Servidor (Global)\n        frankenphp.WithWorkerOnServerStartup(func() {\n            fmt.Println(\"Extensão: Servidor iniciando...\")\n        }),\n\n        // Thread Pronta (Por Thread)\n        // Nota: A função aceita um inteiro representando o ID do Thread\n        frankenphp.WithWorkerOnReady(func(id int) {\n            fmt.Printf(\"Extensão: Thread worker #%d está pronta.\\n\", id)\n        }),\n    )\n}\n```\n"
  },
  {
    "path": "docs/pt-br/extensions.md",
    "content": "# Escrevendo extensões PHP em Go\n\nCom o FrankenPHP, você pode **escrever extensões PHP em Go**, o que permite\ncriar **funções nativas de alto desempenho** que podem ser chamadas diretamente\ndo PHP.\nSuas aplicações podem aproveitar qualquer biblioteca Go existente ou nova, bem\ncomo o famoso modelo de concorrência de **goroutines diretamente do seu código\nPHP**.\n\nEscrever extensões PHP normalmente é feito em C, mas também é possível\nescrevê-las em outras linguagens com um pouco de trabalho extra.\nAs extensões PHP permitem que você aproveite o poder das linguagens de baixo\nnível para estender as funcionalidades do PHP, por exemplo, adicionando funções\nnativas ou otimizando operações específicas.\n\nGraças aos módulos Caddy, você pode escrever extensões PHP em Go e integrá-las\nrapidamente ao FrankenPHP.\n\n## Duas abordagens\n\nO FrankenPHP oferece duas maneiras de criar extensões PHP em Go:\n\n1. **Usando o gerador de extensões** - A abordagem recomendada que gera todo o\n   código boilerplate necessário para a maioria dos casos de uso, permitindo que\n   você se concentre em escrever seu código em Go.\n2. **Implementação manual** - Controle total sobre a estrutura da extensão para\n   casos de uso avançados.\n\nComeçaremos com a abordagem do gerador, pois é a maneira mais fácil de começar,\ne, em seguida, mostraremos a implementação manual para aqueles que precisam de\ncontrole total.\n\n## Usando o gerador de extensões\n\nO FrankenPHP vem com uma ferramenta que permite **criar uma extensão PHP**\nusando apenas Go.\n**Não é necessário escrever código C** ou usar CGO diretamente: o FrankenPHP\ntambém inclui uma **API de tipos pública** para ajudar você a escrever suas\nextensões em Go sem ter que se preocupar com **o malabarismo de tipos entre\nPHP/C e Go**.\n\n> [!TIP]\n> Se quiser entender como as extensões podem ser escritas em Go do zero, leia a\n> seção de implementação manual abaixo, que demonstra como escrever uma extensão\n> PHP em Go sem usar o gerador.\n\nLembre-se de que esta ferramenta **não é um gerador de extensões completo**.\nEla foi criada para ajudar você a escrever extensões simples em Go, mas não\noferece os recursos mais avançados das extensões PHP.\nSe precisar escrever uma extensão mais **complexa e otimizada**, talvez seja\nnecessário escrever algum código em C ou usar CGO diretamente.\n\n### Pré-requisitos\n\nConforme abordado na seção de implementação manual abaixo, você precisa\n[obter o código-fonte do PHP](https://www.php.net/downloads.php) e criar um novo\nmódulo Go.\n\n#### Criando um novo módulo e obtendo o código-fonte do PHP\n\nO primeiro passo para escrever uma extensão PHP em Go é criar um novo módulo Go.\nVocê pode usar o seguinte comando para isso:\n\n```console\ngo mod init github.com/<minha-conta>/<meu-modulo>\n```\n\nO segundo passo é\n[obter o código-fonte do PHP](https://www.php.net/downloads.php) para os\npróximos passos.\nDepois de obtê-los, descompacte-os no diretório de sua escolha, não dentro do\nseu módulo Go:\n\n```console\ntar xf php-*\n```\n\n### Escrevendo a extensão\n\nAgora tudo está configurado para escrever sua função nativa em Go.\nCrie um novo arquivo chamado `stringext.go`.\nNossa primeira função receberá uma string como argumento, o número de vezes que\nela será repetida, um booleano para indicar se a string deve ser invertida e\nretornará a string resultante.\nDeve ficar assim:\n\n```go\nimport (\n    \"C\"\n    \"github.com/dunglas/frankenphp\"\n    \"strings\"\n)\n\n//export_php:function repeat_this(string $str, int $count, bool $reverse): string\nfunc repeat_this(s *C.zend_string, count int64, reverse bool) unsafe.Pointer {\n    str := frankenphp.GoString(unsafe.Pointer(s))\n\n    result := strings.Repeat(str, int(count))\n    if reverse {\n        runes := []rune(result)\n        for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {\n            runes[i], runes[j] = runes[j], runes[i]\n        }\n        result = string(runes)\n    }\n\n    return frankenphp.PHPString(result, false)\n}\n```\n\nHá duas coisas importantes a serem observadas aqui:\n\n- Um comentário de diretiva `//export_php:function` define a assinatura da\n  função no PHP.\n  É assim que o gerador sabe como gerar a função PHP com os parâmetros e o tipo\n  de retorno corretos;\n- A função deve retornar um `unsafe.Pointer`.\n  O FrankenPHP fornece uma API para ajudar você com o malabarismo de tipos entre\n  C e Go.\n\nEmbora o primeiro ponto fale por si, o segundo pode ser mais difícil de\nentender.\nVamos nos aprofundar no malabarismo de tipos na próxima seção.\n\n### Malabarismo de tipos\n\nEmbora alguns tipos de variáveis tenham a mesma representação de memória entre\nC/PHP e Go, alguns tipos exigem mais lógica para serem usados diretamente.\nEsta talvez seja a parte mais difícil quando se trata de escrever extensões,\npois requer a compreensão dos componentes internos da Zend Engine e de como as\nvariáveis são armazenadas internamente no PHP.\nEsta tabela resume o que você precisa saber:\n\n| Tipo PHP           | Tipo Go                       | Conversão direta | Auxiliar de C para Go             | Auxiliar de Go para C              | Suporte a métodos de classe |\n| ------------------ | ----------------------------- | ---------------- | --------------------------------- | ---------------------------------- | --------------------------- |\n| `int`              | `int64`                       | ✅               | -                                 | -                                  | ✅                          |\n| `?int`             | `*int64`                      | ✅               | -                                 | -                                  | ✅                          |\n| `float`            | `float64`                     | ✅               | -                                 | -                                  | ✅                          |\n| `?float`           | `*float64`                    | ✅               | -                                 | -                                  | ✅                          |\n| `bool`             | `bool`                        | ✅               | -                                 | -                                  | ✅                          |\n| `?bool`            | `*bool`                       | ✅               | -                                 | -                                  | ✅                          |\n| `?bool`            | `*bool`                       | ✅               | -                                 | -                                  | ✅                          |\n| `string`/`?string` | `*C.zend_string`              | ❌               | `frankenphp.GoString()`           | `frankenphp.PHPString()`           | ✅                          |\n| `array`            | `frankenphp.AssociativeArray` | ❌               | `frankenphp.GoAssociativeArray()` | `frankenphp.PHPAssociativeArray()` | ✅                          |\n| `array`            | `map[string]any`              | ❌               | `frankenphp.GoMap()`              | `frankenphp.PHPMap()`              | ✅                          |\n| `array`            | `[]any`                       | ❌               | `frankenphp.GoPackedArray()`      | `frankenphp.PHPPackedArray()`      | ✅                          |\n| `mixed`            | `any`                         | ❌               | `GoValue()`                       | `PHPValue()`                       | ❌                          |\n| `object`           | `struct`                      | ❌               | _Ainda não implementado_          | _Ainda não implementado_           | ❌                          |\n\n> [!NOTE]\n> Esta tabela ainda não é exaustiva e será completada à medida que a API de\n> tipos do FrankenPHP se tornar mais completa.\n>\n> Tipos primitivos e arrays são suportados atualmente, especificamente para\n> métodos de classe.\n> Objetos ainda não podem ser usados como parâmetros de métodos ou tipos de\n> retorno.\n\nSe você consultar o trecho de código da seção anterior, poderá ver que os\nauxiliares são usados para converter o primeiro parâmetro e o valor de retorno.\nO segundo e o terceiro parâmetros da nossa função `repeat_this()` não precisam\nser convertidos, pois a representação em memória dos tipos subjacentes é a mesma\npara C e Go.\n\n#### Trabalhando com arrays\n\nO FrankenPHP oferece suporte nativo para arrays PHP por meio de\n`frankenphp.AssociativeArray` ou conversão direta para um mapa ou slice.\n\n`AssociativeArray` representa um\n[hashmap](https://en.wikipedia.org/wiki/Hash_table) composto por um campo\n`Map: map[string]any` e um campo opcional `Order: []string` (ao contrário dos\narrays associativos do PHP, os mapas em Go não são ordenados).\n\nSe a ordem ou a associação não forem necessárias, também é possível converter\ndiretamente para um slice `[]any` ou um mapa não ordenado `map[string]any`.\n\n**Criando e manipulando arrays em Go:**\n\n```go\n//export_php:function process_data_ordered(array $input): array\nfunc process_data_ordered_map(arr *C.zval) unsafe.Pointer {\n    // Converte um array associativo PHP para Go, mantendo a ordem\n    associativeArray := frankenphp.GoAssociativeArray(unsafe.Pointer(arr))\n\n    // percorre as entradas em ordem\n    for _, key := range associativeArray.Order {\n        value, _ = associativeArray.Map[key]\n        // faz algo com a chave e o valor\n    }\n\n    // retorna um array ordenado\n    // se 'Order' não estiver vazio, apenas os pares chave-valor em 'Order'\n    // serão respeitados\n    return frankenphp.PHPAssociativeArray(AssociativeArray{\n        Map: map[string]any{\n            \"chave1\": \"valor1\",\n            \"chave2\": \"valor2\",\n        },\n        Order: []string{\"chave1\", \"chave2\"},\n    })\n}\n\n//export_php:function process_data_unordered(array $input): array\nfunc process_data_unordered_map(arr *C.zval) unsafe.Pointer {\n    // Converte um array associativo PHP em um mapa Go sem manter a ordem\n    // Ignorar a ordem terá melhor desempenho\n    goMap := frankenphp.GoMap(unsafe.Pointer(arr))\n\n    // percorre as entradas sem nenhuma ordem específica\n    for key, value := range goMap {\n        // faz algo com a chave e o valor\n    }\n\n    // retorna um array não ordenado\n    return frankenphp.PHPMap(map[string]any{\n        \"chave1\": \"valor1\",\n        \"chave2\": \"valor2\",\n    })\n}\n\n//export_php:function process_data_packed(array $input): array\nfunc process_data_packed(arr *C.zval) unsafe.Pointer {\n    // Converte um array compactado PHP para Go\n    goSlice := frankenphp.GoPackedArray(unsafe.Pointer(arr), false)\n\n    // percorre o slice em ordem\n    for index, value := range goSlice {\n        // faz algo com a chave e o valor\n    }\n\n    // retorna um array compactado\n    return frankenphp.PHPackedArray([]any{\"valor1\", \"valor2\", \"value3\"})\n}\n```\n\n**Principais recursos da conversão de arrays:**\n\n- **Pares chave-valor ordenados** - Opção para manter a ordem do array\n  associativo;\n- **Otimizado para múltiplos casos** - Opção para ignorar a ordem para melhor\n  desempenho ou converter diretamente para um slice;\n- **Detecção automática de listas** - Ao converter para PHP, detecta\n  automaticamente se o array deve ser uma lista compactada ou um hashmap;\n- **Arrays aninhados** - Os arrays podem ser aninhados e converterão todos os\n  tipos suportados automaticamente (`int64`, `float64`, `string`, `bool`, `nil`,\n  `AssociativeArray`, `map[string]any`, `[]any`);\n- **Objetos não são suportados** - Atualmente, apenas tipos escalares e arrays\n  podem ser usados como valores.\n  Fornecer um objeto resultará em um valor `null` no array PHP.\n\n##### Métodos disponíveis: empacotado e associativo\n\n- `frankenphp.PHPAssociativeArray(arr frankenphp.AssociativeArray) unsafe.Pointer`\n  \\- Converte para um array PHP ordenado com pares chave-valor;\n- `frankenphp.PHPMap(arr map[string]any) unsafe.Pointer` - Converte um mapa em\n  um array PHP não ordenado com pares chave-valor;\n- `frankenphp.PHPPackedArray(slice []any) unsafe.Pointer` - Converte um slice\n  em um array PHP compactado apenas com valores indexados;\n- `frankenphp.GoAssociativeArray(arr unsafe.Pointer, ordered bool) frankenphp.AssociativeArray`\n  \\- Converte um array PHP em um `AssociativeArray` Go ordenado (mapa com ordem);\n- `frankenphp.GoMap(arr unsafe.Pointer) map[string]any` - Converte um array PHP\n  em um mapa Go não ordenado;\n- `frankenphp.GoPackedArray(arr unsafe.Pointer) []any` - Converte um array PHP\n  em um slice Go.\n\n### Declarando uma classe PHP nativa\n\nO gerador suporta a declaração de **classes opacas** como estruturas Go, que\npodem ser usadas para criar objetos PHP.\nVocê pode usar o comentário de diretiva `//export_php:class` para definir uma\nclasse PHP.\nPor exemplo:\n\n```go\n//export_php:class User\ntype UserStruct struct {\n    Name string\n    Age  int\n}\n```\n\n#### O que são classes opacas?\n\n**Classes Opacas** são classes cuja estrutura interna (propriedades) é ocultada\ndo código PHP.\nIsso significa:\n\n- **Sem acesso direto às propriedades**: Você não pode ler ou escrever\n  propriedades diretamente do PHP (`$user->name` não funcionará);\n- **Interface somente para métodos** - Todas as interações devem passar pelos\n  métodos que você definir;\n- **Melhor encapsulamento** - A estrutura interna de dados é completamente\n  controlada pelo código Go;\n- **Segurança de tipos** - Sem risco do código PHP corromper o estado interno\n  com tipos incorretos;\n- **API mais limpa** - Força o design de uma interface pública adequada.\n\nEssa abordagem fornece melhor encapsulamento e evita que o código PHP corrompa\nacidentalmente o estado interno dos seus objetos Go.\nTodas as interações com o objeto devem passar pelos métodos que você definir\nexplicitamente.\n\n#### Adicionando métodos às classes\n\nComo as propriedades não são diretamente acessíveis, você **deve definir\nmétodos** para interagir com suas classes opacas.\nUse a diretiva `//export_php:method` para definir o comportamento:\n\n```go\n//export_php:class User\ntype UserStruct struct {\n    Name string\n    Age  int\n}\n\n//export_php:method User::getName(): string\nfunc (us *UserStruct) GetUserName() unsafe.Pointer {\n    return frankenphp.PHPString(us.Name, false)\n}\n\n//export_php:method User::setAge(int $age): void\nfunc (us *UserStruct) SetUserAge(age int64) {\n    us.Age = int(age)\n}\n\n//export_php:method User::getAge(): int\nfunc (us *UserStruct) GetUserAge() int64 {\n    return int64(us.Age)\n}\n\n//export_php:method User::setNamePrefix(string $prefix = \"User\"): void\nfunc (us *UserStruct) SetNamePrefix(prefix *C.zend_string) {\n    us.Name = frankenphp.GoString(unsafe.Pointer(prefix)) + \": \" + us.Name\n}\n```\n\n#### Parâmetros anuláveis\n\nO gerador suporta parâmetros anuláveis usando o prefixo `?` em assinaturas PHP.\nQuando um parâmetro é anulável, ele se torna um ponteiro na sua função Go,\npermitindo que você verifique se o valor era `null` no PHP:\n\n```go\n//export_php:method User::updateInfo(?string $name, ?int $age, ?bool $active): void\nfunc (us *UserStruct) UpdateInfo(name *C.zend_string, age *int64, active *bool) {\n    // Verifica se o parâmetro name foi fornecido (não nulo)\n    if name != nil {\n        us.Name = frankenphp.GoString(unsafe.Pointer(name))\n    }\n\n    // Verifica se o parâmetro age foi fornecido (não nulo)\n    if age != nil {\n        us.Age = int(*age)\n    }\n\n    // Verifique se o parâmetro active foi fornecido (não nulo)\n    if active != nil {\n        us.Active = *active\n    }\n}\n```\n\n**Pontos-chave sobre parâmetros anuláveis:**\n\n- **Tipos primitivos anuláveis** (`?int`, `?float`, `?bool`) tornam-se ponteiros\n  (`*int64`, `*float64`, `*bool`) em Go;\n- **Strings anuláveis** (`?string`) permanecem como `*C.zend_string`, mas podem\n  ser `*nil`;\n- **Verifique `nil`** antes de dereferenciar valores de ponteiro;\n- **`null` do PHP torna-se `nil` do Go** - quando o PHP passa `null`, sua função\n  em Go recebe um ponteiro `nil`.\n\n> [!WARNING]\n> Atualmente, os métodos de classe têm as seguintes limitações.\n> **Objetos não são suportados** como tipos de parâmetros ou tipos de retorno.\n> **Arrays são totalmente suportados** tanto para parâmetros quanto para tipos\n> de retorno.\n> Tipos suportados: `string`, `int`, `float`, `bool`, `array` e `void` (para\n> tipo de retorno).\n> **Tipos de parâmetros anuláveis são totalmente suportados** para todos os\n> tipos escalares (`?string`, `?int`, `?float`, `?bool`).\n\nApós gerar a extensão, você poderá usar a classe e seus métodos no PHP.\nObserve que você **não pode acessar propriedades diretamente**:\n\n```php\n<?php\n\n$user = new User();\n\n// ✅ Isso funciona - usando métodos\n$user->setAge(25);\necho $user->getName();           // Saída: (vazio, valor padrão)\necho $user->getAge();            // Saída: 25\n$user->setNamePrefix(\"Funcionária\");\n\n// ✅ Isso também funciona - parâmetros anuláveis\n$user->updateInfo(\"João\", 30, true);        // Todos os parâmetros fornecidos\n$user->updateInfo(\"Joana\", null, false);     // Age é nulo\n$user->updateInfo(null, 25, null);          // Name e active são nulos\n\n// ❌ Isso NÃO funcionará - acesso direto à propriedade\n// echo $user->name;             // Error: Cannot access private property\n// $user->age = 30;              // Error: Cannot access private property\n```\n\nEste design garante que seu código Go tenha controle total sobre como o estado\ndo objeto é acessado e modificado, proporcionando melhor encapsulamento e\nsegurança de tipos.\n\n### Declarando constantes\n\nO gerador suporta a exportação de constantes Go para PHP usando duas diretivas:\n`//export_php:const` para constantes globais e `//export_php:classconst` para\nconstantes de classe.\nIsso permite que você compartilhe valores de configuração, códigos de status e\noutras constantes entre código Go e PHP.\n\n#### Constantes globais\n\nUse a diretiva `//export_php:const` para criar constantes PHP globais:\n\n```go\n//export_php:const\nconst MAX_CONNECTIONS = 100\n\n//export_php:const\nconst API_VERSION = \"1.2.3\"\n\n//export_php:const\nconst STATUS_OK = iota\n\n//export_php:const\nconst STATUS_ERROR = iota\n```\n\n#### Constantes de classe\n\nUse a diretiva `//export_php:classconst ClassName` para criar constantes que\npertencem a uma classe PHP específica:\n\n```go\n//export_php:classconst User\nconst STATUS_ACTIVE = 1\n\n//export_php:classconst User\nconst STATUS_INACTIVE = 0\n\n//export_php:classconst User\nconst ROLE_ADMIN = \"admin\"\n\n//export_php:classconst Order\nconst STATE_PENDING = iota\n\n//export_php:classconst Order\nconst STATE_PROCESSING = iota\n\n//export_php:classconst Order\nconst STATE_COMPLETED = iota\n```\n\nConstantes de classe são acessíveis usando o escopo do nome da classe no PHP:\n\n```php\n<?php\n\n// Constantes globais\necho MAX_CONNECTIONS;    // 100\necho API_VERSION;        // \"1.2.3\"\n\n// Constantes de classe\necho User::STATUS_ACTIVE;    // 1\necho User::ROLE_ADMIN;       // \"admin\"\necho Order::STATE_PENDING;   // 0\n```\n\nA diretiva suporta vários tipos de valor, incluindo strings, inteiros,\nbooleanos, floats e constantes `iota`.\nAo usar `iota`, o gerador atribui automaticamente valores sequenciais (0, 1, 2,\netc.).\nAs constantes globais ficam disponíveis no seu código PHP como constantes\nglobais, enquanto as constantes de classe são delimitadas para suas respectivas\nclasses usando a visibilidade pública.\nAo usar inteiros, diferentes notações possíveis (binária, hexadecimal, octal)\nsão suportadas e exibidas como estão no arquivo stub do PHP.\n\nVocê pode usar constantes da mesma forma que está acostumado no código Go.\nPor exemplo, vamos pegar a função `repeat_this()` que declaramos anteriormente e\nalterar o último argumento para um inteiro:\n\n```go\nimport (\n    \"C\"\n    \"github.com/dunglas/frankenphp\"\n    \"strings\"\n)\n\n//export_php:const\nconst STR_REVERSE = iota\n\n//export_php:const\nconst STR_NORMAL = iota\n\n//export_php:classconst StringProcessor\nconst MODE_LOWERCASE = 1\n\n//export_php:classconst StringProcessor\nconst MODE_UPPERCASE = 2\n\n//export_php:function repeat_this(string $str, int $count, int $mode): string\nfunc repeat_this(s *C.zend_string, count int64, mode int) unsafe.Pointer {\n    str := frankenphp.GoString(unsafe.Pointer(s))\n\n    result := strings.Repeat(str, int(count))\n    if mode == STR_REVERSE {\n        // inverte a string\n    }\n\n    if mode == STR_NORMAL {\n        // sem operação, apenas para mostrar a constante\n    }\n\n    return frankenphp.PHPString(result, false)\n}\n\n//export_php:class StringProcessor\ntype StringProcessorStruct struct {\n    // campos internos\n}\n\n//export_php:method StringProcessor::process(string $input, int $mode): string\nfunc (sp *StringProcessorStruct) Process(input *C.zend_string, mode int64) unsafe.Pointer {\n    str := frankenphp.GoString(unsafe.Pointer(input))\n\n    switch mode {\n    case MODE_LOWERCASE:\n        str = strings.ToLower(str)\n    case MODE_UPPERCASE:\n        str = strings.ToUpper(str)\n    }\n\n    return frankenphp.PHPString(str, false)\n}\n```\n\n### Usando namespaces\n\nO gerador suporta a organização das funções, classes e constantes da sua\nextensão PHP em um namespace usando a diretiva `//export_php:namespace`.\nIsso ajuda a evitar conflitos de nomenclatura e proporciona uma melhor\norganização para a API da sua extensão.\n\n#### Declarando um namespace\n\nUse a diretiva `//export_php:namespace` no topo do seu arquivo Go para colocar\ntodos os símbolos exportados em um namespace específico:\n\n```go\n//export_php:namespace My\\Extension\npackage main\n\nimport \"C\"\n\n//export_php:function hello(): string\nfunc hello() string {\n    return \"Olá do namespace My\\\\Extension!\"\n}\n\n//export_php:class User\ntype UserStruct struct {\n    // campos internos\n}\n\n//export_php:method User::getName(): string\nfunc (u *UserStruct) GetName() unsafe.Pointer {\n    return frankenphp.PHPString(\"João Ninguém\", false)\n}\n\n//export_php:const\nconst STATUS_ACTIVE = 1\n```\n\n#### Usando extensões com namespace no PHP\n\nQuando um namespace é declarado, todas as funções, classes e constantes são\ncolocadas sob esse namespace no PHP:\n\n```php\n<?php\n\necho My\\Extension\\hello(); // \"Olá do namespace My\\Extension!\"\n\n$user = new My\\Extension\\User();\necho $user->getName(); // \"João Ninguém\"\n\necho My\\Extension\\STATUS_ACTIVE; // 1\n```\n\n#### Notas importantes\n\n- Apenas **uma** diretiva de namespace é permitida por arquivo.\n  Se várias diretivas de namespace forem encontradas, o gerador retornará um\n  erro;\n- O namespace se aplica a **todos** os símbolos exportados no arquivo: funções,\n  classes, métodos e constantes;\n- Os nomes de namespace seguem as convenções de namespace do PHP, usando barras\n  invertidas (`\\`) como separadores;\n- Se nenhum namespace for declarado, os símbolos serão exportados para o\n  namespace global como de costume.\n\n### Gerando a extensão\n\nÉ aqui que a mágica acontece e sua extensão agora pode ser gerada.\nVocê pode executar o gerador com o seguinte comando:\n\n```console\nGEN_STUB_SCRIPT=php-src/build/gen_stub.php frankenphp extension-init my_extension.go\n```\n\n> [!NOTE]\n> Não se esqueça de definir a variável de ambiente `GEN_STUB_SCRIPT` para o\n> caminho do arquivo `gen_stub.php` no código-fonte PHP que você baixou\n> anteriormente.\n> Este é o mesmo script `gen_stub.php` mencionado na seção de implementação\n> manual.\n\nSe tudo correu bem, um novo diretório chamado `build` deve ter sido criado.\nEste diretório contém os arquivos gerados para sua extensão, incluindo o arquivo\n`my_extension.go` com os stubs de funções PHP gerados.\n\n### Integrando a extensão gerada ao FrankenPHP\n\nNossa extensão agora está pronta para ser compilada e integrada ao FrankenPHP.\nPara fazer isso, consulte a [documentação de compilação](compile.md) do\nFrankenPHP para aprender como compilar o FrankenPHP.\nAdicione o módulo usando a flag `--with`, apontando para o caminho do seu\nmódulo:\n\n```console\nCGO_ENABLED=1 \\\nXCADDY_GO_BUILD_FLAGS=\"-ldflags='-w -s' -tags=nobadger,nomysql,nopgx\" \\\nCGO_CFLAGS=$(php-config --includes) \\\nCGO_LDFLAGS=\"$(php-config --ldflags) $(php-config --libs)\" \\\nxcaddy build \\\n    --output frankenphp \\\n    --with github.com/<minha-conta>/<meu-modulo>/build\n```\n\nObserve que você aponta para o subdiretório `/build` que foi criado durante a\netapa de geração.\nEntretanto, isso não é obrigatório: você também pode copiar os arquivos gerados\npara o diretório do seu módulo e apontar diretamente para ele.\n\n### Testando sua extensão gerada\n\nVocê pode criar um arquivo PHP para testar as funções e classes que criou.\nPor exemplo, crie um arquivo `index.php` com o seguinte conteúdo:\n\n```php\n<?php\n\n// Usando constantes globais\nvar_dump(repeat_this('Olá mundo', 5, STR_REVERSE));\n\n// Usando constantes de classe\n$processor = new StringProcessor();\necho $processor->process('Olá mundo', StringProcessor::MODE_LOWERCASE);  // \"olá mundo\"\necho $processor->process('Olá mundo', StringProcessor::MODE_UPPERCASE);  // \"OLÁ MUNDO\"\n```\n\nDepois de integrar sua extensão ao FrankenPHP, como demonstrado na seção\nanterior, você pode executar este arquivo de teste usando\n`./frankenphp php-server` e deverá ver sua extensão funcionando.\n\n## Implementação manual\n\nSe você quiser entender como as extensões funcionam ou precisar de controle\ntotal sobre elas, pode escrevê-las manualmente.\nEssa abordagem oferece controle total, mas requer mais código boilerplate.\n\n### Função básica\n\nVeremos como escrever uma extensão PHP simples em Go que define uma nova função\nnativa.\nEssa função será chamada do PHP e disparará uma goroutine que registra uma\nmensagem nos logs do Caddy.\nEssa função não recebe parâmetros e não retorna nada.\n\n#### Definindo a função Go\n\nNo seu módulo, você precisa definir uma nova função nativa que será chamada do\nPHP.\nPara fazer isso, crie um arquivo com o nome desejado, por exemplo,\n`extension.go`, e adicione o seguinte código:\n\n```go\npackage ext_go\n\n//#include \"extension.h\"\nimport \"C\"\nimport (\n    \"unsafe\"\n    \"github.com/caddyserver/caddy/v2\"\n    \"github.com/dunglas/frankenphp\"\n)\n\nfunc init() {\n    frankenphp.RegisterExtension(unsafe.Pointer(&C.ext_module_entry))\n}\n\n//export go_print_something\nfunc go_print_something() {\n    go func() {\n        caddy.Log().Info(\"Olá de uma goroutine!\")\n    }()\n}\n```\n\nA função `frankenphp.RegisterExtension()` simplifica o processo de registro de\nextensões, manipulando a lógica interna de registro do PHP.\nA função `go_print_something` usa a diretiva `//export` para indicar que estará\nacessível no código C que escreveremos, graças ao CGO.\n\nNeste exemplo, nossa nova função disparará uma goroutine que registra uma\nmensagem nos logs do Caddy.\n\n#### Definindo a função PHP\n\nPara permitir que o PHP chame nossa função, precisamos definir uma função PHP\ncorrespondente.\nPara isso, criaremos um arquivo stub, por exemplo, `extension.stub.php`, que\nconterá o seguinte código:\n\n```php\n<?php\n\n/** @generate-class-entries */\n\nfunction go_print(): void {}\n```\n\nEste arquivo define a assinatura da função `go_print()`, que será chamada do\nPHP.\nA diretiva `@generate-class-entries` permite que o PHP gere automaticamente\nentradas de função para nossa extensão.\n\nIsso não é feito manualmente, mas usando um script fornecido no código-fonte do\nPHP (certifique-se de ajustar o caminho para o script `gen_stub.php` com base em\nonde o código-fonte do PHP está localizado):\n\n```bash\nphp ../php-src/build/gen_stub.php extension.stub.php\n```\n\nEste script gerará um arquivo chamado `extension_arginfo.h` que contém as\ninformações necessárias para que o PHP saiba como definir e chamar nossa função.\n\n#### Escrevendo a ponte entre Go e C\n\nAgora, precisamos escrever a ponte entre Go e C.\nCrie um arquivo chamado `extension.h` no diretório do seu módulo com o seguinte\nconteúdo:\n\n```c\n#ifndef _EXTENSION_H\n#define _EXTENSION_H\n\n#include <php.h>\n\nextern zend_module_entry ext_module_entry;\n\n#endif\n```\n\nEm seguida, crie um arquivo chamado `extension.c` que executará as seguintes\netapas:\n\n- Incluir cabeçalhos PHP;\n- Declarar nossa nova função nativa PHP `go_print()`;\n- Declarar os metadados da extensão.\n\nVamos começar incluindo os cabeçalhos necessários:\n\n```c\n#include <php.h>\n#include \"extension.h\"\n#include \"extension_arginfo.h\"\n\n// Contém símbolos exportados pelo Go\n#include \"_cgo_export.h\"\n```\n\nEm seguida, definimos nossa função PHP como uma função nativa da linguagem:\n\n```c\nPHP_FUNCTION(go_print)\n{\n    ZEND_PARSE_PARAMETERS_NONE();\n\n    go_print_something();\n}\n\nzend_module_entry ext_module_entry = {\n    STANDARD_MODULE_HEADER,\n    \"ext_go\",\n    ext_functions, /* Funções */\n    NULL,          /* MINIT */\n    NULL,          /* MSHUTDOWN */\n    NULL,          /* RINIT */\n    NULL,          /* RSHUTDOWN */\n    NULL,          /* MINFO */\n    \"0.1.1\",\n    STANDARD_MODULE_PROPERTIES\n};\n```\n\nNeste caso, nossa função não recebe parâmetros e não retorna nada.\nEla simplesmente chama a função Go que definimos anteriormente, exportada usando\na diretiva `//export`.\n\nFinalmente, definimos os metadados da extensão em uma estrutura\n`zend_module_entry`, como seu nome, versão e propriedades.\nEssas informações são necessárias para que o PHP reconheça e carregue nossa\nextensão.\nObserve que `ext_functions` é um array de ponteiros para as funções PHP que\ndefinimos e foi gerado automaticamente pelo script `gen_stub.php` no arquivo\n`extension_arginfo.h`.\n\nO registro da extensão é tratado automaticamente pela função\n`RegisterExtension()` do FrankenPHP, que chamamos em nosso código Go.\n\n### Uso avançado\n\nAgora que sabemos como criar uma extensão PHP básica em Go, vamos tornar nosso\nexemplo mais complexo.\nAgora, criaremos uma função PHP que recebe uma string como parâmetro e retorna\nsua versão em letras maiúsculas.\n\n#### Definindo o stub da função PHP\n\nPara definir a nova função PHP, modificaremos nosso arquivo `extension.stub.php`\npara incluir a nova assinatura da função:\n\n```php\n<?php\n\n/** @generate-class-entries */\n\n/**\n * Converte uma string para letras maiúsculas.\n *\n * @param string $string A string a ser convertida.\n * @return string A versão em letras maiúsculas da string.\n */\nfunction go_upper(string $string): string {}\n```\n\n> [!TIP]\n> Não negligencie a documentação das suas funções!\n> Você provavelmente compartilhará os stubs da sua extensão com outras\n> pessoas desenvolvedoras para documentar como usar a sua extensão e quais\n> recursos estão disponíveis.\n\nAo gerar novamente o arquivo stub com o script `gen_stub.php`, o arquivo\n`extension_arginfo.h` deverá ficar assim:\n\n```c\nZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_go_upper, 0, 1, IS_STRING, 0)\n    ZEND_ARG_TYPE_INFO(0, string, IS_STRING, 0)\nZEND_END_ARG_INFO()\n\nZEND_FUNCTION(go_upper);\n\nstatic const zend_function_entry ext_functions[] = {\n    ZEND_FE(go_upper, arginfo_go_upper)\n    ZEND_FE_END\n};\n```\n\nPodemos ver que a função `go_upper` é definida com um parâmetro do tipo `string`\ne um tipo de retorno `string`.\n\n#### Malabarismo de tipos entre Go e PHP/C\n\nSua função Go não pode aceitar diretamente uma string PHP como parâmetro.\nVocê precisa convertê-la para uma string Go.\nFelizmente, o FrankenPHP fornece funções auxiliares para lidar com a conversão\nentre strings PHP e strings Go, semelhante ao que vimos na abordagem do gerador.\n\nO arquivo de cabeçalho permanece simples:\n\n```c\n#ifndef _EXTENSION_H\n#define _EXTENSION_H\n\n#include <php.h>\n\nextern zend_module_entry ext_module_entry;\n\n#endif\n```\n\nAgora podemos escrever a ponte entre Go e C no nosso arquivo `extension.c`.\nPassaremos a string PHP diretamente para a nossa função Go:\n\n```c\nPHP_FUNCTION(go_upper)\n{\n    zend_string *str;\n\n    ZEND_PARSE_PARAMETERS_START(1, 1)\n        Z_PARAM_STR(str)\n    ZEND_PARSE_PARAMETERS_END();\n\n    zend_string *result = go_upper(str);\n    RETVAL_STR(result);\n}\n```\n\nVocê pode aprender mais sobre `ZEND_PARSE_PARAMETERS_START` e análise de\nparâmetros na página dedicada do\n[PHP Internals Book](https://www.phpinternalsbook.com/php7/extensions_design/php_functions.html#parsing-parameters-zend-parse-parameters).\nAqui, informamos ao PHP que nossa função recebe um parâmetro obrigatório do tipo\n`string` como uma `zend_string`.\nEm seguida, passamos essa string diretamente para nossa função Go e retornamos o\nresultado usando `RETVAL_STR`.\n\nSó resta uma coisa a fazer: implementar a função `go_upper` em Go.\n\n#### Implementando a função Go\n\nNossa função Go receberá uma `*C.zend_string` como parâmetro, a converterá em\numa string Go usando a função auxiliar do FrankenPHP, a processará e retornará o\nresultado como uma nova `*C.zend_string`.\nAs funções auxiliares cuidam de todo o gerenciamento de memória e da\ncomplexidade da conversão para nós.\n\n```go\nimport \"strings\"\n\n//export go_upper\nfunc go_upper(s *C.zend_string) *C.zend_string {\n    str := frankenphp.GoString(unsafe.Pointer(s))\n\n    upper := strings.ToUpper(str)\n\n    return (*C.zend_string)(frankenphp.PHPString(upper, false))\n}\n```\n\nEssa abordagem é muito mais limpa e segura do que o gerenciamento manual de\nmemória.\nAs funções auxiliares do FrankenPHP lidam automaticamente com a conversão entre\no formato `zend_string` do PHP e strings em Go.\nO parâmetro `false` em `PHPString()` indica que queremos criar uma nova string\nnão persistente (liberada ao final da requisição).\n\n> [!TIP]\n> Neste exemplo, não realizamos nenhum tratamento de erro, mas você deve sempre\n> verificar se os ponteiros não são `nil` e se os dados são válidos antes de\n> usá-los em suas funções em Go.\n\n### Integrando a extensão ao FrankenPHP\n\nNossa extensão agora está pronta para ser compilada e integrada ao FrankenPHP.\nPara isso, consulte a [documentação de compilação](compile.md) do FrankenPHP\npara aprender como compilar o FrankenPHP.\nAdicione o módulo usando a flag `--with`, apontando para o caminho do seu\nmódulo:\n\n```console\nCGO_ENABLED=1 \\\nXCADDY_GO_BUILD_FLAGS=\"-ldflags='-w -s' -tags=nobadger,nomysql,nopgx\" \\\nCGO_CFLAGS=$(php-config --includes) \\\nCGO_LDFLAGS=\"$(php-config --ldflags) $(php-config --libs)\" \\\nxcaddy build \\\n    --output frankenphp \\\n    --with github.com/<minha-conta>/<meu-modulo>\n```\n\nPronto!\nSua extensão agora está integrada ao FrankenPHP e pode ser usada no seu código\nPHP.\n\n### Testando sua extensão\n\nApós integrar sua extensão ao FrankenPHP, você pode criar um arquivo `index.php`\ncom exemplos para as funções que você implementou:\n\n```php\n<?php\n\n// Testa a função básica\ngo_print();\n\n// Testa a função avançada\necho go_upper(\"olá mundo\") . \"\\n\";\n```\n\nAgora você pode executar o FrankenPHP com este arquivo usando\n`./frankenphp php-server` e deverá ver sua extensão funcionando.\n"
  },
  {
    "path": "docs/pt-br/github-actions.md",
    "content": "# Usando GitHub Actions\n\nEste repositório constrói e implanta a imagem Docker no\n[Docker Hub](https://hub.docker.com/r/dunglas/frankenphp) a cada pull request\naprovado ou em seu próprio fork após a configuração.\n\n## Configurando GitHub Actions\n\nNas configurações do repositório, em \"Secrets\", adicione os seguintes segredos:\n\n- `REGISTRY_LOGIN_SERVER`: O registro do Docker a ser usado (por exemplo,\n  `docker.io`).\n- `REGISTRY_USERNAME`: O nome de usuário a ser usado para fazer login no\n  registro (por exemplo, `dunglas`).\n- `REGISTRY_PASSWORD`: A senha a ser usada para fazer login no registro (por\n  exemplo, uma chave de acesso).\n- `IMAGE_NAME`: O nome da imagem (por exemplo, `dunglas/frankenphp`).\n\n## Construindo e enviando a imagem\n\n1. Crie um pull request ou faça o push para o seu fork.\n2. O GitHub Actions construirá a imagem e executará os testes.\n3. Se a construção for bem-sucedida, a imagem será enviada para o registro\n   usando a tag `pr-x`, onde `x` é o número do PR.\n\n## Implantando a imagem\n\n1. Após o merge do pull request, o GitHub Actions executará os testes novamente\n   e criará uma nova imagem.\n2. Se a construção for bem-sucedida, a tag `main` será atualizada no registro do\n   Docker.\n\n## Versões\n\n1. Crie uma nova tag no repositório.\n2. O GitHub Actions construirá a imagem e executará os testes.\n3. Se a construção for bem-sucedida, a imagem será enviada para o registro\n   usando o nome da tag como tag (por exemplo, `v1.2.3` e `v1.2` serão criadas).\n4. A tag `latest` também será atualizada.\n"
  },
  {
    "path": "docs/pt-br/hot-reload.md",
    "content": "# Recarregamento Instantâneo\n\nFrankenPHP inclui um recurso de **recarregamento instantâneo** embutido, projetado para melhorar drasticamente a experiência do desenvolvedor.\n\n![Hot Reload](hot-reload.png)\n\nEste recurso oferece um fluxo de trabalho semelhante ao **Hot Module Replacement (HMR)** em ferramentas modernas de JavaScript, como Vite ou webpack.\nEm vez de atualizar o navegador manualmente após cada alteração de arquivo (código PHP, templates, arquivos JavaScript e CSS...), o FrankenPHP atualiza o conteúdo da página em tempo real.\n\nO Recarregamento Instantâneo funciona nativamente com WordPress, Laravel, Symfony e qualquer outra aplicação ou framework PHP.\n\nQuando ativado, o FrankenPHP monitora seu diretório de trabalho atual em busca de alterações no sistema de arquivos. Quando um arquivo é modificado, ele envia uma atualização [Mercure](mercure.md) para o navegador.\n\nDependendo da sua configuração, o navegador irá:\n\n- **Transformar o DOM** (preservando a posição de rolagem e o estado dos inputs) se o [Idiomorph](https://github.com/bigskysoftware/idiomorph) estiver carregado.\n- **Recarregar a página** (recarregamento ao vivo padrão) se o Idiomorph não estiver presente.\n\n## Configuração\n\nPara ativar o recarregamento instantâneo, habilite o Mercure e, em seguida, adicione a subdiretiva `hot_reload` à diretiva `php_server` no seu `Caddyfile`.\n\n> [!WARNING]\n>\n> Este recurso é destinado **apenas para ambientes de desenvolvimento**.\n> Não ative `hot_reload` em produção, pois este recurso não é seguro (expõe detalhes internos sensíveis) e desacelera a aplicação.\n>\n```caddyfile\nlocalhost\n\nmercure {\n    anonymous\n}\n\nroot public/\nphp_server {\n    hot_reload\n}\n```\n\nPor padrão, o FrankenPHP monitorará todos os arquivos no diretório de trabalho atual que correspondem a este padrão glob: `./**/*.{css,env,gif,htm,html,jpg,jpeg,js,mjs,php,png,svg,twig,webp,xml,yaml,yml}`\n\nÉ possível definir os arquivos a serem monitorados usando a sintaxe glob explicitamente:\n\n```caddyfile\nlocalhost\n\nmercure {\n    anonymous\n}\n\nroot public/\nphp_server {\n    hot_reload src/**/*{.php,.js} config/**/*.yaml\n}\n```\n\nUse a forma longa de `hot_reload` para especificar o tópico Mercure a ser usado, bem como quais diretórios ou arquivos monitorar:\n\n```caddyfile\nlocalhost\n\nmercure {\n    anonymous\n}\n\nroot public/\nphp_server {\n    hot_reload {\n        topic hot-reload-topic\n        watch src/**/*.php\n        watch assets/**/*.{ts,json}\n        watch templates/\n        watch public/css/\n    }\n}\n```\n\n## Integração no Lado do Cliente\n\nEnquanto o servidor detecta as alterações, o navegador precisa se inscrever nesses eventos para atualizar a página.\nO FrankenPHP expõe a URL do Hub Mercure a ser usada para se inscrever em alterações de arquivo através da variável de ambiente `$_SERVER['FRANKENPHP_HOT_RELOAD']`.\n\nUma biblioteca JavaScript de conveniência, [frankenphp-hot-reload](https://www.npmjs.com/package/frankenphp-hot-reload), também está disponível para lidar com a lógica do lado do cliente.\nPara usá-la, adicione o seguinte ao seu layout principal:\n\n```php\n<!DOCTYPE html>\n<title>FrankenPHP Hot Reload</title>\n<?php if (isset($_SERVER['FRANKENPHP_HOT_RELOAD'])): ?>\n<meta name=\"frankenphp-hot-reload:url\" content=\"<?=$_SERVER['FRANKENPHP_HOT_RELOAD']?>\">\n<script src=\"https://cdn.jsdelivr.net/npm/idiomorph\"></script>\n<script src=\"https://cdn.jsdelivr.net/npm/frankenphp-hot-reload/+esm\" type=\"module\"></script>\n<?php endif ?>\n```\n\nA biblioteca se inscreverá automaticamente no hub Mercure, buscará a URL atual em segundo plano quando uma alteração de arquivo for detectada e transformará o DOM.\nEstá disponível como um pacote [npm](https://www.npmjs.com/package/frankenphp-hot-reload) e no [GitHub](https://github.com/dunglas/frankenphp-hot-reload).\n\nAlternativamente, você pode implementar sua própria lógica do lado do cliente inscrevendo-se diretamente no hub Mercure usando a classe nativa `EventSource` do JavaScript.\n\n### Preservando Nós DOM Existentes\n\nEm casos raros, como ao usar ferramentas de desenvolvimento [como a barra de depuração da web do Symfony](https://github.com/symfony/symfony/pull/62970), você pode querer preservar nós DOM específicos.\nPara fazer isso, adicione o atributo `data-frankenphp-hot-reload-preserve` ao elemento HTML relevante:\n\n```html\n<div data-frankenphp-hot-reload-preserve><!-- My debug bar --></div>\n```\n\n## Modo Worker\n\nSe você estiver executando sua aplicação em [Modo Worker](https://frankenphp.dev/docs/worker/), seu script de aplicação permanece na memória.\nIsso significa que as alterações no seu código PHP não serão refletidas imediatamente, mesmo que o navegador recarregue.\n\nPara a melhor experiência do desenvolvedor, você deve combinar `hot_reload` com [a subdiretiva `watch` na diretiva `worker`](config.md#observando-alteracoes-de-arquivos).\n\n- `hot_reload`: atualiza o **navegador** quando os arquivos são alterados\n- `worker.watch`: reinicia o worker quando os arquivos são alterados\n\n```caddy\nlocalhost\n\nmercure {\n    anonymous\n}\n\nroot public/\nphp_server {\n    hot_reload\n    worker {\n        file /path/to/my_worker.php\n        watch\n    }\n}\n```\n\n## Como Funciona\n\n1. **Monitoramento**: O FrankenPHP monitora o sistema de arquivos em busca de modificações usando a biblioteca [`e-dant/watcher`](https://github.com/e-dant/watcher) por baixo dos panos (contribuímos com o binding Go).\n2. **Reiniciar (Modo Worker)**: se `watch` estiver habilitado na configuração do worker, o worker PHP é reiniciado para carregar o novo código.\n3. **Envio**: Um payload JSON contendo a lista de arquivos alterados é enviado para o [hub Mercure](https://mercure.rocks) embutido.\n4. **Recebimento**: O navegador, ouvindo através da biblioteca JavaScript, recebe o evento Mercure.\n5. **Atualização**:\n\n- Se o **Idiomorph** for detectado, ele busca o conteúdo atualizado e transforma o HTML atual para corresponder ao novo estado, aplicando as alterações instantaneamente sem perder o estado.\n- Caso contrário, `window.location.reload()` é chamado para recarregar a página.\n"
  },
  {
    "path": "docs/pt-br/known-issues.md",
    "content": "# Problemas conhecidos\n\n## Extensões PHP não suportadas\n\nAs seguintes extensões são conhecidas por não serem compatíveis com o\nFrankenPHP:\n\n| Nome                                                                                                        | Motivo            | Alternativas                                                                                                         |\n| ----------------------------------------------------------------------------------------------------------- | ----------------- | -------------------------------------------------------------------------------------------------------------------- |\n| [imap](https://www.php.net/manual/pt_BR/imap.installation.php)                                              | Não é thread-safe | [javanile/php-imap2](https://github.com/javanile/php-imap2), [webklex/php-imap](https://github.com/Webklex/php-imap) |\n| [newrelic](https://docs.newrelic.com/docs/apm/agents/php-agent/getting-started/introduction-new-relic-php/) | Não é thread-safe | -                                                                                                                    |\n\n## Extensões PHP com falhas\n\nAs seguintes extensões apresentam falhas conhecidas e comportamentos inesperados\nquando usadas com o FrankenPHP:\n\n| Nome                                                             | Problema                                                                                                                                                                                                                                                                                                                    |\n| ---------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [ext-openssl](https://www.php.net/manual/pt_BR/book.openssl.php) | Ao usar uma versão estática do FrankenPHP (compilada com a `libc` `musl`), a extensão OpenSSL pode quebrar sob cargas pesadas. Uma solução alternativa é usar uma versão vinculada dinamicamente (como a usada em imagens Docker). Esta falha está [sendo monitorada pelo PHP](https://github.com/php/php-src/issues/13648) |\n\n## `get_browser`\n\nA função\n[`get_browser()`](https://www.php.net/manual/pt_BR/function.get-browser.php)\nparece apresentar mau desempenho após algum tempo.\nUma solução alternativa é armazenar em cache (por exemplo, com\n[APCu](https://www.php.net/manual/pt_BR/book.apcu.php)) os resultados por Agente\nde Usuário, pois são estáticos.\n\n## Imagens Docker binárias independentes e baseadas em Alpine\n\nAs imagens Docker binárias independentes e baseadas em Alpine\n(`dunglas/frankenphp:*-alpine`) usam a [`libc` `musl`](https://musl.libc.org/)\nem vez de [`glibc` e similares](https://www.etalabs.net/compare_libcs.html) para\nmanter um tamanho binário menor.\nIsso pode levar a alguns problemas de compatibilidade.\nEm particular, o sinalizador glob `GLOB_BRACE`\n[não está disponível](https://www.php.net/manual/pt_BR/function.glob.php)\n\n## Usando `https://127.0.0.1` com o Docker\n\nPor padrão, o FrankenPHP gera um certificado TLS para `localhost`.\nÉ a opção mais fácil e recomendada para desenvolvimento local.\n\nSe você realmente deseja usar `127.0.0.1` como host, é possível configurá-lo\npara gerar um certificado definindo o nome do servidor como `127.0.0.1`.\n\nInfelizmente, isso não é suficiente ao usar o Docker devido ao\n[seu sistema de rede](https://docs.docker.com/network/).\nVocê receberá um erro TLS semelhante a\n`curl: (35) LibreSSL/3.3.6: erro:1404B438:SSL routines:ST_CONNECT:tlsv1 alert internal error`.\n\nSe você estiver usando Linux, uma solução é usar\n[o driver de rede do host](https://docs.docker.com/network/network-tutorial-host/):\n\n```console\ndocker run \\\n    -e SERVER_NAME=\"127.0.0.1\" \\\n    -v $PWD:/app/public \\\n    --network host \\\n    dunglas/frankenphp\n```\n\nO driver de rede do host não é compatível com Mac e Windows.\nNessas plataformas, você terá que descobrir o endereço IP do contêiner e\nincluí-lo nos nomes dos servidores.\n\nExecute o comando `docker network inspect bridge` e verifique a chave\n`Containers` para identificar o último endereço IP atribuído atualmente sob a\nchave `IPv4Address` e incremente-o em um.\nSe nenhum contêiner estiver em execução, o primeiro endereço IP atribuído\ngeralmente é `172.17.0.2`.\n\nEm seguida, inclua isso na variável de ambiente `SERVER_NAME`:\n\n```console\ndocker run \\\n    -e SERVER_NAME=\"127.0.0.1, 172.17.0.3\" \\\n    -v $PWD:/app/public \\\n    -p 80:80 -p 443:443 -p 443:443/udp \\\n    dunglas/frankenphp\n```\n\n> [!CAUTION]\n>\n> Certifique-se de substituir `172.17.0.3` pelo IP que será atribuído ao seu\n> contêiner.\n\nAgora você deve conseguir acessar `https://127.0.0.1` a partir da máquina host.\n\nSe este não for o caso, inicie o FrankenPHP em modo de depuração para tentar\ndescobrir o problema:\n\n```console\ndocker run \\\n    -e CADDY_GLOBAL_OPTIONS=\"debug\" \\\n    -e SERVER_NAME=\"127.0.0.1\" \\\n    -v $PWD:/app/public \\\n    -p 80:80 -p 443:443 -p 443:443/udp \\\n    dunglas/frankenphp\n```\n\n## Scripts do Composer que referenciam `@php`\n\n[Scripts do Composer](https://getcomposer.org/doc/articles/scripts.md) podem\nquerer executar um binário PHP para algumas tarefas, por exemplo, em\n[um projeto Laravel](laravel.md) para executar\n`@php artisan package:discover --ansi`.\nIsso\n[atualmente falha](https://github.com/php/frankenphp/issues/483#issuecomment-1899890915)\npor dois motivos:\n\n- O Composer não sabe como chamar o binário do FrankenPHP;\n- O Composer pode adicionar configurações do PHP usando a flag `-d` no comando,\n  que o FrankenPHP ainda não suporta.\n\nComo solução alternativa, podemos criar um script de shell em\n`/usr/local/bin/php` que remove os parâmetros não suportados e, em seguida,\nchama o FrankenPHP:\n\n```bash\n#!/usr/bin/env bash\nargs=(\"$@\")\nindex=0\nfor i in \"$@\"\ndo\n    if [ \"$i\" == \"-d\" ]; then\n        unset 'args[$index]'\n        unset 'args[$index+1]'\n    fi\n    index=$((index+1))\ndone\n\n/usr/local/bin/frankenphp php-cli ${args[@]}\n```\n\nEm seguida, defina a variável de ambiente `PHP_BINARY` para o caminho do nosso\nscript `php` e execute o Composer:\n\n```console\nexport PHP_BINARY=/usr/local/bin/php\ncomposer install\n```\n\n## Solução de problemas de TLS/SSL com binários estáticos\n\nAo usar binários estáticos, você pode encontrar os seguintes erros relacionados\na TLS, por exemplo, ao enviar emails usando STARTTLS:\n\n```text\nUnable to connect with STARTTLS: stream_socket_enable_crypto(): SSL operation failed with code 5. OpenSSL Error messages:\nerror:80000002:system library::No such file or directory\nerror:80000002:system library::No such file or directory\nerror:80000002:system library::No such file or directory\nerror:0A000086:SSL routines::certificate verify failed\n```\n\nComo o binário estático não empacota certificados TLS, você precisa apontar o\nOpenSSL para a instalação local de certificados de CA.\n\nInspecione a saída de\n[`openssl_get_cert_locations()`](https://www.php.net/manual/pt_BR/function.openssl-get-cert-locations.php),\npara descobrir onde os certificados de CA devem ser instalados e armazene-os\nneste local.\n\n> [!WARNING]\n>\n> Contextos web e CLI podem ter configurações diferentes.\n> Certifique-se de executar `openssl_get_cert_locations()` no contexto\n> apropriado.\n\n[Certificados CA extraídos do Mozilla podem ser baixados no site do cURL](https://curl.se/docs/caextract.html).\n\nComo alternativa, muitas distribuições, incluindo Debian, Ubuntu e Alpine,\nfornecem pacotes chamados `ca-certificates` que contêm esses certificados.\n\nTambém é possível usar `SSL_CERT_FILE` e `SSL_CERT_DIR` para indicar à OpenSSL\nonde procurar certificados CA:\n\n```console\n# Define variáveis de ambiente para certificados TLS\nexport SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt\nexport SSL_CERT_DIR=/etc/ssl/certs\n```\n"
  },
  {
    "path": "docs/pt-br/laravel.md",
    "content": "# Laravel\n\n## Docker\n\nServir uma aplicação web [Laravel](https://laravel.com) com FrankenPHP é tão\nfácil quanto montar o projeto no diretório `/app` da imagem Docker oficial.\n\nExecute este comando a partir do diretório principal da sua aplicação Laravel:\n\n```console\ndocker run -p 80:80 -p 443:443 -p 443:443/udp -v $PWD:/app dunglas/frankenphp\n```\n\nE divirta-se!\n\n## Instalação local\n\nAlternativamente, você pode executar seus projetos Laravel com FrankenPHP a\npartir da sua máquina local:\n\n1. [Baixe o binário correspondente ao seu sistema](../#standalone-binary).\n2. Adicione a seguinte configuração a um arquivo chamado `Caddyfile` no\n   diretório raiz do seu projeto Laravel:\n\n   ```caddyfile\n   {\n       frankenphp\n   }\n\n   # O nome de domínio do seu servidor\n   localhost {\n       # Define o diretório raiz como public/\n       root public/\n       # Habilita a compressão (opcional)\n       encode zstd br gzip\n       # Executa os arquivos PHP a partir do diretório public/ e serve os assets\n       php_server {\n           try_files {path} index.php\n       }\n   }\n   ```\n\n3. Inicie o FrankenPHP a partir do diretório raiz do seu projeto Laravel:\n   `frankenphp run`.\n\n## Laravel Octane\n\nO Octane pode ser instalado através do gerenciador de pacotes Composer:\n\n```console\ncomposer require laravel/octane\n```\n\nApós instalar o Octane, você pode executar o comando `octane:install` do\nArtisan, que instalará o arquivo de configuração do Octane em sua aplicação:\n\n```console\nphp artisan octane:install --server=frankenphp\n```\n\nO servidor Octane pode ser iniciado por meio do comando `octane:frankenphp` do\nArtisan.\n\n```console\nphp artisan octane:frankenphp\n```\n\nO comando `octane:frankenphp` pode receber as seguintes opções:\n\n- `--host`: O endereço IP ao qual o servidor deve se vincular (padrão:\n  `127.0.0.1`);\n- `--port`: A porta na qual o servidor deve estar disponível (padrão: `8000`);\n- `--admin-port`: A porta na qual o servidor de administração deve estar\n  disponível (padrão: `2019`);\n- `--workers`: O número de workers que devem estar disponíveis para processar\n  requisições (padrão: `auto`);\n- `--max-requests`: O número de requisições a serem processadas antes de\n  recarregar o servidor (padrão: `500`);\n- `--caddyfile`: O caminho para o arquivo `Caddyfile` do FrankenPHP (padrão:\n  [stub do `Caddyfile` no Laravel Octane](https://github.com/laravel/octane/blob/2.x/src/Commands/stubs/Caddyfile));\n- `--https`: Habilita HTTPS, HTTP/2 e HTTP/3 e gera e renova certificados\n  automaticamente;\n- `--http-redirect`: Habilita o redirecionamento de HTTP para HTTPS (somente\n- habilitado se `--https` for passada);\n- `--watch`: Recarrega o servidor automaticamente quando a aplicação é\n  modificada;\n- `--poll`: Usa o polling do sistema de arquivos durante a verificação para\n  monitorar arquivos em uma rede;\n- `--log-level`: Registra mensagens de log no nível de log especificado ou acima\n  dele, usando o logger nativo do Caddy.\n\n> [!TIP]\n> Para obter logs JSON estruturados (útil ao usar soluções de análise de logs),\n> passe explicitamente a opção `--log-level`.\n\nSaiba mais sobre o\n[Laravel Octane em sua documentação oficial](https://laravel.com/docs/octane).\n\n## Aplicações Laravel como binários independentes\n\nUsando o [recurso de incorporação de aplicações do FrankenPHP](embed.md), é\npossível distribuir aplicações Laravel como binários independentes.\n\nSiga estes passos para empacotar sua aplicação Laravel como um binário\nindependente para Linux:\n\n1. Crie um arquivo chamado `static-build.Dockerfile` no repositório da sua\n   aplicação:\n\n   ```dockerfile\n   FROM --platform=linux/amd64 dunglas/frankenphp:static-builder-gnu\n   # Se você pretende executar o binário em sistemas musl-libc, use o static-builder-musl\n\n   # Copia sua aplicação\n   WORKDIR /go/src/app/dist/app\n   COPY . .\n\n   # Remove os testes e outros arquivos desnecessários para economizar espaço\n   # Como alternativa, adicione esses arquivos a um arquivo .dockerignore\n   RUN rm -Rf tests/\n\n   # Copia o arquivo .env\n   RUN cp .env.example .env\n   # Altera APP_ENV e APP_DEBUG para que estejam prontas para produção\n   RUN sed -i'' -e 's/^APP_ENV=.*/APP_ENV=production/' -e 's/^APP_DEBUG=.*/APP_DEBUG=false/' .env\n\n   # Faça outras alterações no seu arquivo .env, se necessário\n\n   # Instala as dependências\n   RUN composer install --ignore-platform-reqs --no-dev -a\n\n   # Compila o binário estático\n   WORKDIR /go/src/app/\n   RUN EMBED=dist/app/ ./build-static.sh\n   ```\n\n   > [!CAUTION]\n   >\n   > Alguns arquivos `.dockerignore` ignorarão o diretório `vendor/` e os\n   > arquivos `.env`.\n   > Certifique-se de ajustar ou remover o arquivo `.dockerignore` antes da\n   > compilação.\n\n2. Construa:\n\n   ```console\n   docker build -t static-laravel-app -f static-build.Dockerfile .\n   ```\n\n3. Extraia o binário:\n\n   ```console\n   docker cp $(docker create --name static-laravel-app-tmp static-laravel-app):/go/src/app/dist/frankenphp-linux-x86_64 frankenphp ; docker rm static-laravel-app-tmp\n   ```\n\n4. Popule os caches:\n\n   ```console\n   frankenphp php-cli artisan optimize\n   ```\n\n5. Execute as migrações de banco de dados (se houver):\n\n   ```console\n   frankenphp php-cli artisan migrate\n   ```\n\n6. Gere a chave secreta da aplicação:\n\n   ```console\n   frankenphp php-cli artisan key:generate\n   ```\n\n7. Inicie o servidor:\n\n   ```console\n   frankenphp php-server\n   ```\n\nAgora sua aplicação está pronta!\n\nSaiba mais sobre as opções disponíveis e como compilar binários para outros\nsistemas operacionais na documentação de\n[incorporação de aplicações](embed.md).\n\n### Alterando o caminho do armazenamento\n\nPor padrão, o Laravel armazena arquivos enviados, caches, logs, etc., no\ndiretório `storage/` da aplicação.\nIsso não é adequado para aplicações embarcadas, pois cada nova versão será\nextraída para um diretório temporário diferente.\n\nDefina a variável de ambiente `LARAVEL_STORAGE_PATH` (por exemplo, no seu\narquivo `.env`) ou chame o método\n`Illuminate\\Foundation\\Application::useStoragePath()` para usar um diretório\nfora do diretório temporário.\n\n### Executando o Octane com binários independentes\n\nÉ possível até empacotar aplicações Octane do Laravel como binários\nindependentes!\n\nPara fazer isso, [instale o Octane corretamente](#laravel-octane) e siga os\npassos descritos na\n[seção anterior](#aplicações-laravel-como-binários-independentes).\n\nEm seguida, para iniciar o FrankenPHP no modo worker através do Octane, execute:\n\n```console\nPATH=\"$PWD:$PATH\" frankenphp php-cli artisan octane:frankenphp\n```\n\n> [!CAUTION]\n>\n> Para que o comando funcione, o binário independente **deve** ser nomeado\n> `frankenphp` porque o Octane precisa de um programa chamado `frankenphp`\n> disponível no caminho.\n"
  },
  {
    "path": "docs/pt-br/mercure.md",
    "content": "# Tempo real\n\nO FrankenPHP vem com um hub [Mercure](https://mercure.rocks) integrado!\nO Mercure permite que você envie eventos em tempo real para todos os\ndispositivos conectados: eles receberão um evento JavaScript instantaneamente.\n\nNão é necessária nenhuma biblioteca JS ou SDK!\n\n![Mercure](mercure-hub.png)\n\nPara habilitar o hub Mercure, atualize o `Caddyfile` conforme descrito\n[no site do Mercure](https://mercure.rocks/docs/hub/config).\n\nO caminho do hub Mercure é `/.well-known/mercure`.\nAo executar o FrankenPHP dentro do Docker, a URL de envio completa seria\n`http://php/.well-known/mercure` (com `php` sendo o nome do contêiner que\nexecuta o FrankenPHP).\n\nPara enviar atualizações do Mercure a partir do seu código, recomendamos o\n[Componente Symfony Mercure](https://symfony.com/components/Mercure) (você não\nprecisa do framework full-stack do Symfony para usá-lo).\n"
  },
  {
    "path": "docs/pt-br/metrics.md",
    "content": "# Métricas\n\nQuando as [métricas do Caddy](https://caddyserver.com/docs/metrics) estão\nhabilitadas, o FrankenPHP expõe as seguintes métricas:\n\n- `frankenphp_total_threads`: O número total de threads PHP.\n- `frankenphp_busy_threads`: O número de threads PHP processando uma requisição\n  no momento (workers em execução sempre consomem uma thread).\n- `frankenphp_queue_depth`: O número de requisições regulares na fila.\n- `frankenphp_total_workers{worker=\"[nome_do_worker]\"}`: O número total de\n  workers.\n- `frankenphp_busy_workers{worker=\"[nome_do_worker]\"}`: O número de workers\n  processando uma requisição no momento.\n- `frankenphp_worker_request_time{worker=\"[nome_do_worker]\"}`: O tempo gasto no\n  processamento de requisições por todos os workers.\n- `frankenphp_worker_request_count{worker=\"[nome_do_worker]\"}`: O número de\n  requisições processadas por todos os workers.\n- `frankenphp_ready_workers{worker=\"[nome_do_worker]\"}`: O número de workers que\n  chamaram `frankenphp_handle_request` pelo menos uma vez.\n- `frankenphp_worker_crashes{worker=\"[nome_do_worker]\"}`: O número de vezes que\n  um worker foi encerrado inesperadamente.\n- `frankenphp_worker_restarts{worker=\"[nome_do_worker]\"}`: O número de vezes que\n  um worker foi reiniciado deliberadamente.\n- `frankenphp_worker_queue_depth{worker=\"[nome_do_worker]\"}`: O número de\n  requisições na fila.\n\nPara métricas de worker, o placeholder `[nome_do_worker]` é substituído pelo\nnome do worker no Caddyfile; caso contrário, o caminho absoluto do arquivo do\nworker será usado.\n"
  },
  {
    "path": "docs/pt-br/performance.md",
    "content": "# Desempenho\n\nPor padrão, o FrankenPHP tenta oferecer um bom equilíbrio entre desempenho e\nfacilidade de uso.\nNo entanto, é possível melhorar substancialmente o desempenho usando uma\nconfiguração apropriada.\n\n## Número de Threads e Workers\n\nPor padrão, o FrankenPHP inicia 2 vezes mais threads e workers (no modo worker)\ndo que o número de núcleos de CPU disponíveis.\n\nOs valores apropriados dependem muito de como sua aplicação foi escrita, do que\nela faz e do seu hardware.\nRecomendamos fortemente alterar esses valores. Para melhor estabilidade do sistema, recomenda-se ter `num_threads` x\n`memory_limit` < `available_memory`.\n\nPara encontrar os valores corretos, é melhor executar testes de carga simulando\ntráfego real.\n[k6](https://k6.io) e [Gatling](https://gatling.io) são boas ferramentas para\nisso.\n\nPara configurar o número de threads, use a opção `num_threads` das diretivas\n`php_server` e `php`.\nPara alterar o número de workers, use a opção `num` da seção `worker` da\ndiretiva `frankenphp`.\n\n### `max_threads`\n\nEmbora seja sempre melhor saber exatamente como será o seu tráfego, aplicações\nreais tendem a ser mais imprevisíveis.\nA [configuração](config.md#configuracao-do-caddyfile) `max_threads` permite que\no FrankenPHP crie threads adicionais automaticamente em tempo de execução até o\nlimite especificado.\n`max_threads` pode ajudar você a descobrir quantas threads são necessárias para\nlidar com seu tráfego e pode tornar o servidor mais resiliente a picos de\nlatência.\nSe definido como `auto`, o limite será estimado com base no `memory_limit` em\nseu `php.ini`. Se não for possível fazer isso,\n`auto` assumirá como padrão 2x `num_threads`.\nLembre-se de que `auto` pode subestimar bastante o número de threads\nnecessárias.\n`max_threads` é semelhante ao\n[pm.max_children](https://www.php.net/manual/pt_BR/install.fpm.configuration.php#pm.max-children)\ndo PHP FPM.\nA principal diferença é que o FrankenPHP usa threads em vez de processos e as\ndelega automaticamente entre diferentes worker scripts e o 'classic mode',\nconforme necessário.\n\n## Modo worker\n\nHabilitar [o modo worker](worker.md) melhora drasticamente o desempenho, mas sua\naplicação precisa ser adaptada para ser compatível com este modo: você precisa\ncriar um worker script e garantir que a aplicação não esteja com vazamento de\nmemória.\n\n## Não use musl\n\nA variante Alpine Linux das imagens oficiais do Docker e os binários padrão que\nfornecemos usam [a biblioteca C musl](https://musl.libc.org).\n\nO PHP é conhecido por ser\n[mais lento](https://gitlab.alpinelinux.org/alpine/aports/-/issues/14381)\nao usar esta biblioteca C alternativa em vez da biblioteca GNU tradicional,\nespecialmente quando compilado no modo ZTS (thread-safe), necessário para o\nFrankenPHP. A diferença pode ser significativa em um ambiente com muitas threads.\n\nAlém disso,\n[alguns bugs só acontecem ao usar musl](https://github.com/php/php-src/issues?q=sort%3Aupdated-desc+is%3Aissue+is%3Aopen+label%3ABug+musl).\n\nEm ambientes de produção, recomendamos o uso do FrankenPHP vinculado à glibc, compilado com um nível de otimização apropriado.\n\nIsso pode ser feito usando as imagens Docker do Debian, usando\n[nossos pacotes .deb, .rpm ou .apk dos mantenedores](https://pkgs.henderkes.com), ou\n[compilando o FrankenPHP a partir do código-fonte](compile.md).\n\nPara contêineres mais leves ou seguros, você pode considerar\n[uma imagem Debian reforçada](docker.md#hardening-images) em vez de Alpine.\n\n## Configuração do runtime do Go\n\nO FrankenPHP é escrito em Go.\n\nEm geral, o runtime do Go não requer nenhuma configuração especial, mas em\ncertas circunstâncias, configurações específicas melhoram o desempenho.\n\nVocê provavelmente deseja definir a variável de ambiente `GODEBUG` como\n`cgocheck=0` (o padrão nas imagens Docker do FrankenPHP).\n\nSe você executa o FrankenPHP em contêineres (Docker, Kubernetes, LXC...) e\nlimita a memória disponível para os contêineres, defina a variável de ambiente\n`GOMEMLIMIT` para a quantidade de memória disponível.\n\nPara mais detalhes,\n[a página da documentação do Go dedicada a este assunto](https://pkg.go.dev/runtime#hdr-Environment_Variables)\né uma leitura obrigatória para aproveitar ao máximo o runtime.\n\n## `file_server`\n\nPor padrão, a diretiva `php_server` configura automaticamente um servidor de\narquivos para servir arquivos estáticos (assets) armazenados no diretório raiz.\n\nEste recurso é conveniente, mas tem um custo.\nPara desativá-lo, use a seguinte configuração:\n\n```caddyfile\nphp_server {\n    file_server off\n}\n```\n\n## `try_files`\n\nAlém de arquivos estáticos e arquivos PHP, `php_server` também tentará servir o\narquivo de índice da sua aplicação e os arquivos de índice de diretório (`/path/` ->\n`/path/index.php`).\nSe você não precisa de arquivos de índice de diretório, pode desativá-los definindo\nexplicitamente `try_files` assim:\n\n```caddyfile\nphp_server {\n    try_files {path} index.php\n    root /root/to/your/app # adicionar explicitamente o root aqui permite um melhor cache\n}\n```\n\nIsso pode reduzir significativamente o número de operações desnecessárias com\narquivos.\nUm equivalente no modo worker da configuração anterior seria:\n\n```caddyfile\nroute {\n    php_server { # use \"php\" em vez de \"php_server\" se você não precisar do servidor de arquivos\n        root /root/to/your/app\n        worker /path/to/worker.php {\n            match * # envia todas as requisições diretamente para o worker\n        }\n    }\n}\n```\n\nUma abordagem alternativa com 0 operações desnecessárias no sistema de arquivos\nseria usar a diretiva `php` e dividir os arquivos estáticos dos arquivos PHP por caminho.\nEssa abordagem funciona bem se toda a sua aplicação for servida por um arquivo\nde entrada.\nUm exemplo de [configuração](config.md#configuracao-do-caddyfile) que serve\narquivos estáticos atrás de uma pasta `/assets` poderia ser assim:\n\n```caddyfile\nroute {\n    @assets {\n        path /assets/*\n    }\n\n    # tudo o que está em /assets é gerenciado pelo servidor de arquivos\n    file_server @assets {\n        root /root/to/your/app\n    }\n\n    # tudo o que não está em /assets é gerenciado pelo seu arquivo de índice ou worker PHP\n    rewrite index.php\n    php {\n        root /root/to/your/app # adicionar explicitamente o root aqui permite um melhor cache\n    }\n}\n```\n\n## Placeholders\n\nVocê pode usar\n[placeholders](https://caddyserver.com/docs/conventions#placeholders) nas\ndiretivas `root` e `env`.\nNo entanto, isso impede o armazenamento em cache desses valores e acarreta um\ncusto significativo de desempenho.\n\nSe possível, evite placeholders nessas diretivas.\n\n## `resolve_root_symlink`\n\nPor padrão, se o diretório raiz for um link simbólico, ele será resolvido\nautomaticamente pelo FrankenPHP (isso é necessário para o funcionamento correto\ndo PHP).\nSe o diretório raiz não for um link simbólico, você pode desativar esse recurso.\n\n```caddyfile\nphp_server {\n    resolve_root_symlink false\n}\n```\n\nIsso melhorará o desempenho se a diretiva `root` contiver\n[placeholders](https://caddyserver.com/docs/conventions#placeholders).\nO ganho será insignificante em outros casos.\n\n## Logs\n\nO logging é obviamente muito útil, mas, por definição, requer operações de E/S e\nalocações de memória, o que reduz consideravelmente o desempenho.\nCertifique-se de\n[definir o nível de logging](https://caddyserver.com/docs/caddyfile/options#log)\ncorretamente e registrar em log apenas o necessário.\n\n## Desempenho do PHP\n\nO FrankenPHP usa o interpretador PHP oficial.\nTodas as otimizações de desempenho usuais relacionadas ao PHP se aplicam ao\nFrankenPHP.\n\nEm particular:\n\n- Verifique se o [OPcache](https://www.php.net/manual/pt_BR/book.opcache.php)\n  está instalado, habilitado e configurado corretamente;\n- Habilite as\n  [otimizações do carregador automático do Composer](https://getcomposer.org/doc/articles/autoloader-optimization.md);\n- Certifique-se de que o cache do `realpath` seja grande o suficiente para as\n  necessidades da sua aplicação;\n- Use\n  [pré-carregamento](https://www.php.net/manual/pt_BR/opcache.preloading.php).\n\nPara mais detalhes, leia\n[a entrada dedicada na documentação do Symfony](https://symfony.com/doc/current/performance.html)\n(a maioria das dicas é útil mesmo se você não usa o Symfony).\n\n## Dividindo o Pool de Threads\n\nÉ comum que aplicações interajam com serviços externos lentos, como uma\nAPI que tende a ser instável sob alta carga ou que consistentemente leva mais de 10 segundos para responder.\nNesses casos, pode ser benéfico dividir o pool de threads para ter pools \"lentos\" dedicados.\nIsso impede que os endpoints lentos consumam todos os recursos/threads do servidor e\nlimita a concorrência de requisições direcionadas ao endpoint lento, semelhante a um\npool de conexões.\n\n```caddyfile\nexample.com {\n    php_server {\n        root /app/public # a raiz da sua aplicação\n        worker index.php {\n            match /slow-endpoint/* # todas as requisições com caminho /slow-endpoint/* são tratadas por este pool de threads\n            num 1 # mínimo de 1 thread para requisições que correspondem a /slow-endpoint/*\n            max_threads 20 # permite até 20 threads para requisições que correspondem a /slow-endpoint/*, se necessário\n        }\n        worker index.php {\n            match * # todas as outras requisições são tratadas separadamente\n            num 1 # mínimo de 1 thread para outras requisições, mesmo que os endpoints lentos comecem a travar\n            max_threads 20 # permite até 20 threads para outras requisições, se necessário\n        }\n    }\n}\n```\n\nGeralmente, também é aconselhável lidar com endpoints muito lentos de forma assíncrona, usando mecanismos relevantes como filas de mensagens.\n"
  },
  {
    "path": "docs/pt-br/production.md",
    "content": "# Implantando em produção\n\nNeste tutorial, aprenderemos como implantar uma aplicação PHP em um único\nservidor usando o Docker Compose.\n\nSe você estiver usando o Symfony, leia a documentação\n[Implantar em produção](https://github.com/dunglas/symfony-docker/blob/main/docs/production.md)\ndo projeto Docker do Symfony (que usa FrankenPHP).\n\nSe você estiver usando a API Platform (que também usa FrankenPHP), consulte\n[a documentação de implantação do framework](https://api-platform.com/docs/deployment/).\n\n## Preparando sua aplicação\n\nPrimeiro, crie um `Dockerfile` no diretório raiz do seu projeto PHP:\n\n```dockerfile\nFROM dunglas/frankenphp\n\n# Certifique-se de substituir \"seu-nome-de-dominio.example.com\" pelo seu nome de\n# domínio\nENV SERVER_NAME=seu-nome-de-dominio.example.com\n# Se quiser desabilitar o HTTPS, use este valor:\n#ENV SERVER_NAME=:80\n\n# Se o seu projeto não estiver usando o diretório \"public\" como diretório raiz,\n# você pode defini-lo aqui:\n# ENV SERVER_ROOT=web/\n\n# Habilita as configurações de produção do PHP\nRUN mv \"$PHP_INI_DIR/php.ini-production\" \"$PHP_INI_DIR/php.ini\"\n\n# Copia os arquivos PHP do seu projeto para o diretório public\nCOPY . /app/public\n# Se você usa Symfony ou Laravel, precisa copiar o projeto inteiro:\n#COPY . /app\n```\n\nConsulte [Construindo uma imagem Docker personalizada](docker.md) para mais\ndetalhes e opções, e para aprender como personalizar a configuração, instalar\nextensões PHP e módulos Caddy.\n\nSe o seu projeto usa o Composer, certifique-se de incluí-lo na imagem Docker e\ninstalar suas dependências.\n\nEm seguida, adicione um arquivo `compose.yaml`:\n\n```yaml\nservices:\n  php:\n    image: dunglas/frankenphp\n    restart: always\n    ports:\n      - \"80:80\" # HTTP\n      - \"443:443\" # HTTPS\n      - \"443:443/udp\" # HTTP/3\n    volumes:\n      - caddy_data:/data\n      - caddy_config:/config\n\n# Volumes necessários para certificados e configuração do Caddy\nvolumes:\n  caddy_data:\n  caddy_config:\n```\n\n> [!NOTE]\n>\n> Os exemplos anteriores são destinados ao uso em produção.\n> Em desenvolvimento, você pode querer usar um volume, uma configuração PHP\n> diferente e um valor diferente para a variável de ambiente `SERVER_NAME`.\n>\n> Consulte o projeto [Symfony Docker](https://github.com/dunglas/symfony-docker)\n> (que usa FrankenPHP) para um exemplo mais avançado usando imagens\n> multiestágio, Composer, extensões PHP extras, etc.\n\nFinalmente, se você usa Git, faça o commit e o push desses arquivos.\n\n## Preparando um servidor\n\nPara implantar sua aplicação em produção, você precisa de um servidor.\nNeste tutorial, usaremos uma máquina virtual fornecida pela DigitalOcean, mas\nqualquer servidor Linux pode ser usado.\nSe você já possui um servidor Linux com o Docker instalado, pode pular direto\npara [a próxima seção](#configurando-um-nome-de-domínio).\n\nCaso contrário, use [este link de afiliado](https://m.do.co/c/5d8aabe3ab80) para\nobter US$ 200 em créditos gratuitos, crie uma conta e clique em \"Create a\nDroplet\".\nEm seguida, clique na aba \"Marketplace\" na seção \"Choose an image\" e procure a\naplicação \"Docker\".\nIsso provisionará um servidor Ubuntu com as versões mais recentes do Docker e do\nDocker Compose já instaladas!\n\nPara fins de teste, os planos mais baratos serão suficientes.\nPara uso real em produção, você provavelmente escolherá um plano na seção\n\"General Purpose\" que atenda às suas necessidades.\n\n![Implantando o FrankenPHP na DigitalOcean com Docker](digitalocean-droplet.png)\n\nVocê pode manter os padrões para outras configurações ou ajustá-los de acordo\ncom suas necessidades.\nNão se esqueça de adicionar sua chave SSH ou criar uma senha e, em seguida,\nclicar no botão \"Finalize and create\".\n\nEm seguida, aguarde alguns segundos enquanto seu Droplet é provisionado.\nQuando seu Droplet estiver pronto, use SSH para se conectar:\n\n```console\nssh root@<droplet-ip>\n```\n\n## Configurando um nome de domínio\n\nNa maioria dos casos, você precisará associar um nome de domínio ao seu site.\nSe você ainda não possui um nome de domínio, precisará comprar um por meio de um\nregistrar.\n\nEm seguida, crie um registro DNS do tipo `A` para o seu nome de domínio,\napontando para o endereço IP do seu servidor:\n\n```dns\nseu-nome-de-dominio.example.com.  IN  A  <ip-do-seu-servidor>\n```\n\nExemplo com o serviço DigitalOcean Domains (\"Networking\" > \"Domains\"):\n\n![Configurando DNS na DigitalOcean](digitalocean-dns.png)\n\n> [!NOTE]\n>\n> O Let's Encrypt, o serviço usado por padrão pelo FrankenPHP para gerar\n> automaticamente um certificado TLS, não suporta o uso de endereços IP.\n> O uso de um nome de domínio é obrigatório para usar o Let's Encrypt.\n\n## Implantando\n\nCopie seu projeto para o servidor usando `git clone`, `scp` ou qualquer outra\nferramenta que atenda às suas necessidades.\nSe você usa o GitHub, pode ser útil usar\n[uma chave de implantação](https://docs.github.com/en/free-pro-team@latest/developers/overview/managing-deploy-keys#deploy-keys).\nChaves de implantação também são [suportadas pelo GitLab](https://docs.gitlab.com/ee/user/project/deploy_keys/).\n\nExemplo com Git:\n\n```console\ngit clone git@github.com:<usuario>/<nome-do-projeto>.git\n```\n\nAcesse o diretório que contém seu projeto (`<nome-do-projeto>`) e inicie a\naplicação em modo de produção:\n\n```console\ndocker compose up --wait\n```\n\nSeu servidor está funcionando e um certificado HTTPS foi gerado automaticamente\npara você.\nAcesse `https://seu-nome-de-dominio.example.com` e divirta-se!\n\n> [!CAUTION]\n>\n> O Docker pode ter uma camada de cache; certifique-se de ter a construção\n> correta para cada implantação ou reconstrua seu projeto com a opção\n> `--no-cache` para evitar problemas de cache.\n\n## Implantando em múltiplos nós\n\nSe você deseja implantar sua aplicação em um cluster de máquinas, pode usar o\n[Docker Swarm](https://docs.docker.com/engine/swarm/stack-deploy/), que é\ncompatível com os arquivos Compose fornecidos.\nPara implantar no Kubernetes, consulte o\n[chart do Helm fornecido com a API Platform](https://api-platform.com/docs/deployment/kubernetes/),\nque usa FrankenPHP.\n"
  },
  {
    "path": "docs/pt-br/static.md",
    "content": "# Criar uma compilação estática\n\nEm vez de usar uma instalação local da biblioteca PHP, é possível criar uma\ncompilação estática ou principalmente estática do FrankenPHP graças ao excelente\n[projeto static-php-cli](https://github.com/crazywhalecc/static-php-cli) (apesar\ndo nome, este projeto suporta todas as SAPIs, não apenas CLI).\n\nCom este método, um único binário portátil conterá o interpretador PHP, o\nservidor web Caddy e o FrankenPHP!\n\nExecutáveis nativos totalmente estáticos não requerem dependências e podem até\nser executados na\n[imagem Docker `scratch`](https://docs.docker.com/build/building/base-images/#create-a-minimal-base-image-using-scratch).\nNo entanto, eles não podem carregar extensões PHP dinâmicas (como o Xdebug) e\ntêm algumas limitações por usarem a `libc` `musl`.\n\nA maioria dos binários estáticos requer apenas `glibc` e pode carregar extensões\ndinâmicas.\n\nSempre que possível, recomendamos o uso de compilações principalmente estáticas\nbaseadas na `glibc`.\n\nO FrankenPHP também suporta\n[a incorporação da aplicação PHP no binário estático](embed.md).\n\n## Linux\n\nFornecemos imagens Docker para compilar binários estáticos para Linux:\n\n### Compilação totalmente estática baseada na `musl`\n\nPara um binário totalmente estático que roda em qualquer distribuição Linux sem\ndependências, mas não suporta carregamento dinâmico de extensões:\n\n```console\ndocker buildx bake --load static-builder-musl\ndocker cp $(docker create --name static-builder-musl dunglas/frankenphp:static-builder-musl):/go/src/app/dist/frankenphp-linux-$(uname -m) frankenphp ; docker rm static-builder-musl\n```\n\nPara melhor desempenho em cenários com alta concorrência, considere usar o\nalocador [`mimalloc`](https://github.com/microsoft/mimalloc).\n\n```console\ndocker buildx bake --load --set static-builder-musl.args.MIMALLOC=1 static-builder-musl\n```\n\n### Compilação principalmente estática baseada na `glibc` (com suporte a extensões dinâmicas)\n\nPara um binário que suporta o carregamento dinâmico de extensões PHP, mantendo\nas extensões selecionadas compiladas estaticamente:\n\n```console\ndocker buildx bake --load static-builder-gnu\ndocker cp $(docker create --name static-builder-gnu dunglas/frankenphp:static-builder-gnu):/go/src/app/dist/frankenphp-linux-$(uname -m) frankenphp ; docker rm static-builder-gnu\n```\n\nEste binário suporta todas as versões 2.17 e superiores da `glibc`, mas não roda\nem sistemas baseados em `musl` (como o Alpine Linux).\n\nO binário principalmente estático (exceto a `glibc`) resultante é chamado\n`frankenphp` e está disponível no diretório atual.\n\nSe você quiser compilar o binário estático sem o Docker, consulte as instruções\npara macOS, que também funcionam para Linux.\n\n### Extensões personalizadas\n\nPor padrão, as extensões PHP mais populares são compiladas.\n\nPara reduzir o tamanho do binário e a superfície de ataque, você pode escolher a\nlista de extensões a serem compiladas usando o `ARG` `PHP_EXTENSIONS` do Docker.\n\nPor exemplo, execute o seguinte comando para compilar apenas a extensão\n`opcache`:\n\n```console\ndocker buildx bake --load --set static-builder-musl.args.PHP_EXTENSIONS=opcache,pdo_sqlite static-builder-musl\n# ...\n```\n\nPara adicionar bibliotecas que habilitem funcionalidades adicionais às extensões\nhabilitadas, você pode passar o `ARG` `PHP_EXTENSION_LIBS` do Docker:\n\n```console\ndocker buildx bake \\\n  --load \\\n  --set static-builder-musl.args.PHP_EXTENSIONS=gd \\\n  --set static-builder-musl.args.PHP_EXTENSION_LIBS=libjpeg,libwebp \\\n  static-builder-musl\n```\n\n### Módulos Caddy extras\n\nPara adicionar módulos Caddy extras ou passar outros argumentos para o\n[`xcaddy`](https://github.com/caddyserver/xcaddy), use o `ARG` `XCADDY_ARGS` do\nDocker:\n\n```console\ndocker buildx bake \\\n  --load \\\n  --set static-builder-musl.args.XCADDY_ARGS=\"--with github.com/darkweak/souin/plugins/caddy --with github.com/dunglas/caddy-cbrotli --with github.com/dunglas/mercure/caddy --with github.com/dunglas/vulcain/caddy\" \\\n  static-builder-musl\n```\n\nNeste exemplo, adicionamos o módulo de cache HTTP [Souin](https://souin.io) para\no Caddy, bem como os módulos\n[cbrotli](https://github.com/dunglas/caddy-cbrotli),\n[Mercure](https://mercure.rocks) e [Vulcain](https://vulcain.rocks).\n\n> [!TIP]\n>\n> Os módulos cbrotli, Mercure e Vulcain são incluídos por padrão se\n> `XCADDY_ARGS` estiver vazio ou não definido.\n> Se você personalizar o valor de `XCADDY_ARGS`, deverá incluí-los\n> explicitamente se desejar que sejam incluídos.\n\nVeja também como [personalizar a compilação](#personalizando-a-compilação).\n\n### Token do GitHub\n\nSe você atingir o limite de taxa da API do GitHub, defina um Token de Acesso\nPessoal do GitHub em uma variável de ambiente chamada `GITHUB_TOKEN`:\n\n```console\nGITHUB_TOKEN=\"xxx\" docker --load buildx bake static-builder-musl\n# ...\n```\n\n## macOS\n\nExecute o seguinte script para criar um binário estático para macOS (você\nprecisa ter o [Homebrew](https://brew.sh/) instalado):\n\n```console\ngit clone https://github.com/php/frankenphp\ncd frankenphp\n./build-static.sh\n```\n\nObservação: este script também funciona no Linux (e provavelmente em outros\nsistemas Unix) e é usado internamente pelas imagens Docker que fornecemos.\n\n## Personalizando a compilação\n\nAs seguintes variáveis de ambiente podem ser passadas para `docker build` e para\no script `build-static.sh` para personalizar a compilação estática:\n\n- `FRANKENPHP_VERSION`: a versão do FrankenPHP a ser usada;\n- `PHP_VERSION`: a versão do PHP a ser usada;\n- `PHP_EXTENSIONS`: as extensões PHP a serem compiladas\n  ([lista de extensões suportadas](https://static-php.dev/en/guide/extensions.html));\n- `PHP_EXTENSION_LIBS`: bibliotecas extras a serem compiladas que adicionam\n  recursos às extensões;\n- `XCADDY_ARGS`: argumentos a passar para o\n  [`xcaddy`](https://github.com/caddyserver/xcaddy), por exemplo, para adicionar\n  módulos Caddy extras;\n- `EMBED`: caminho da aplicação PHP a ser incorporada no binário;\n- `CLEAN`: quando definida, a `libphp` e todas as suas dependências são\n  compiladas do zero (sem cache);\n- `NO_COMPRESS`: não compacta o binário resultante usando UPX;\n- `DEBUG_SYMBOLS`: quando definida, os símbolos de depuração não serão removidos\n  e serão adicionados ao binário;\n- `MIMALLOC`: (experimental, somente Linux) substitui `mallocng` da `musl` por\n  [`mimalloc`](https://github.com/microsoft/mimalloc) para melhor desempenho.\n  Recomendamos usar isso apenas para compilações direcionadas à `musl`; para\n  `glibc`, prefira desabilitar essa opção e usar\n  [`LD_PRELOAD`](https://microsoft.github.io/mimalloc/overrides.html) ao\n  executar seu binário;\n- `RELEASE`: (somente pessoas mantenedoras) quando definida, o binário\n  resultante será enviado para o GitHub.\n\n## Extensões\n\nCom os binários baseados na `glibc` ou no macOS, você pode carregar extensões\nPHP dinamicamente.\nNo entanto, essas extensões precisarão ser compiladas com suporte a ZTS.\nComo a maioria dos gerenciadores de pacotes não oferece atualmente versões ZTS\nde suas extensões, você terá que compilá-las você mesmo.\n\nPara isso, você pode compilar e executar o contêiner Docker\n`static-builder-gnu`, acessá-lo remotamente e compilar as extensões com\n`./configure --with-php-config=/go/src/app/dist/static-php-cli/buildroot/bin/php-config`.\n\nPassos de exemplo para [a extensão Xdebug](https://xdebug.org):\n\n```console\ndocker build -t gnu-ext -f static-builder-gnu.Dockerfile --build-arg FRANKENPHP_VERSION=1.0 .\ndocker create --name static-builder-gnu -it gnu-ext /bin/sh\ndocker start static-builder-gnu\ndocker exec -it static-builder-gnu /bin/sh\ncd /go/src/app/dist/static-php-cli/buildroot/bin\ngit clone https://github.com/xdebug/xdebug.git && cd xdebug\nsource scl_source enable devtoolset-10\n../phpize\n./configure --with-php-config=/go/src/app/dist/static-php-cli/buildroot/bin/php-config\nmake\nexit\ndocker cp static-builder-gnu:/go/src/app/dist/static-php-cli/buildroot/bin/xdebug/modules/xdebug.so xdebug-zts.so\ndocker cp static-builder-gnu:/go/src/app/dist/frankenphp-linux-$(uname -m) ./frankenphp\ndocker stop static-builder-gnu\ndocker rm static-builder-gnu\ndocker rmi gnu-ext\n```\n\nIsso criará `frankenphp` e `xdebug-zts.so` no diretório atual.\nSe você mover `xdebug-zts.so` para o diretório de extensões, adicione\n`zend_extension=xdebug-zts.so` ao seu `php.ini` e execute o FrankenPHP, ele\ncarregará o Xdebug.\n"
  },
  {
    "path": "docs/pt-br/worker.md",
    "content": "# Usando Workers do FrankenPHP\n\nInicialize sua aplicação uma vez e mantenha-a na memória.\nO FrankenPHP processará as requisições recebidas em poucos milissegundos.\n\n## Iniciando Worker Scripts\n\n### Docker\n\nDefina o valor da variável de ambiente `FRANKENPHP_CONFIG` como `worker /path/to/your/worker/script.php`:\n\n```console\ndocker run \\\n    -e FRANKENPHP_CONFIG=\"worker /app/path/to/your/worker/script.php\" \\\n    -v $PWD:/app \\\n    -p 80:80 -p 443:443 -p 443:443/udp \\\n    dunglas/frankenphp\n```\n\n### Binário independente\n\nUse a opção `--worker` do comando `php-server` para servir o conteúdo do diretório atual usando um worker:\n\n```console\nfrankenphp php-server --worker /path/to/your/worker/script.php\n```\n\nSe a sua aplicação PHP estiver [embutida no binário](embed.md), você pode adicionar um `Caddyfile` personalizado no diretório raiz da aplicação.\nEle será usado automaticamente.\n\nTambém é possível [reiniciar o worker em caso de alterações em arquivos](config.md#watching-for-file-changes) com a opção `--watch`.\nO comando a seguir acionará uma reinicialização se qualquer arquivo terminado em `.php` no diretório `/path/to/your/app/` ou subdiretórios for modificado:\n\n```console\nfrankenphp php-server --worker /path/to/your/worker/script.php --watch=\"/path/to/your/app/**/*.php\"\n```\n\nEste recurso é frequentemente usado em combinação com [hot reloading](hot-reload.md).\n\n## Symfony Runtime\n\n> [!TIP]\n> A seção a seguir é necessária apenas antes do Symfony 7.4, onde o suporte nativo para o modo worker do FrankenPHP foi introduzido.\n\nO modo worker do FrankenPHP é suportado pelo [Componente Symfony Runtime](https://symfony.com/doc/current/components/runtime.html).\nPara iniciar qualquer aplicação Symfony em um worker, instale o pacote FrankenPHP do [PHP Runtime](https://github.com/php-runtime/runtime):\n\n```console\ncomposer require runtime/frankenphp-symfony\n```\n\nInicie seu servidor de aplicações definindo a variável de ambiente `APP_RUNTIME` para usar o Symfony Runtime do FrankenPHP:\n\n```console\ndocker run \\\n    -e FRANKENPHP_CONFIG=\"worker ./public/index.php\" \\\n    -e APP_RUNTIME=Runtime\\\\FrankenPhpSymfony\\\\Runtime \\\n    -v $PWD:/app \\\n    -p 80:80 -p 443:443 -p 443:443/udp \\\n    dunglas/frankenphp\n```\n\n## Laravel Octane\n\nConsulte [a documentação dedicada](laravel.md#laravel-octane).\n\n## Aplicações personalizadas\n\nO exemplo a seguir mostra como criar seu próprio worker script sem depender de uma biblioteca de terceiros:\n\n```php\n<?php\n// public/index.php\n\n// Inicializa a aplicação\nrequire __DIR__.'/vendor/autoload.php';\n\n$myApp = new \\App\\Kernel();\n$myApp->boot();\n\n// Manipulador fora do loop para melhor desempenho (fazendo menos trabalho)\n$handler = static function () use ($myApp) {\n    try {\n        // Chamado quando uma requisição é recebida,\n        // superglobais, php://input e similares são redefinidos\n        echo $myApp->handle($_GET, $_POST, $_COOKIE, $_FILES, $_SERVER);\n    } catch (\\Throwable $exception) {\n        // `set_exception_handler` é chamado apenas quando o worker script termina,\n        // o que pode não ser o que você espera, então capture e trate exceções aqui\n        (new \\MyCustomExceptionHandler)->handleException($exception);\n    }\n};\n\n$maxRequests = (int)($_SERVER['MAX_REQUESTS'] ?? 0);\nfor ($nbRequests = 0; !$maxRequests || $nbRequests < $maxRequests; ++$nbRequests) {\n    $keepRunning = \\frankenphp_handle_request($handler);\n\n    // Faz algo depois de enviar a resposta HTTP\n    $myApp->terminate();\n\n    // Chama o coletor de lixo para reduzir as chances de ele ser acionado no meio da geração de uma página\n    gc_collect_cycles();\n\n    if (!$keepRunning) break;\n}\n\n// Limpeza\n$myApp->shutdown();\n```\n\nEm seguida, inicie sua aplicação e use a variável de ambiente `FRANKENPHP_CONFIG` para configurar seu worker:\n\n```console\ndocker run \\\n    -e FRANKENPHP_CONFIG=\"worker ./public/index.php\" \\\n    -v $PWD:/app \\\n    -p 80:80 -p 443:443 -p 443:443/udp \\\n    dunglas/frankenphp\n```\n\nPor padrão, são iniciados 2 workers por CPU.\nVocê também pode configurar o número de workers a serem iniciados:\n\n```console\ndocker run \\\n    -e FRANKENPHP_CONFIG=\"worker ./public/index.php 42\" \\\n    -v $PWD:/app \\\n    -p 80:80 -p 443:443 -p 443:443/udp \\\n    dunglas/frankenphp\n```\n\n### Reiniciar o Worker Após um Certo Número de Requisições\n\nComo o PHP não foi originalmente projetado para processos de longa duração, ainda existem muitas bibliotecas e códigos legados que vazam memória.\nUma solução alternativa para usar esse tipo de código no modo worker é reiniciar o worker script após processar um certo número de requisições:\n\nO trecho de código de worker anterior permite configurar um número máximo de requisições a serem processadas, definindo uma variável de ambiente chamada `MAX_REQUESTS`.\n\n### Reiniciar os Workers Manualmente\n\nEmbora seja possível reiniciar os workers [em alterações de arquivo](config.md#watching-for-file-changes), também é possível reiniciar todos os workers graciosamente por meio da [API de administração do Caddy](https://caddyserver.com/docs/api). Se o administrador estiver habilitado no seu [Caddyfile](config.md#caddyfile-config), você pode acionar o endpoint de reinicialização com uma simples requisição POST como esta:\n\n```console\ncurl -X POST http://localhost:2019/frankenphp/workers/restart\n```\n\n### Falhas de Worker\n\nSe um worker script travar com um código de saída diferente de zero, o FrankenPHP o reiniciará com uma estratégia de backoff exponencial.\nSe o worker script permanecer ativo por mais tempo do que o último backoff \\* 2, ele não irá penalizar o worker script e reiniciá-lo novamente.\nNo entanto, se o worker script continuar a falhar com um código de saída diferente de zero em um curto período de tempo (por exemplo, com um erro de digitação em um script), o FrankenPHP travará com o erro: `too many consecutive failures`.\n\nO número de falhas consecutivas pode ser configurado no seu [Caddyfile](config.md#caddyfile-config) com a opção `max_consecutive_failures`:\n\n```caddyfile\nfrankenphp {\n    worker {\n        # ...\n        max_consecutive_failures 10\n    }\n}\n```\n\n## Comportamento das Superglobais\n\nAs [superglobais do PHP](https://www.php.net/manual/pt_BR/language.variables.superglobals.php) (`$_SERVER`, `$_ENV`, `$_GET`...) se comportam da seguinte maneira:\n\n- antes da primeira chamada para `frankenphp_handle_request()`, as superglobais contêm valores vinculados ao próprio worker script.\n- durante e após a chamada para `frankenphp_handle_request()`, as superglobais contêm valores gerados a partir da requisição HTTP processada. Cada chamada para `frankenphp_handle_request()` altera os valores das superglobais.\n\nPara acessar as superglobais do worker script dentro do retorno de chamada, você deve copiá-las e importar a cópia para o escopo do retorno de chamada:\n\n```php\n<?php\n// Copia a superglobal $_SERVER do worker antes da primeira chamada para\n// frankenphp_handle_request()\n$workerServer = $_SERVER;\n\n$handler = static function () use ($workerServer) {\n    var_dump($_SERVER); // $_SERVER vinculada à requisição\n    var_dump($workerServer); // $_SERVER do worker script\n};\n\n// ...\n"
  },
  {
    "path": "docs/pt-br/x-sendfile.md",
    "content": "# Servindo arquivos estáticos grandes com eficiência (`X-Sendfile`/`X-Accel-Redirect`)\n\nNormalmente, arquivos estáticos podem ser servidos diretamente pelo servidor\nweb, mas às vezes é necessário executar algum código PHP antes de enviá-los:\ncontrole de acesso, estatísticas, cabeçalhos HTTP personalizados...\n\nInfelizmente, usar PHP para servir arquivos estáticos grandes é ineficiente em\ncomparação com o uso direto do servidor web (sobrecarga de memória, desempenho\nreduzido...).\n\nO FrankenPHP permite delegar o envio de arquivos estáticos ao servidor web\n**após** a execução do código PHP personalizado.\n\nPara fazer isso, sua aplicação PHP só precisa definir um cabeçalho HTTP\npersonalizado contendo o caminho do arquivo a ser servido.\nO FrankenPHP cuida do resto.\n\nEsse recurso é conhecido como **`X-Sendfile`** para Apache e\n**`X-Accel-Redirect`** para NGINX.\n\nNos exemplos a seguir, assumimos que o diretório raiz do projeto é o diretório\n`public/` e que queremos usar PHP para servir arquivos armazenados fora do\ndiretório `public/`, de um diretório chamado `arquivos-privados/`.\n\n## Configuração\n\nPrimeiro, adicione a seguinte configuração ao seu `Caddyfile` para habilitar\neste recurso:\n\n```patch\n\troot public/\n\t# ...\n\n+\t# Necessário para Symfony, Laravel e outros projetos que usam o componente\n+\t# Symfony HttpFoundation\n+\trequest_header X-Sendfile-Type x-accel-redirect\n+\trequest_header X-Accel-Mapping ../arquivos-privados=/arquivos-privados\n+\n+\tintercept {\n+\t\t@accel header X-Accel-Redirect *\n+\t\thandle_response @accel {\n+\t\t\troot arquivos-privados/\n+\t\t\trewrite * {resp.header.X-Accel-Redirect}\n+\t\t\tmethod * GET\n+\n+\t\t\t# Remove o cabeçalho X-Accel-Redirect definido pelo PHP para maior\n+\t\t\t# segurança\n+\t\t\theader -X-Accel-Redirect\n+\n+\t\t\tfile_server\n+\t\t}\n+\t}\n\n\tphp_server\n```\n\n## PHP simples\n\nDefina o caminho relativo do arquivo (de `arquivos-privados/`) como o valor do\ncabeçalho `X-Accel-Redirect`:\n\n```php\nheader('X-Accel-Redirect: arquivo.txt');\n```\n\n## Projetos que utilizam o componente Symfony HttpFoundation (Symfony, Laravel, Drupal...)\n\nSymfony HttpFoundation\n[suporta nativamente este recurso](https://symfony.com/doc/current/components/http_foundation.html#serving-files).\nEle determinará automaticamente o valor correto para o cabeçalho\n`X-Accel-Redirect` e o adicionará à resposta.\n\n```php\nuse Symfony\\Component\\HttpFoundation\\BinaryFileResponse;\n\nBinaryFileResponse::trustXSendfileTypeHeader();\n$response = new BinaryFileResponse(__DIR__.'/../arquivos-privados/arquivo.txt');\n\n// ...\n```\n"
  },
  {
    "path": "docs/ru/CONTRIBUTING.md",
    "content": "# Участие в проекте\n\n## Компиляция PHP\n\n### С помощью Docker (Linux)\n\nСоздайте образ Docker для разработки:\n\n```console\ndocker build -t frankenphp-dev -f dev.Dockerfile .\ndocker run --cap-add=SYS_PTRACE --security-opt seccomp=unconfined -p 8080:8080 -p 443:443 -p 443:443/udp -v $PWD:/go/src/app -it frankenphp-dev\n```\n\nОбраз содержит стандартные инструменты для разработки (Go, GDB, Valgrind, Neovim и др.) и использует следующие пути для настроек PHP\n\n- php.ini: `/etc/frankenphp/php.ini` По умолчанию предоставляется файл php.ini с настройками для разработки.\n- дополнительные файлы конфигурации: `/etc/frankenphp/php.d/*.ini`\n- расширения php: `/usr/lib/frankenphp/modules/`\n\nЕсли ваша версия Docker ниже 23.0, сборка может завершиться ошибкой из-за [проблемы с шаблонами dockerignore](https://github.com/moby/moby/pull/42676). Добавьте в `.dockerignore` следующие директории:\n\n```patch\n !testdata/*.php\n !testdata/*.txt\n+!caddy\n+!internal\n```\n\n### Без Docker (Linux и macOS)\n\n[Следуйте инструкциям по компиляции из исходников](https://frankenphp.dev/docs/compile/) и укажите флаг конфигурации `--debug`.\n\n## Запуск тестов\n\n```console\ngo test -race -v ./...\n```\n\n## Модуль Caddy\n\nСоберите Caddy с модулем FrankenPHP:\n\n```console\ncd caddy/frankenphp/\ngo build -tags nobadger,nomysql,nopgx\ncd ../../\n```\n\nЗапустите Caddy с модулем FrankenPHP:\n\n```console\ncd testdata/\n../caddy/frankenphp/frankenphp run\n```\n\nСервер будет доступен по адресу `127.0.0.1:8080`:\n\n```console\ncurl -vk https://localhost/phpinfo.php\n```\n\n## Минимальный тестовый сервер\n\nСоберите минимальный тестовый сервер:\n\n```console\ncd internal/testserver/\ngo build\ncd ../../\n```\n\nЗапустите тестовый сервер:\n\n```console\ncd testdata/\n../internal/testserver/testserver\n```\n\nСервер будет доступен по адресу `127.0.0.1:8080`:\n\n```console\ncurl -v http://127.0.0.1:8080/phpinfo.php\n```\n\n## Локальная сборка Docker-образов\n\nВыведите план bake:\n\n```console\ndocker buildx bake -f docker-bake.hcl --print\n```\n\nСоберите образы FrankenPHP для amd64 локально:\n\n```console\ndocker buildx bake -f docker-bake.hcl --pull --load --set \"*.platform=linux/amd64\"\n```\n\nСоберите образы FrankenPHP для arm64 локально:\n\n```console\ndocker buildx bake -f docker-bake.hcl --pull --load --set \"*.platform=linux/arm64\"\n```\n\nСоберите образы FrankenPHP с нуля для arm64 и amd64 и отправьте их в Docker Hub:\n\n```console\ndocker buildx bake -f docker-bake.hcl --pull --no-cache --push\n```\n\n## Отладка ошибок сегментации с использованием статических сборок\n\n1. Скачайте отладочную версию бинарного файла FrankenPHP с GitHub или создайте собственную статическую сборку с включённым отладочным режимом:\n\n   ```console\n   docker buildx bake \\\n       --load \\\n       --set static-builder.args.DEBUG_SYMBOLS=1 \\\n       --set \"static-builder.platform=linux/amd64\" \\\n       static-builder\n   docker cp $(docker create --name static-builder-musl dunglas/frankenphp:static-builder-musl):/go/src/app/dist/frankenphp-linux-$(uname -m) frankenphp\n   ```\n\n2. Замените текущую версию `frankenphp` на бинарный файл с включенным отладочным режимом.\n3. Запустите FrankenPHP как обычно (или сразу запустите FrankenPHP с GDB: `gdb --args frankenphp run`).\n4. Подключитесь к процессу через GDB:\n\n   ```console\n   gdb -p `pidof frankenphp`\n   ```\n\n5. При необходимости введите `continue` в консоли GDB.\n6. Вызовите сбой FrankenPHP.\n7. Введите `bt` в консоли GDB.\n8. Скопируйте вывод.\n\n## Отладка ошибок сегментации в GitHub Actions\n\n1. Откройте файл `.github/workflows/tests.yml`.\n2. Включите режим отладки PHP:\n\n   ```patch\n       - uses: shivammathur/setup-php@v2\n         # ...\n         env:\n           phpts: ts\n   +       debug: true\n   ```\n\n3. Настройте `tmate` для удалённого подключения к контейнеру:\n\n   ```patch\n       -\n         name: Set CGO flags\n         run: echo \"CGO_CFLAGS=$(php-config --includes)\" >> \"$GITHUB_ENV\"\n   +   -\n   +     run: |\n   +       sudo apt install gdb\n   +       mkdir -p /home/runner/.config/gdb/\n   +       printf \"set auto-load safe-path /\\nhandle SIG34 nostop noprint pass\" > /home/runner/.config/gdb/gdbinit\n   +   -\n   +     uses: mxschmitt/action-tmate@v3\n   ```\n\n4. Подключитесь к контейнеру.\n5. Откройте файл `frankenphp.go`.\n6. Включите `cgosymbolizer`:\n\n   ```patch\n   -\t//_ \"github.com/ianlancetaylor/cgosymbolizer\"\n   +\t_ \"github.com/ianlancetaylor/cgosymbolizer\"\n   ```\n\n7. Загрузите модуль: `go get`.\n8. В контейнере используйте GDB и другие инструменты:\n\n   ```console\n   go test -c -ldflags=-w\n   gdb --args frankenphp.test -test.run ^MyTest$\n   ```\n\n9. После исправления ошибки откатите все внесенные изменения.\n\n## Дополнительные ресурсы для разработки\n\n- [Встраивание PHP в uWSGI](https://github.com/unbit/uwsgi/blob/master/plugins/php/php_plugin.c)\n- [Встраивание PHP в NGINX Unit](https://github.com/nginx/unit/blob/master/src/nxt_php_sapi.c)\n- [Встраивание PHP в Go (go-php)](https://github.com/deuill/go-php)\n- [Встраивание PHP в Go (GoEmPHP)](https://github.com/mikespook/goemphp)\n- [Встраивание PHP в C++](https://gist.github.com/paresy/3cbd4c6a469511ac7479aa0e7c42fea7)\n- [Книга \"Extending and Embedding PHP\" Сары Големан](https://books.google.fr/books?id=zMbGvK17_tYC&pg=PA254&lpg=PA254#v=onepage&q&f=false)\n- [Статья: Что такое TSRMLS_CC?](http://blog.golemon.com/2006/06/what-heck-is-tsrmlscc-anyway.html)\n- [SDL bindings](https://pkg.go.dev/github.com/veandco/go-sdl2@v0.4.21/sdl#Main)\n\n## Docker-ресурсы\n\n- [Определение файлов bake](https://docs.docker.com/build/customize/bake/file-definition/)\n- [Документация по команде `docker buildx build`](https://docs.docker.com/engine/reference/commandline/buildx_build/)\n\n## Полезные команды\n\n```console\napk add strace util-linux gdb\nstrace -e 'trace=!futex,epoll_ctl,epoll_pwait,tgkill,rt_sigreturn' -p 1\n```\n\n## Перевод документации\n\nЧтобы перевести документацию и сайт на новый язык, выполните следующие шаги:\n\n1. Создайте новую директорию с 2-буквенным ISO-кодом языка в папке `docs/`.\n2. Скопируйте все `.md` файлы из корня папки `docs/` в новую директорию (используйте английскую версию как основу для перевода).\n3. Скопируйте файлы `README.md` и `CONTRIBUTING.md` из корневой директории в новую папку.\n4. Переведите содержимое файлов, но не изменяйте имена файлов. Не переводите строки, начинающиеся с `> [!`, это специальная разметка GitHub.\n5. Создайте Pull Request с переводом.\n6. В [репозитории сайта](https://github.com/dunglas/frankenphp-website/tree/main) скопируйте и переведите файлы в папках `content/`, `data/` и `i18n/`.\n7. Переведите значения в созданных YAML-файлах.\n8. Откройте Pull Request в репозитории сайта.\n"
  },
  {
    "path": "docs/ru/README.md",
    "content": "# FrankenPHP: Современный сервер приложений для PHP\n\n<h1 align=\"center\"><a href=\"https://frankenphp.dev\"><img src=\"../../frankenphp.png\" alt=\"FrankenPHP\" width=\"600\"></a></h1>\n\n**FrankenPHP** — это современный сервер приложений для PHP, построенный на базе веб-сервера [Caddy](https://caddyserver.com/).\n\nFrankenPHP добавляет новые возможности вашим PHP-приложениям благодаря следующим функциям: [_Early Hints_](https://frankenphp.dev/docs/early-hints/), [Worker режим](https://frankenphp.dev/docs/worker/), [Real-time режим](https://frankenphp.dev/docs/mercure/), автоматическая поддержка HTTPS, HTTP/2 и HTTP/3.\n\nFrankenPHP совместим с любыми PHP-приложениями и значительно ускоряет ваши проекты на Laravel и Symfony благодаря их официальной поддержке в worker режиме.\n\nFrankenPHP также может использоваться как автономная Go-библиотека для встраивания PHP в любое приложение с использованием `net/http`.\n\n[**Узнайте больше** на сайте _frankenphp.dev_](https://frankenphp.dev) или из этой презентации:\n\n<a href=\"https://dunglas.dev/2022/10/frankenphp-the-modern-php-app-server-written-in-go/\"><img src=\"https://dunglas.dev/wp-content/uploads/2022/10/frankenphp.png\" alt=\"Slides\" width=\"600\"></a>\n\n## Начало работы\n\nВ Windows используйте [WSL](https://learn.microsoft.com/windows/wsl/) для запуска FrankenPHP.\n\n### Скрипт установки\n\nСкопируйте и выполните эту команду в терминале, чтобы автоматически установить подходящую версию для вашей платформы:\n\n```console\ncurl https://frankenphp.dev/install.sh | sh\n```\n\n### Автономный бинарный файл\n\nЕсли вы предпочитаете не использовать Docker, мы предоставляем автономные статические бинарные файлы FrankenPHP для Linux и macOS, включающие [PHP 8.4](https://www.php.net/releases/8.4/en.php) и большинство популярных PHP‑расширений.\n\n[Скачать FrankenPHP](https://github.com/php/frankenphp/releases)\n\n**Установка расширений:** Наиболее распространенные расширения уже включены. Устанавливать дополнительные расширения невозможно.\n\n### Пакеты rpm\n\nНаши мейнтейнеры предлагают rpm‑пакеты для всех систем с `dnf`. Для установки выполните:\n\n```console\nsudo dnf install https://rpm.henderkes.com/static-php-1-0.noarch.rpm\nsudo dnf module enable php-zts:static-8.4 # доступны 8.2–8.5\nsudo dnf install frankenphp\n```\n\n**Установка расширений:** `sudo dnf install php-zts-<extension>`\n\nДля расширений, недоступных по умолчанию, используйте [PIE](https://github.com/php/pie):\n\n```console\nsudo dnf install pie-zts\nsudo pie-zts install asgrim/example-pie-extension\n```\n\n### Пакеты deb\n\nНаши мейнтейнеры предлагают deb‑пакеты для всех систем с `apt`. Для установки выполните:\n\n```console\nsudo curl -fsSL https://key.henderkes.com/static-php.gpg -o /usr/share/keyrings/static-php.gpg && \\\necho \"deb [signed-by=/usr/share/keyrings/static-php.gpg] https://deb.henderkes.com/ stable main\" | sudo tee /etc/apt/sources.list.d/static-php.list && \\\nsudo apt update\nsudo apt install frankenphp\n```\n\n**Установка расширений:** `sudo apt install php-zts-<extension>`\n\nДля расширений, недоступных по умолчанию, используйте [PIE](https://github.com/php/pie):\n\n```console\nsudo apt install pie-zts\nsudo pie-zts install asgrim/example-pie-extension\n```\n\n### Docker\n\n```console\ndocker run -v .:/app/public \\\n    -p 80:80 -p 443:443 -p 443:443/udp \\\n    dunglas/frankenphp\n```\n\nПерейдите по адресу `https://localhost` и наслаждайтесь!\n\n> [!TIP]\n>\n> Не используйте `https://127.0.0.1`. Используйте `https://localhost` и настройте самоподписанный сертификат.  \n> Чтобы изменить используемый домен, настройте переменную окружения [`SERVER_NAME`](config.md#переменные-окружения).\n\n### Homebrew\n\nFrankenPHP также доступен как пакет [Homebrew](https://brew.sh) для macOS и Linux.\n\n```console\nbrew install dunglas/frankenphp/frankenphp\n```\n\n**Установка расширений:** Используйте [PIE](https://github.com/php/pie).\n\n### Использование\n\nДля запуска содержимого текущего каталога выполните:\n\n```console\nfrankenphp php-server\n```\n\nТакже можно запускать CLI‑скрипты:\n\n```console\nfrankenphp php-cli /path/to/your/script.php\n```\n\nДля пакетов deb и rpm можно запустить сервис systemd:\n\n```console\nsudo systemctl start frankenphp\n```\n\n## Документация\n\n- [Worker режим](https://frankenphp.dev/docs/worker/)\n- [Поддержка Early Hints (103 HTTP статус код)](https://frankenphp.dev/docs/early-hints/)\n- [Real-time режим](https://frankenphp.dev/docs/mercure/)\n- [Конфигурация](https://frankenphp.dev/docs/config/)\n- [Docker-образы](https://frankenphp.dev/docs/docker/)\n- [Деплой в продакшен](https://frankenphp.dev/docs/production/)\n- [Оптимизация производительности](https://frankenphp.dev/docs/performance/)\n- [Создание автономного PHP-приложений](https://frankenphp.dev/docs/embed/)\n- [Создание статических бинарных файлов](https://frankenphp.dev/docs/static/)\n- [Компиляция из исходников](https://frankenphp.dev/docs/compile/)\n- [Интеграция с Laravel](https://frankenphp.dev/docs/laravel/)\n- [Известные проблемы](https://frankenphp.dev/docs/known-issues/)\n- [Демо-приложение (Symfony) и бенчмарки](https://github.com/dunglas/frankenphp-demo)\n- [Документация Go-библиотеки](https://pkg.go.dev/github.com/dunglas/frankenphp)\n- [Участие в проекте и отладка](https://frankenphp.dev/docs/contributing/)\n\n## Примеры и шаблоны\n\n- [Symfony](https://github.com/dunglas/symfony-docker)\n- [API Platform](https://api-platform.com/docs/symfony)\n- [Laravel](https://frankenphp.dev/docs/laravel/)\n- [Sulu](https://sulu.io/blog/running-sulu-with-frankenphp)\n- [WordPress](https://github.com/StephenMiracle/frankenwp)\n- [Drupal](https://github.com/dunglas/frankenphp-drupal)\n- [Joomla](https://github.com/alexandreelise/frankenphp-joomla)\n- [TYPO3](https://github.com/ochorocho/franken-typo3)\n"
  },
  {
    "path": "docs/ru/compile.md",
    "content": "# Компиляция из исходников\n\nЭтот документ объясняет, как создать бинарный файл FrankenPHP, который будет загружать PHP как динамическую библиотеку.  \nЭто рекомендуемый способ.\n\nАльтернативно можно создать [статическую сборку](static.md).\n\n## Установка PHP\n\nFrankenPHP совместим с PHP версии 8.2 и выше.\n\nСначала [загрузите исходники PHP](https://www.php.net/downloads.php) и распакуйте их:\n\n```console\ntar xf php-*\ncd php-*/\n```\n\nДалее выполните скрипт `configure` с параметрами, необходимыми для вашей платформы.  \nСледующие флаги `./configure` обязательны, но вы можете добавить и другие, например, для компиляции расширений или дополнительных функций.\n\n### Linux\n\n```console\n./configure \\\n    --enable-embed \\\n    --enable-zts \\\n    --disable-zend-signals \\\n    --enable-zend-max-execution-timers\n```\n\n### Mac\n\nИспользуйте пакетный менеджер [Homebrew](https://brew.sh/) для установки\n`libiconv`, `bison`, `re2c` и `pkg-config`:\n\n```console\nbrew install libiconv bison brotli re2c pkg-config\necho 'export PATH=\"/opt/homebrew/opt/bison/bin:$PATH\"' >> ~/.zshrc\n```\n\nЗатем выполните скрипт configure:\n\n```console\n./configure \\\n    --enable-embed=static \\\n    --enable-zts \\\n    --disable-zend-signals \\\n    --disable-opcache-jit \\\n    --enable-static \\\n    --enable-shared=no \\\n    --with-iconv=/opt/homebrew/opt/libiconv/\n```\n\n## Компиляция PHP\n\nНаконец, скомпилируйте и установите PHP:\n\n```console\nmake -j\"$(getconf _NPROCESSORS_ONLN)\"\nsudo make install\n```\n\n## Установка дополнительных зависимостей\n\nНекоторые функции FrankenPHP зависят от опциональных системных зависимостей.  \nАльтернативно, эти функции можно отключить, передав соответствующие теги сборки компилятору Go.\n\n| Функция                                         | Зависимость                                                           | Тег сборки для отключения |\n| ----------------------------------------------- | --------------------------------------------------------------------- | ------------------------- |\n| Сжатие Brotli                                   | [Brotli](https://github.com/google/brotli)                            | nobrotli                  |\n| Перезапуск worker-скриптов при изменении файлов | [Watcher C](https://github.com/e-dant/watcher/tree/release/watcher-c) | nowatcher                 |\n\n## Компиляция Go-приложения\n\nТеперь можно собрать итоговый бинарный файл:\n\n```console\ncurl -L https://github.com/php/frankenphp/archive/refs/heads/main.tar.gz | tar xz\ncd frankenphp-main/caddy/frankenphp\nCGO_CFLAGS=$(php-config --includes) CGO_LDFLAGS=\"$(php-config --ldflags) $(php-config --libs)\" go build -tags=nobadger,nomysql,nopgx\n```\n\n### Использование xcaddy\n\nАльтернативно, используйте [xcaddy](https://github.com/caddyserver/xcaddy) для компиляции FrankenPHP с [пользовательскими модулями Caddy](https://caddyserver.com/docs/modules/):\n\n```console\nCGO_ENABLED=1 \\\nXCADDY_GO_BUILD_FLAGS=\"-ldflags='-w -s' -tags=nobadger,nomysql,nopgx\" \\\nCGO_CFLAGS=$(php-config --includes) \\\nCGO_LDFLAGS=\"$(php-config --ldflags) $(php-config --libs)\" \\\nxcaddy build \\\n    --output frankenphp \\\n    --with github.com/dunglas/frankenphp/caddy \\\n    --with github.com/dunglas/mercure/caddy \\\n    --with github.com/dunglas/vulcain/caddy\n    # Добавьте дополнительные модули Caddy здесь\n```\n\n> [!TIP]\n>\n> Если вы используете musl libc (по умолчанию в Alpine Linux) и Symfony,  \n> возможно, потребуется увеличить размер стека.  \n> В противном случае вы можете столкнуться с ошибками вроде  \n> `PHP Fatal error: Maximum call stack size of 83360 bytes reached during compilation. Try splitting expression`.\n>\n> Для этого измените значение переменной окружения `XCADDY_GO_BUILD_FLAGS`, например:\n> `XCADDY_GO_BUILD_FLAGS=$'-ldflags \"-w -s -extldflags \\'-Wl,-z,stack-size=0x80000\\'\"'`  \n> (измените значение размера стека в зависимости от требований вашего приложения).\n"
  },
  {
    "path": "docs/ru/config.md",
    "content": "# Конфигурация\n\nFrankenPHP, Caddy, а также модули [Mercure](mercure.md) и [Vulcain](https://vulcain.rocks) могут быть настроены с использованием [форматов, поддерживаемых Caddy](https://caddyserver.com/docs/getting-started#your-first-config).\n\nНаиболее распространенным форматом является `Caddyfile`, который представляет собой простой, удобочитаемый текстовый формат.\nПо умолчанию FrankenPHP будет искать `Caddyfile` в текущей директории.\nВы можете указать пользовательский путь с помощью опции `-c` или `--config`.\n\nНиже показан минимальный `Caddyfile` для обслуживания PHP-приложения:\n\n```caddyfile\n# Хостнейм, на который будет отвечать сервер\nlocalhost\n\n# Опционально, директория для обслуживания файлов, иначе по умолчанию используется текущая директория\n#root public/\nphp_server\n```\n\nБолее продвинутый `Caddyfile`, включающий дополнительные функции и предоставляющий удобные переменные окружения, можно найти [в репозитории FrankenPHP](https://github.com/php/frankenphp/blob/main/caddy/frankenphp/Caddyfile), а также в Docker-образах.\n\nPHP можно настроить [с помощью файла `php.ini`](https://www.php.net/manual/en/configuration.file.php).\n\nВ зависимости от метода установки, FrankenPHP и PHP-интерпретатор будут искать конфигурационные файлы в местах, описанных ниже.\n\n## Docker\n\nFrankenPHP:\n\n- `/etc/frankenphp/Caddyfile`: основной файл конфигурации\n- `/etc/frankenphp/Caddyfile.d/*.caddyfile`: дополнительные файлы конфигурации, которые загружаются автоматически\n\nPHP:\n\n- `php.ini`: `/usr/local/etc/php/php.ini` (по умолчанию `php.ini` не предоставляется)\n- дополнительные файлы конфигурации: `/usr/local/etc/php/conf.d/*.ini`\n- расширения PHP: `/usr/local/lib/php/extensions/no-debug-zts-<YYYYMMDD>/`\n- Вы должны скопировать официальный шаблон, предоставляемый проектом PHP:\n\n```dockerfile\nFROM dunglas/frankenphp\n\n# Production:\nRUN cp $PHP_INI_DIR/php.ini-production $PHP_INI_DIR/php.ini\n\n# Или development:\nRUN cp $PHP_INI_DIR/php.ini-development $PHP_INI_DIR/php.ini\n```\n\n## RPM и Debian пакеты\n\nFrankenPHP:\n\n- `/etc/frankenphp/Caddyfile`: основной файл конфигурации\n- `/etc/frankenphp/Caddyfile.d/*.caddyfile`: дополнительные файлы конфигурации, которые загружаются автоматически\n\nPHP:\n\n- `php.ini`: `/etc/php-zts/php.ini` (по умолчанию предоставляется файл `php.ini` с производственными настройками)\n- дополнительные файлы конфигурации: `/etc/php-zts/conf.d/*.ini`\n\n## Статический бинарный файл\n\nFrankenPHP:\n\n- В текущей рабочей директории: `Caddyfile`\n\nPHP:\n\n- `php.ini`: Директория, в которой выполняется `frankenphp run` или `frankenphp php-server`, затем `/etc/frankenphp/php.ini`\n- дополнительные файлы конфигурации: `/etc/frankenphp/php.d/*.ini`\n- расширения PHP: не могут быть загружены, их следует встраивать в сам бинарный файл\n- скопируйте один из шаблонов `php.ini-production` или `php.ini-development`, предоставленных [в исходниках PHP](https://github.com/php/php-src/).\n\n## Конфигурация Caddyfile\n\n[HTTP-директивы](https://caddyserver.com/docs/caddyfile/concepts#directives) `php_server` или `php` могут быть использованы в блоках сайта для обслуживания вашего PHP-приложения.\n\nМинимальный пример:\n\n```caddyfile\nlocalhost {\n\t# Включить сжатие (опционально)\n\tencode zstd br gzip\n\t# Выполнять PHP-файлы в текущей директории и обслуживать ресурсы\n\tphp_server\n}\n```\n\nВы также можете явно настроить FrankenPHP, используя [глобальную опцию](https://caddyserver.com/docs/caddyfile/concepts#global-options) `frankenphp`:\n\n```caddyfile\n{\n\tfrankenphp {\n\t\tnum_threads <num_threads> # Устанавливает количество запускаемых потоков PHP. По умолчанию: 2x от числа доступных CPU.\n\t\tmax_threads <num_threads> # Ограничивает количество дополнительных потоков PHP, которые могут быть запущены во время выполнения. По умолчанию: num_threads. Может быть установлено в 'auto'.\n\t\tmax_wait_time <duration> # Устанавливает максимальное время, в течение которого запрос может ожидать свободный поток PHP до истечения таймаута. По умолчанию: отключено.\n\t\tmax_idle_time <duration> # Устанавливает максимальное время бездействия автоматически масштабируемого потока до его деактивации. По умолчанию: 5s.\n\t\tphp_ini <key> <value> # Устанавливает директиву php.ini. Может использоваться несколько раз для установки нескольких директив.\n\t\tworker {\n\t\t\tfile <path> # Устанавливает путь к worker-скрипту.\n\t\t\tnum <num> # Устанавливает количество запускаемых потоков PHP, по умолчанию 2x от числа доступных CPU.\n\t\t\tenv <key> <value> # Устанавливает дополнительную переменную окружения с заданным значением. Может быть указано несколько раз для нескольких переменных окружения.\n\t\t\twatch <path> # Устанавливает путь для отслеживания изменений файлов. Может быть указано несколько раз для нескольких путей.\n\t\t\tname <name> # Устанавливает имя worker, используемое в логах и метриках. По умолчанию: абсолютный путь к файлу worker.\n\t\t\tmax_consecutive_failures <num> # Устанавливает максимальное количество последовательных сбоев, после которых worker считается неработоспособным; -1 означает, что worker всегда будет перезапускаться. По умолчанию: 6.\n\t\t}\n\t}\n}\n\n# ...\n```\n\nВ качестве альтернативы можно использовать однострочную краткую форму для опции `worker`:\n\n```caddyfile\n{\n\tfrankenphp {\n\t\tworker <file> <num>\n\t}\n}\n\n# ...\n```\n\nВы также можете определить несколько workers, если обслуживаете несколько приложений на одном сервере:\n\n```caddyfile\napp.example.com {\n    root /path/to/app/public\n\tphp_server {\n\t\troot /path/to/app/public # позволяет лучше кэшировать\n\t\tworker index.php <num>\n\t}\n}\n\nother.example.com {\n    root /path/to/other/public\n\tphp_server {\n\t\troot /path/to/other/public\n\t\tworker index.php <num>\n\t}\n}\n\n# ...\n```\n\nИспользование директивы `php_server` — это то, что нужно в большинстве случаев,\nно если требуется полный контроль, вы можете использовать более низкоуровневую директиву `php`.\nДиректива `php` передает все входные данные PHP, не проверяя предварительно,\nявляется ли это PHP-файлом или нет. Подробнее об этом читайте на [странице производительности](performance.md#try_files).\n\nИспользование директивы `php_server` эквивалентно следующей конфигурации:\n\n```caddyfile\nroute {\n\t# Добавить слэш в конец запросов к директориям\n\t@canonicalPath {\n\t\tfile {path}/index.php\n\t\tnot path */\n\t}\n\tredir @canonicalPath {path}/ 308\n\t# Если запрошенный файл не существует, попытаться использовать файлы index\n\t@indexFiles file {\n\t\ttry_files {path} {path}/index.php index.php\n\t\tsplit_path .php\n\t}\n\trewrite @indexFiles {http.matchers.file.relative}\n\t# FrankenPHP!\n\t@phpFiles path *.php\n\tphp @phpFiles\n\tfile_server\n}\n```\n\nДирективы `php_server` и `php` имеют следующие опции:\n\n```caddyfile\nphp_server [<matcher>] {\n\troot <directory> # Устанавливает корневую папку для сайта. По умолчанию: директива `root`.\n\tsplit_path <delim...> # Устанавливает подстроки для разделения URI на две части. Первая совпадающая подстрока будет использована для разделения \"path info\" от пути. Первая часть будет дополнена совпадающей подстрокой и будет считаться фактическим именем ресурса (CGI-скрипта). Вторая часть будет установлена как PATH_INFO для использования скриптом. По умолчанию: `.php`.\n\tresolve_root_symlink false # Отключает разрешение директории `root` до ее фактического значения путем оценки символической ссылки, если таковая существует (включено по умолчанию).\n\tenv <key> <value> # Устанавливает дополнительную переменную окружения с заданным значением. Может быть указано несколько раз для нескольких переменных окружения.\n\tfile_server off # Отключает встроенную директиву file_server.\n\tworker { # Создает worker, специфичный для этого сервера. Может быть указано несколько раз для нескольких workers.\n\t\tfile <path> # Устанавливает путь к worker-скрипту, может быть относительным к корню php_server\n\t\tnum <num> # Устанавливает количество запускаемых потоков PHP, по умолчанию 2x от числа доступных CPU.\n\t\tname <name> # Устанавливает имя для worker, используемое в логах и метриках. По умолчанию: абсолютный путь к файлу worker. Всегда начинается с m# при определении в блоке php_server.\n\t\twatch <path> # Устанавливает путь для отслеживания изменений файлов. Может быть указано несколько раз для нескольких путей.\n\t\tenv <key> <value> # Устанавливает дополнительную переменную окружения с заданным значением. Может быть указано несколько раз для нескольких переменных окружения. Переменные окружения для этого worker также наследуются от родительского php_server, но могут быть переопределены здесь.\n\t\tmatch <path> # сопоставляет worker с шаблоном пути. Переопределяет try_files и может использоваться только в директиве php_server.\n\t}\n\tworker <other_file> <num> # Также можно использовать краткую форму, как и в глобальном блоке frankenphp.\n}\n```\n\n### Отслеживание изменений файлов\n\nПоскольку workers запускают ваше приложение только один раз и держат его в памяти, любые изменения\nв ваших PHP-файлах не будут отражены немедленно.\n\nВместо этого workers могут быть перезапущены при изменении файлов с помощью директивы `watch`.\nЭто полезно для сред разработки.\n\n```caddyfile\n{\n\tfrankenphp {\n\t\tworker {\n\t\t\tfile  /path/to/app/public/worker.php\n\t\t\twatch\n\t\t}\n\t}\n}\n```\n\nЭта функция часто используется в сочетании с [горячей перезагрузкой](hot-reload.md).\n\nЕсли директория для `watch` не указана, по умолчанию будет использоваться `./**/*.{env,php,twig,yaml,yml}`,\nкоторый отслеживает все файлы с расширениями `.env`, `.php`, `.twig`, `.yaml` и `.yml` в директории и поддиректориях,\nгде был запущен процесс FrankenPHP. Вы также можете указать одну или несколько директорий с использованием\n[шаблона имён файлов оболочки](https://pkg.go.dev/path/filepath#Match):\n\n```caddyfile\n{\n\tfrankenphp {\n\t\tworker {\n\t\t\tfile  /path/to/app/public/worker.php\n\t\t\twatch /path/to/app # отслеживает все файлы во всех поддиректориях /path/to/app\n\t\t\twatch /path/to/app/*.php # отслеживает файлы с расширением .php в /path/to/app\n\t\t\twatch /path/to/app/**/*.php # отслеживает PHP-файлы в /path/to/app и поддиректориях\n\t\t\twatch /path/to/app/**/*.{php,twig} # отслеживает PHP и Twig-файлы в /path/to/app и поддиректориях\n\t\t}\n\t}\n}\n```\n\n- Шаблон `**` указывает на рекурсивное отслеживание.\n- Директории также могут быть относительными (к месту запуска процесса FrankenPHP).\n- Если у вас определено несколько workers, все они будут перезапущены при изменении файлов.\n- Будьте осторожны с отслеживанием файлов, создаваемых во время выполнения (например, логов), так как это может вызвать нежелательные перезапуски workers.\n\nМеханизм отслеживания файлов основан на [e-dant/watcher](https://github.com/e-dant/watcher).\n\n## Сопоставление Worker с путем\n\nВ традиционных PHP-приложениях скрипты всегда размещаются в публичной директории.\nЭто также верно для worker-скриптов, которые обрабатываются как любые другие PHP-скрипты.\nЕсли вы хотите разместить worker-скрипт вне публичной директории, вы можете сделать это с помощью директивы `match`.\n\nДиректива `match` является оптимизированной альтернативой `try_files`, доступной только внутри `php_server` и `php`.\nСледующий пример всегда будет обслуживать файл в публичной директории, если он присутствует,\nи в противном случае будет перенаправлять запрос worker, соответствующему шаблону пути.\n\n```caddyfile\n{\n\tfrankenphp {\n\t\tphp_server {\n\t\t\tworker {\n\t\t\t\tfile /path/to/worker.php # файл может находиться вне публичного пути\n\t\t\t\tmatch /api/* # все запросы, начинающиеся с /api/, будут обрабатываться этим worker\n\t\t\t}\n\t\t}\n\t}\n}\n```\n\n## Переменные окружения\n\nСледующие переменные окружения могут быть использованы для внедрения директив Caddy в `Caddyfile` без его изменения:\n\n- `SERVER_NAME`: изменение [адресов для прослушивания](https://caddyserver.com/docs/caddyfile/concepts#addresses); предоставленные хостнеймы также будут использованы для генерации TLS-сертификата\n- `SERVER_ROOT`: изменение корневой директории сайта, по умолчанию `public/`\n- `CADDY_GLOBAL_OPTIONS`: внедрение [глобальных опций](https://caddyserver.com/docs/caddyfile/options)\n- `FRANKENPHP_CONFIG`: внедрение конфигурации под директиву `frankenphp`\n\nКак и для FPM и CLI SAPIs, переменные окружения по умолчанию доступны в суперглобальной переменной `$_SERVER`.\n\nЗначение `S` в [директиве PHP `variables_order`](https://www.php.net/manual/en/ini.core.php#ini.variables-order) всегда эквивалентно `ES`, независимо от того, где расположена `E` в этой директиве.\n\n## Конфигурация PHP\n\nДля загрузки [дополнительных конфигурационных файлов PHP](https://www.php.net/manual/en/configuration.file.php#configuration.file.scan) можно использовать переменную окружения `PHP_INI_SCAN_DIR`.\nЕсли она установлена, PHP загрузит все файлы с расширением `.ini`, находящиеся в указанных директориях.\n\nВы также можете изменить конфигурацию PHP с помощью директивы `php_ini` в `Caddyfile`:\n\n```caddyfile\n{\n    frankenphp {\n        php_ini memory_limit 256M\n\n        # или\n\n        php_ini {\n            memory_limit 256M\n            max_execution_time 15\n        }\n    }\n}\n```\n\n### Отключение HTTPS\n\nПо умолчанию FrankenPHP автоматически включает HTTPS для всех хостнеймов, включая `localhost`.\nЕсли вы хотите отключить HTTPS (например, в среде разработки), вы можете установить переменную окружения `SERVER_NAME` в `http://` или `:80`:\n\nВ качестве альтернативы вы можете использовать все другие методы, описанные в [документации Caddy](https://caddyserver.com/docs/automatic-https#activation).\n\nЕсли вы хотите использовать HTTPS с IP-адресом `127.0.0.1` вместо хостнейма `localhost`, пожалуйста, ознакомьтесь с разделом [известные проблемы](known-issues.md#using-https127001-with-docker).\n\n### Полный дуплекс (HTTP/1)\n\nПри использовании HTTP/1.x может быть желательно включить режим полного дуплекса, чтобы разрешить запись ответа до завершения чтения всего тела запроса. (например: [Mercure](mercure.md), WebSocket, Server-Sent Events и т.д.)\n\nЭто конфигурация, которую необходимо добавить в глобальные опции в `Caddyfile`:\n\n```caddyfile\n{\n  servers {\n    enable_full_duplex\n  }\n}\n```\n\n> [!CAUTION]\n>\n> Включение этой опции может привести к зависанию устаревших HTTP/1.x клиентов, которые не поддерживают полный дуплекс.\n> Это также можно настроить с помощью переменной окружения `CADDY_GLOBAL_OPTIONS`:\n\n```sh\nCADDY_GLOBAL_OPTIONS=\"servers {\n  enable_full_duplex\n}\"\n```\n\nДополнительную информацию об этой настройке можно найти в [документации Caddy](https://caddyserver.com/docs/caddyfile/options#enable-full-duplex).\n\n## Включение режима отладки\n\nПри использовании Docker-образа установите переменную окружения `CADDY_GLOBAL_OPTIONS` в `debug`, чтобы включить режим отладки:\n\n```console\ndocker run -v $PWD:/app/public \\\n    -e CADDY_GLOBAL_OPTIONS=debug \\\n    -p 80:80 -p 443:443 -p 443:443/udp \\\n    dunglas/frankenphp\n```\n\n## Автодополнение команд оболочки\n\nFrankenPHP предоставляет встроенную поддержку автодополнения команд для Bash, Zsh, Fish и PowerShell. Это позволяет автоматически завершать все команды (включая пользовательские команды, такие как `php-server`, `php-cli` и `extension-init`) и их флаги.\n\n### Bash\n\nДля загрузки автодополнения в текущую сессию оболочки:\n\n```console\nsource <(frankenphp completion bash)\n```\n\nДля загрузки автодополнения для каждой новой сессии выполните:\n\n**Linux:**\n\n```console\nfrankenphp completion bash > /usr/share/bash-completion/completions/frankenphp\n```\n\n**macOS:**\n\n```console\nfrankenphp completion bash > $(brew --prefix)/share/bash-completion/completions/frankenphp\n```\n\n### Zsh\n\nЕсли автодополнение команд еще не включено в вашей среде, вам нужно будет его активировать. Вы можете выполнить следующее один раз:\n\n```console\necho \"autoload -U compinit; compinit\" >> ~/.zshrc\n```\n\nЧтобы загрузить автодополнение для каждой сессии, выполните один раз:\n\n```console\nfrankenphp completion zsh > \"${fpath[1]}/_frankenphp\"\n```\n\nВам потребуется запустить новую оболочку, чтобы эти настройки вступили в силу.\n\n### Fish\n\nДля загрузки автодополнения в текущую сессию оболочки:\n\n```console\nfrankenphp completion fish | source\n```\n\nДля загрузки автодополнения для каждой новой сессии выполните один раз:\n\n```console\nfrankenphp completion fish > ~/.config/fish/completions/frankenphp.fish\n```\n\n### PowerShell\n\nДля загрузки автодополнения в текущую сессию оболочки:\n\n```powershell\nfrankenphp completion powershell | Out-String | Invoke-Expression\n```\n\nДля загрузки автодополнения для каждой новой сессии выполните один раз:\n\n```powershell\nfrankenphp completion powershell | Out-File -FilePath (Join-Path (Split-Path $PROFILE) \"frankenphp.ps1\")\nAdd-Content -Path $PROFILE -Value '. (Join-Path (Split-Path $PROFILE) \"frankenphp.ps1\")'\n```\n\nВам потребуется запустить новую оболочку, чтобы эти настройки вступили в силу.\n\nВам потребуется запустить новую оболочку, чтобы эти настройки вступили в силу.\n"
  },
  {
    "path": "docs/ru/docker.md",
    "content": "# Создание кастомных Docker-образов\n\n[Docker-образы FrankenPHP](https://hub.docker.com/r/dunglas/frankenphp) основаны на [официальных PHP-образах](https://hub.docker.com/_/php/).\nДоступны варианты для Debian и Alpine Linux для популярных архитектур.\nРекомендуется использовать Debian-варианты.\n\nДоступны версии для PHP 8.2, 8.3, 8.4 и 8.5.\n\nТеги следуют следующему шаблону: `dunglas/frankenphp:<frankenphp-version>-php<php-version>-<os>`.\n\n- `<frankenphp-version>` и `<php-version>` — версии FrankenPHP и PHP соответственно, начиная с мажорных (например, `1`), минорных (например, `1.2`) и заканчивая патч-версиями (например, `1.2.3`).\n- `<os>` может быть `trixie` (для Debian Trixie), `bookworm` (для Debian Bookworm) или `alpine` (для последней стабильной версии Alpine).\n\n[Просмотреть доступные теги](https://hub.docker.com/r/dunglas/frankenphp/tags).\n\n## Как использовать образы\n\nСоздайте `Dockerfile` в вашем проекте:\n\n```dockerfile\nFROM dunglas/frankenphp\n\nCOPY . /app/public\n```\n\nЗатем выполните следующие команды для сборки и запуска Docker-образа:\n\n```console\ndocker build -t my-php-app .\ndocker run -it --rm --name my-running-app my-php-app\n```\n\n## Как настроить конфигурацию\n\nДля удобства в образе предоставлен [Caddyfile по умолчанию](https://github.com/php/frankenphp/blob/main/caddy/frankenphp/Caddyfile), содержащий полезные переменные окружения.\n\n## Как установить дополнительные PHP-расширения\n\nСкрипт [`docker-php-extension-installer`](https://github.com/mlocati/docker-php-extension-installer) предоставляется в базовом образе. Установка дополнительных PHP-расширений осуществляется просто:\n\n```dockerfile\nFROM dunglas/frankenphp\n\n# Добавьте дополнительные расширения здесь:\nRUN install-php-extensions \\\n\tpdo_mysql \\\n\tgd \\\n\tintl \\\n\tzip \\\n\topcache\n```\n\n## Как установить дополнительные модули Caddy\n\nFrankenPHP построен на базе Caddy, и все [модули Caddy](https://caddyserver.com/docs/modules/) можно использовать с FrankenPHP.\n\nСамый простой способ установить пользовательские модули Caddy — использовать [xcaddy](https://github.com/caddyserver/xcaddy):\n\n```dockerfile\nFROM dunglas/frankenphp:builder AS builder\n\n# Копируем xcaddy в образ сборки\nCOPY --from=caddy:builder /usr/bin/xcaddy /usr/bin/xcaddy\n\n# Для сборки FrankenPHP необходимо включить CGO\nRUN CGO_ENABLED=1 \\\n    XCADDY_SETCAP=1 \\\n    XCADDY_GO_BUILD_FLAGS=\"-ldflags='-w -s' -tags=nobadger,nomysql,nopgx\" \\\n    CGO_CFLAGS=$(php-config --includes) \\\n    CGO_LDFLAGS=\"$(php-config --ldflags) $(php-config --libs)\" \\\n    xcaddy build \\\n        --output /usr/local/bin/frankenphp \\\n        --with github.com/dunglas/frankenphp=./ \\\n        --with github.com/dunglas/frankenphp/caddy=./caddy/ \\\n        --with github.com/dunglas/caddy-cbrotli \\\n        # Mercure и Vulcain включены в официальный билд, но вы можете их удалить\n        --with github.com/dunglas/mercure/caddy \\\n        --with github.com/dunglas/vulcain/caddy\n        # Добавьте дополнительные модули Caddy здесь\n\nFROM dunglas/frankenphp AS runner\n\n# Заменяем официальный бинарный файл на пользовательский с добавленными модулями\nCOPY --from=builder /usr/local/bin/frankenphp /usr/local/bin/frankenphp\n```\n\nОбраз `builder`, предоставляемый FrankenPHP, содержит скомпилированную версию `libphp`.\n[Образы builder](https://hub.docker.com/r/dunglas/frankenphp/tags?name=builder) доступны для всех версий FrankenPHP и PHP, как для Debian, так и для Alpine.\n\n> [!TIP]\n>\n> Если вы используете Alpine Linux и Symfony,\n> возможно, потребуется [увеличить размер стека](compile.md#использование-xcaddy).\n\n## Активировать worker режим по умолчанию\n\nУстановите переменную окружения `FRANKENPHP_CONFIG`, чтобы запускать FrankenPHP с рабочим скриптом:\n\n```dockerfile\nFROM dunglas/frankenphp\n\n# ...\n\nENV FRANKENPHP_CONFIG=\"worker ./public/index.php\"\n```\n\n## Использование тома в разработке\n\nДля удобной разработки с FrankenPHP смонтируйте директорию с исходным кодом приложения на хосте как том в Docker-контейнере:\n\n```console\ndocker run -v $PWD:/app/public -p 80:80 -p 443:443 -p 443:443/udp --tty my-php-app\n```\n\n> [!TIP]\n>\n> Опция `--tty` позволяет получать удобочитаемые логи вместо JSON-формата.\n\nС использованием Docker Compose:\n\n```yaml\n# compose.yaml\n\nservices:\n  php:\n    image: dunglas/frankenphp\n    # раскомментируйте следующую строку, если хотите использовать собственный Dockerfile\n    #build: .\n    # раскомментируйте следующую строку для работы в production-окружении\n    # restart: always\n    ports:\n      - \"80:80\" # HTTP\n      - \"443:443\" # HTTPS\n      - \"443:443/udp\" # HTTP/3\n    volumes:\n      - ./:/app/public\n      - caddy_data:/data\n      - caddy_config:/config\n    # закомментируйте следующую строку в production — в dev она обеспечивает удобочитаемые логи\n    tty: true\n\n# Тома, необходимые для сертификатов и конфигурации Caddy\nvolumes:\n  caddy_data:\n  caddy_config:\n```\n\n## Запуск под обычным пользователем\n\nFrankenPHP может запускаться под обычным пользователем в Docker.\n\nПример `Dockerfile` для этого:\n\n```dockerfile\nFROM dunglas/frankenphp\n\nARG USER=appuser\n\nRUN \\\n\t# Для дистрибутивов на основе Alpine используйте \"adduser -D ${USER}\"\n\tuseradd ${USER}; \\\n\t# Добавить возможность привязки к портам 80 и 443\n\tsetcap CAP_NET_BIND_SERVICE=+eip /usr/local/bin/frankenphp; \\\n\t# Предоставить доступ на запись в /config/caddy и /data/caddy\n\tchown -R ${USER}:${USER} /config/caddy /data/caddy\n\nUSER ${USER}\n```\n\n### Запуск без дополнительных прав\n\nДаже при запуске без root-прав, FrankenPHP требуется возможность `CAP_NET_BIND_SERVICE` для привязки веб-сервера к зарезервированным портам (80 и 443).\n\nЕсли вы открываете доступ к FrankenPHP на непривилегированном порту (1024 и выше), можно запустить веб-сервер от имени обычного пользователя без необходимости предоставления дополнительных возможностей:\n\n```dockerfile\nFROM dunglas/frankenphp\n\nARG USER=appuser\n\nRUN \\\n\t# Для дистрибутивов на основе Alpine используйте \"adduser -D ${USER}\"\n\tuseradd ${USER}; \\\n\t# Удалить стандартные возможности\n\tsetcap -r /usr/local/bin/frankenphp; \\\n\t# Предоставить доступ на запись в /config/caddy и /data/caddy\n\tchown -R ${USER}:${USER} /config/caddy /data/caddy\n\nUSER ${USER}\n```\n\nЗатем установите переменную окружения `SERVER_NAME`, чтобы использовать непривилегированный порт.\nПример: `:8000`.\n\n## Обновления\n\nDocker-образы собираются:\n\n- при выпуске новой версии;\n- ежедневно в 4 утра UTC, если доступны новые версии официальных PHP-образов.\n\n## Укрепление образов\n\nЧтобы ещё больше уменьшить поверхность атаки и размер ваших Docker-образов FrankenPHP, также возможно создавать их на основе [Google Distroless](https://github.com/GoogleContainerTools/distroless) или [Docker Hardened](https://www.docker.com/products/hardened-images) образов.\n\n> [!WARNING]\n> Эти минимальные базовые образы не включают оболочку или менеджер пакетов, что значительно усложняет отладку.\n> Поэтому они рекомендуются только для продакшена, если безопасность является высоким приоритетом.\n\nПри добавлении дополнительных PHP-расширений вам потребуется промежуточная стадия сборки:\n\n```dockerfile\nFROM dunglas/frankenphp AS builder\n\n# Добавьте дополнительные PHP-расширения здесь\nRUN install-php-extensions pdo_mysql pdo_pgsql #...\n\n# Скопировать общие библиотеки frankenphp и всех установленных расширений во временное место\n# Этот шаг можно также выполнить вручную, проанализировав вывод ldd для бинарного файла frankenphp и каждого файла расширения .so\nRUN apt-get update && apt-get install -y libtree && \\\n    EXT_DIR=\"$(php -r 'echo ini_get(\"extension_dir\");')\" && \\\n    FRANKENPHP_BIN=\"$(which frankenphp)\"; \\\n    LIBS_TMP_DIR=\"/tmp/libs\"; \\\n    mkdir -p \"$LIBS_TMP_DIR\"; \\\n    for target in \"$FRANKENPHP_BIN\" $(find \"$EXT_DIR\" -maxdepth 2 -type f -name \"*.so\"); do \\\n        libtree -pv \"$target\" | sed 's/.*── \\(.*\\) \\[.*/\\1/' | grep -v \"^$target\" | while IFS= read -r lib; do \\\n            [ -z \"$lib\" ] && continue; \\\n            base=$(basename \"$lib\"); \\\n            destfile=\"$LIBS_TMP_DIR/$base\"; \\\n            if [ ! -f \"$destfile\" ]; then \\\n                cp \"$lib\" \"$destfile\"; \\\n            fi; \\\n        done; \\\n    done\n\n\n# Базовый образ Distroless Debian — убедитесь, что версия Debian совпадает с базовым образом\nFROM gcr.io/distroless/base-debian13\n# Альтернатива: Docker Hardened Image\n# FROM dhi.io/debian:13\n\n# Путь к приложению и Caddyfile для копирования в контейнер\nARG PATH_TO_APP=\".\"\nARG PATH_TO_CADDYFILE=\"./Caddyfile\"\n\n# Скопировать приложение в /app\n# Для дополнительного усиления убедитесь, что только записываемые пути принадлежат пользователю nonroot\nCOPY --chown=nonroot:nonroot \"$PATH_TO_APP\" /app\nCOPY \"$PATH_TO_CADDYFILE\" /etc/caddy/Caddyfile\n\n# Скопировать frankenphp и необходимые библиотеки\nCOPY --from=builder /usr/local/bin/frankenphp /usr/local/bin/frankenphp\nCOPY --from=builder /usr/local/lib/php/extensions /usr/local/lib/php/extensions\nCOPY --from=builder /tmp/libs /usr/lib\n\n# Скопировать конфигурационные файлы php.ini\nCOPY --from=builder /usr/local/etc/php/conf.d /usr/local/etc/php/conf.d\nCOPY --from=builder /usr/local/etc/php/php.ini-production /usr/local/etc/php/php.ini\n\n# Директории данных Caddy — должны быть доступны для записи пользователю nonroot даже в файловой системе только для чтения\nENV XDG_CONFIG_HOME=/config \\\n    XDG_DATA_HOME=/data\nCOPY --from=builder --chown=nonroot:nonroot /data/caddy /data/caddy\nCOPY --from=builder --chown=nonroot:nonroot /config/caddy /config/caddy\n\nUSER nonroot\n\nWORKDIR /app\n\n# Точка входа для запуска frankenphp с указанным Caddyfile\nENTRYPOINT [\"/usr/local/bin/frankenphp\", \"run\", \"-c\", \"/etc/caddy/Caddyfile\"]\n```\n\n## Версии для разработки\n\nВерсии для разработки доступны в Docker-репозитории [`dunglas/frankenphp-dev`](https://hub.docker.com/repository/docker/dunglas/frankenphp-dev).\nСборка запускается автоматически при каждом коммите в основную ветку GitHub-репозитория.\n\nТеги `latest*` указывают на голову ветки `main`.\nТакже доступны теги в формате `sha-<git-commit-hash>`.\n"
  },
  {
    "path": "docs/ru/early-hints.md",
    "content": "# Early Hints\n\nFrankenPHP изначально поддерживает [Early Hints (103 HTTP статус код)](https://developer.chrome.com/blog/early-hints/).  \nИспользование Early Hints может улучшить время загрузки ваших веб-страниц на 30%.\n\n```php\n<?php\n\nheader('Link: </style.css>; rel=preload; as=style');\nheaders_send(103);\n\n// ваши медленные алгоритмы и SQL-запросы 🤪\n\necho <<<'HTML'\n<!DOCTYPE html>\n<title>Hello FrankenPHP</title>\n<link rel=\"stylesheet\" href=\"style.css\">\nHTML;\n```\n\nEarly Hints поддерживается как в обычном, так и в [worker режиме](worker.md).\n"
  },
  {
    "path": "docs/ru/embed.md",
    "content": "# PHP-приложения как автономные бинарные файлы\n\nFrankenPHP позволяет встраивать исходный код и ресурсы PHP-приложений в статический автономный бинарный файл.\n\nБлагодаря этой функции PHP-приложения могут распространяться как автономные бинарные файлы, которые содержат само приложение, интерпретатор PHP и Caddy — веб-сервер уровня продакшн.\n\nПодробнее об этой функции [в презентации Кевина на SymfonyCon 2023](https://dunglas.dev/2023/12/php-and-symfony-apps-as-standalone-binaries/).\n\nДля встраивания Laravel-приложений ознакомьтесь с [документацией](laravel.md#laravel-приложения-как-автономные-бинарные-файлы).\n\n## Подготовка приложения\n\nПеред созданием автономного бинарного файла убедитесь, что ваше приложение готово для встраивания.\n\nНапример, вам может понадобиться:\n\n- Установить продакшн-зависимости приложения.\n- Сгенерировать автозагрузчик.\n- Включить продакшн-режим приложения (если он есть).\n- Удалить ненужные файлы, такие как `.git` или тесты, чтобы уменьшить размер итогового бинарного файла.\n\nДля приложения на Symfony это может выглядеть так:\n\n```console\n# Экспорт проекта, чтобы избавиться от .git/ и других ненужных файлов\nmkdir $TMPDIR/my-prepared-app\ngit archive HEAD | tar -x -C $TMPDIR/my-prepared-app\ncd $TMPDIR/my-prepared-app\n\n# Установить соответствующие переменные окружения\necho APP_ENV=prod > .env.local\necho APP_DEBUG=0 >> .env.local\n\n# Удалить тесты и другие ненужные файлы\nrm -Rf tests/\n\n# Установить зависимости\ncomposer install --ignore-platform-reqs --no-dev -a\n\n# Оптимизировать .env\ncomposer dump-env prod\n```\n\n### Настройка конфигурации\n\nЧтобы настроить [конфигурацию](config.md), вы можете разместить файлы `Caddyfile` и `php.ini` в основной директории приложения (`$TMPDIR/my-prepared-app` в примере выше).\n\n## Создание бинарного файла для Linux\n\nСамый простой способ создать бинарный файл для Linux — использовать предоставленный Docker-билдер.\n\n1. Создайте файл `static-build.Dockerfile` в репозитории вашего приложения:\n\n   ```dockerfile\n   FROM --platform=linux/amd64 dunglas/frankenphp:static-builder-gnu\n   # Если вы планируете запускать бинарный файл на системах с musl-libc, используйте static-builder-musl\n\n   # Скопировать приложение\n   WORKDIR /go/src/app/dist/app\n   COPY . .\n\n   # Сборка статического бинарного файла\n   WORKDIR /go/src/app/\n   RUN EMBED=dist/app/ ./build-static.sh\n   ```\n\n   > [!CAUTION]\n   >\n   > Некоторые `.dockerignore` файлы (например, [Symfony Docker `.dockerignore`](https://github.com/dunglas/symfony-docker/blob/main/.dockerignore))  \n   > игнорируют директорию `vendor/` и файлы `.env`. Перед сборкой убедитесь, что `.dockerignore` файл настроен корректно или удалён.\n\n2. Соберите образ:\n\n   ```console\n   docker build -t static-app -f static-build.Dockerfile .\n   ```\n\n3. Извлеките бинарный файл:\n\n   ```console\n   docker cp $(docker create --name static-app-tmp static-app):/go/src/app/dist/frankenphp-linux-x86_64 my-app ; docker rm static-app-tmp\n   ```\n\nСозданный бинарный файл сохранится в текущей директории под именем `my-app`.\n\n## Создание бинарного файла для других ОС\n\nЕсли вы не хотите использовать Docker или хотите собрать бинарный файл для macOS, используйте предоставленный скрипт:\n\n```console\ngit clone https://github.com/php/frankenphp\ncd frankenphp\nEMBED=/path/to/your/app ./build-static.sh\n```\n\nИтоговый бинарный файл будет находиться в директории `dist/` под именем `frankenphp-<os>-<arch>`.\n\n## Использование бинарного файла\n\nГотово! Файл `my-app` (или `dist/frankenphp-<os>-<arch>` для других ОС) содержит ваше автономное приложение.\n\nДля запуска веб-приложения выполните:\n\n```console\n./my-app php-server\n```\n\nЕсли ваше приложение содержит [worker-скрипт](worker.md), запустите его следующим образом:\n\n```console\n./my-app php-server --worker public/index.php\n```\n\nЧтобы включить HTTPS (Let's Encrypt автоматически создаст сертификат), HTTP/2 и HTTP/3, укажите доменное имя:\n\n```console\n./my-app php-server --domain localhost\n```\n\nВы также можете запускать PHP-скрипты CLI, встроенные в бинарный файл:\n\n```console\n./my-app php-cli bin/console\n```\n\n## PHP-расширения\n\nПо умолчанию скрипт собирает расширения, указанные в `composer.json` вашего проекта.  \nЕсли файла `composer.json` нет, собираются стандартные расширения, как указано в [документации по статической сборке](static.md).\n\nЧтобы настроить список расширений, используйте переменную окружения `PHP_EXTENSIONS`.\n\n## Настройка сборки\n\n[Ознакомьтесь с документацией по статической сборке](static.md), чтобы узнать, как настроить бинарный файл (расширения, версию PHP и т.д.).\n\n## Распространение бинарного файла\n\nНа Linux созданный бинарный файл сжимается с помощью [UPX](https://upx.github.io).\n\nНа Mac для уменьшения размера файла перед отправкой его можно сжать. Рекомендуется использовать `xz`.\n"
  },
  {
    "path": "docs/ru/extension-workers.md",
    "content": "# Расширение Workers\n\nРасширение Workers позволяет вашему [расширению FrankenPHP](https://frankenphp.dev/docs/extensions/) управлять выделенным пулом PHP-потоков для выполнения фоновых задач, обработки асинхронных событий или реализации пользовательских протоколов. Полезно для систем очередей, слушателей событий, планировщиков и т. д.\n\n## Регистрация Worker-а\n\n### Статическая регистрация\n\nЕсли вам не нужно делать Worker-а настраиваемым пользователем (фиксированный путь к скрипту, фиксированное количество потоков), вы можете просто зарегистрировать Worker в функции `init()`.\n\n```go\npackage myextension\n\nimport (\n\t\"github.com/dunglas/frankenphp\"\n\t\"github.com/dunglas/frankenphp/caddy\"\n)\n\n// Глобальный дескриптор для связи с пулом Worker-ов\nvar worker frankenphp.Workers\n\nfunc init() {\n\t// Зарегистрировать Worker при загрузке модуля.\n\tworker = caddy.RegisterWorkers(\n\t\t\"my-internal-worker\", // Уникальное имя\n\t\t\"worker.php\",         // Путь к скрипту (относительный или абсолютный)\n\t\t2,                    // Фиксированное количество потоков\n\t\t// Дополнительные хуки жизненного цикла\n\t\tfrankenphp.WithWorkerOnServerStartup(func() {\n\t\t\t// Глобальная логика настройки...\n\t\t}),\n\t)\n}\n```\n\n### В модуле Caddy (настраивается пользователем)\n\nЕсли вы планируете делиться своим расширением (например, универсальной очередью или слушателем событий), вам следует обернуть его в модуль Caddy. Это позволит пользователям настраивать путь к скрипту и количество потоков через свой `Caddyfile`. Это требует реализации интерфейса `caddy.Provisioner` и парсинга Caddyfile ([см. пример](https://github.com/dunglas/frankenphp-queue/blob/989120d394d66dd6c8e2101cac73dd622fade334/caddy.go)).\n\n### В чистом Go-приложении (встраивание)\n\nЕсли вы [встраиваете FrankenPHP в стандартное Go-приложение без Caddy](https://pkg.go.dev/github.com/dunglas/frankenphp#example-ServeHTTP), вы можете зарегистрировать Worker-ы расширения, используя `frankenphp.WithExtensionWorkers` при инициализации опций.\n\n## Взаимодействие с Worker-ами\n\nКак только пул Worker-ов активен, вы можете отправлять ему задачи. Это можно сделать внутри [нативных функций, экспортированных в PHP](https://frankenphp.dev/docs/extensions/#writing-the-extension), или из любой Go-логики, такой как планировщик cron, слушатель событий (MQTT, Kafka) или любая другая горутина.\n\n### Безголовый режим: `SendMessage`\n\nИспользуйте `SendMessage` для прямой передачи необработанных данных вашему скрипту Worker-а. Это идеально подходит для очередей или простых команд.\n\n#### Пример: Расширение асинхронной очереди\n\n```go\n// #include <Zend/zend_types.h>\nimport \"C\"\nimport (\n\t\"context\"\n\t\"unsafe\"\n\t\"github.com/dunglas/frankenphp\"\n)\n\n//export_php:function my_queue_push(mixed $data): bool\nfunc my_queue_push(data *C.zval) bool {\n\t// 1. Убедитесь, что Worker готов\n\tif worker == nil {\n\t\treturn false\n\t}\n\n\t// 2. Отправить фоновому Worker-у\n\t_, err := worker.SendMessage(\n\t\tcontext.Background(), // Стандартный Go-контекст\n\t\tunsafe.Pointer(data), // Данные для передачи Worker-у\n\t\tnil, // Опциональный http.ResponseWriter\n\t)\n\n\treturn err == nil\n}\n```\n\n### Эмуляция HTTP: `SendRequest`\n\nИспользуйте `SendRequest`, если ваше расширение должно вызвать PHP-скрипт, который ожидает стандартную веб-среду (заполнение `$_SERVER`, `$_GET` и т. д.).\n\n```go\n// #include <Zend/zend_types.h>\nimport \"C\"\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"unsafe\"\n\t\"github.com/dunglas/frankenphp\"\n)\n\n//export_php:function my_worker_http_request(string $path): string\nfunc my_worker_http_request(path *C.zend_string) unsafe.Pointer {\n\t// 1. Подготовьте запрос и рекордер\n\turl := frankenphp.GoString(unsafe.Pointer(path))\n\treq, _ := http.NewRequest(\"GET\", url, http.NoBody)\n\trr := httptest.NewRecorder()\n\n\t// 2. Отправить Worker-у\n\tif err := worker.SendRequest(rr, req); err != nil {\n\t\treturn nil\n\t}\n\n\t// 3. Вернуть захваченный ответ\n\treturn frankenphp.PHPString(rr.Body.String(), false)\n}\n```\n\n## Скрипт Worker-а\n\nPHP-скрипт Worker-а выполняется в цикле и может обрабатывать как необработанные сообщения, так и HTTP-запросы.\n\n```php\n<?php\n// Обрабатывать как необработанные сообщения, так и HTTP-запросы в одном цикле\n$handler = function ($payload = null) {\n    // Случай 1: Режим сообщений\n    if ($payload !== null) {\n        return \"Received payload: \" . $payload;\n    }\n\n    // Случай 2: Режим HTTP (заполняются стандартные суперглобальные переменные PHP)\n    echo \"Hello from page: \" . $_SERVER['REQUEST_URI'];\n};\n\nwhile (frankenphp_handle_request($handler)) {\n    gc_collect_cycles();\n}\n```\n\n## Хуки жизненного цикла\n\nFrankenPHP предоставляет хуки для выполнения Go-кода в определенные моменты жизненного цикла.\n\n| Тип хука  | Имя опции                    | Подпись              | Контекст и вариант использования                                      |\n| :-------- | :--------------------------- | :------------------- | :-------------------------------------------------------------------- |\n| **Сервер** | `WithWorkerOnServerStartup`  | `func()`             | Глобальная настройка. Выполняется **один раз**. Пример: Подключение к NATS/Redis. |\n| **Сервер** | `WithWorkerOnServerShutdown` | `func()`             | Глобальная очистка. Выполняется **один раз**. Пример: Закрытие общих соединений. |\n| **Поток** | `WithWorkerOnReady`          | `func(threadID int)` | Настройка для каждого потока. Вызывается при запуске потока. Получает ID потока. |\n| **Поток** | `WithWorkerOnShutdown`       | `func(threadID int)` | Очистка для каждого потока. Получает ID потока.                     |\n\n### Пример\n\n```go\npackage myextension\n\nimport (\n    \"fmt\"\n    \"github.com/dunglas/frankenphp\"\n    frankenphpCaddy \"github.com/dunglas/frankenphp/caddy\"\n)\n\nfunc init() {\n    workerHandle = frankenphpCaddy.RegisterWorkers(\n        \"my-worker\", \"worker.php\", 2,\n\n        // Запуск сервера (Глобальный)\n        frankenphp.WithWorkerOnServerStartup(func() {\n            fmt.Println(\"Extension: Server starting up...\")\n        }),\n\n        // Поток готов (Для каждого потока)\n        // Примечание: Функция принимает целое число, представляющее ID потока\n        frankenphp.WithWorkerOnReady(func(id int) {\n            fmt.Printf(\"Extension: Worker thread #%d is ready.\\n\", id)\n        }),\n    )\n}\n```\n"
  },
  {
    "path": "docs/ru/github-actions.md",
    "content": "# Использование GitHub Actions\n\nЭтот репозиторий автоматически собирает и публикует Docker-образы в [Docker Hub](https://hub.docker.com/r/dunglas/frankenphp) для каждого одобренного pull request или вашего собственного форка после настройки.\n\n## Настройка GitHub Actions\n\nВ настройках репозитория, в разделе \"Secrets\", добавьте следующие секреты:\n\n- `REGISTRY_LOGIN_SERVER`: Docker-реестр, который будет использоваться (например, `docker.io`).\n- `REGISTRY_USERNAME`: Имя пользователя для входа в реестр (например, `dunglas`).\n- `REGISTRY_PASSWORD`: Пароль для входа в реестр (например, токен доступа).\n- `IMAGE_NAME`: Имя образа (например, `dunglas/frankenphp`).\n\n## Сборка и загрузка образа\n\n1. Создайте Pull Request или выполните push в ваш форк.\n2. GitHub Actions соберёт образ и выполнит тесты.\n3. Если сборка пройдёт успешно, образ будет отправлен в реестр с тегом `pr-x`, где `x` — номер PR.\n\n## Развёртывание образа\n\n1. После слияния Pull Request GitHub Actions выполнит повторные тесты и соберёт новый образ.\n2. Если сборка пройдёт успешно, тег `main` будет обновлён в Docker-реестре.\n\n## Релизы\n\n1. Создайте новый тег в репозитории.\n2. GitHub Actions соберёт образ и выполнит тесты.\n3. Если сборка пройдёт успешно, образ будет отправлен в реестр с именем тега (например, `v1.2.3` и `v1.2` будут созданы).\n4. Также будет обновлён тег `latest`.\n"
  },
  {
    "path": "docs/ru/hot-reload.md",
    "content": "# Горячая перезагрузка\n\nFrankenPHP включает встроенную функцию **горячей перезагрузки**, разработанную для значительного улучшения опыта разработчика.\n\n![Горячая перезагрузка](hot-reload.png)\n\nЭта функция обеспечивает рабочий процесс, похожий на **Hot Module Replacement (HMR)** в современных инструментах JavaScript, таких как Vite или webpack.\nВместо ручного обновления браузера после каждого изменения файла (PHP-код, шаблоны, файлы JavaScript и CSS...), FrankenPHP обновляет содержимое страницы в реальном времени.\n\nГорячая перезагрузка нативно работает с WordPress, Laravel, Symfony и любым другим PHP-приложением или фреймворком.\n\nКогда функция включена, FrankenPHP отслеживает изменения файловой системы в вашем текущем рабочем каталоге.\nПри изменении файла он отправляет обновление [Mercure](mercure.md) в браузер.\n\nВ зависимости от вашей настройки, браузер будет либо:\n\n- **Морфировать DOM** (сохраняя позицию прокрутки и состояние ввода), если загружен [Idiomorph](https://github.com/bigskysoftware/idiomorph).\n- **Перезагружать страницу** (стандартная живая перезагрузка), если Idiomorph отсутствует.\n\n## Конфигурация\n\nЧтобы включить горячую перезагрузку, включите Mercure, затем добавьте поддирективу `hot_reload` в директиву `php_server` в вашем `Caddyfile`.\n\n> [!WARNING]\n>\n> Эта функция предназначена **только для сред разработки**.\n> Не включайте `hot_reload` в продакшене, так как эта функция небезопасна (раскрывает конфиденциальные внутренние детали) и замедляет работу приложения.\n>\n```caddyfile\nlocalhost\n\nmercure {\n    anonymous\n}\n\nroot public/\nphp_server {\n    hot_reload\n}\n```\n\nПо умолчанию FrankenPHP будет отслеживать все файлы в текущем рабочем каталоге, соответствующие следующему глобальному шаблону: `./**/*.{css,env,gif,htm,html,jpg,jpeg,js,mjs,php,png,svg,twig,webp,xml,yaml,yml}`\n\nМожно явно задать файлы для отслеживания, используя синтаксис глобов:\n\n```caddyfile\nlocalhost\n\nmercure {\n    anonymous\n}\n\nroot public/\nphp_server {\n    hot_reload src/**/*{.php,.js} config/**/*.yaml\n}\n```\n\nИспользуйте полную форму `hot_reload`, чтобы указать используемый топик Mercure, а также каталоги или файлы для отслеживания:\n\n```caddyfile\nlocalhost\n\nmercure {\n    anonymous\n}\n\nroot public/\nphp_server {\n    hot_reload {\n        topic hot-reload-topic\n        watch src/**/*.php\n        watch assets/**/*.{ts,json}\n        watch templates/\n        watch public/css/\n    }\n}\n```\n\n## Интеграция на стороне клиента\n\nВ то время как сервер обнаруживает изменения, браузер должен подписаться на эти события, чтобы обновить страницу.\nFrankenPHP предоставляет URL-адрес Mercure Hub для подписки на изменения файлов через переменную окружения `$_SERVER['FRANKENPHP_HOT_RELOAD']`.\n\nУдобная библиотека JavaScript, [frankenphp-hot-reload](https://www.npmjs.com/package/frankenphp-hot-reload), также доступна для обработки логики на стороне клиента.\nЧтобы использовать ее, добавьте следующее в ваш основной макет:\n\n```php\n<!DOCTYPE html>\n<title>FrankenPHP Hot Reload</title>\n<?php if (isset($_SERVER['FRANKENPHP_HOT_RELOAD'])): ?>\n<meta name=\"frankenphp-hot-reload:url\" content=\"<?=$_SERVER['FRANKENPHP_HOT_RELOAD']?>\">\n<script src=\"https://cdn.jsdelivr.net/npm/idiomorph\"></script>\n<script src=\"https://cdn.jsdelivr.net/npm/frankenphp-hot-reload/+esm\" type=\"module\"></script>\n<?php endif ?>\n```\n\nБиблиотека автоматически подпишется на хаб Mercure, получит текущий URL-адрес в фоновом режиме при обнаружении изменения файла и морфирует DOM.\nОна доступна как [npm](https://www.npmjs.com/package/frankenphp-hot-reload) пакет и на [GitHub](https://github.com/dunglas/frankenphp-hot-reload).\n\nВ качестве альтернативы вы можете реализовать свою собственную клиентскую логику, подписавшись непосредственно на хаб Mercure с помощью нативного JavaScript-класса `EventSource`.\n\n### Сохранение существующих узлов DOM\n\nВ редких случаях, например, при использовании инструментов разработки [таких как панель отладки Symfony web debug toolbar](https://github.com/symfony/symfony/pull/62970),\nвы можете захотеть сохранить определенные узлы DOM.\nДля этого добавьте атрибут `data-frankenphp-hot-reload-preserve` к соответствующему HTML-элементу:\n\n```html\n<div data-frankenphp-hot-reload-preserve><!-- Моя панель отладки --></div>\n```\n\n## Режим Worker\n\nЕсли вы запускаете свое приложение в [режиме Worker](https://frankenphp.dev/docs/worker/), скрипт вашего приложения остается в памяти.\nЭто означает, что изменения в вашем PHP-коде не будут отражены немедленно, даже если браузер перезагрузится.\n\nДля лучшего опыта разработчика вы должны объединить `hot_reload` с [поддирективой `watch` в директиве `worker`](config.md#watching-for-file-changes).\n\n- `hot_reload`: обновляет **браузер** при изменении файлов\n- `worker.watch`: перезапускает воркер при изменении файлов\n\n```caddy\nlocalhost\n\nmercure {\n    anonymous\n}\n\nroot public/\nphp_server {\n    hot_reload\n    worker {\n        file /path/to/my_worker.php\n        watch\n    }\n}\n```\n\n## Как это работает\n\n1. **Отслеживание**: FrankenPHP отслеживает изменения файловой системы, используя библиотеку [`e-dant/watcher`](https://github.com/e-dant/watcher) (мы внесли вклад в Go-биндинг).\n2. **Перезапуск (Режим Worker)**: если `watch` включен в конфигурации воркера, PHP-воркер перезапускается для загрузки нового кода.\n3. **Отправка**: JSON-полезная нагрузка, содержащая список измененных файлов, отправляется во встроенный [Mercure hub](https://mercure.rocks).\n4. **Получение**: Браузер, прослушивающий через JavaScript-библиотеку, получает событие Mercure.\n5. **Обновление**:\n\n- Если обнаружен **Idiomorph**, он получает обновленное содержимое и морфирует текущий HTML, чтобы соответствовать новому состоянию, применяя изменения мгновенно без потери состояния.\n- В противном случае вызывается `window.location.reload()` для обновления страницы.\n"
  },
  {
    "path": "docs/ru/known-issues.md",
    "content": "# Известные проблемы\n\n## Неподдерживаемые расширения PHP\n\nСледующие расширения не совместимы с FrankenPHP:\n\n| Название                                                                                                    | Причина                            | Альтернативы                                                                                                         |\n| ----------------------------------------------------------------------------------------------------------- | ---------------------------------- | -------------------------------------------------------------------------------------------------------------------- |\n| [imap](https://www.php.net/manual/en/imap.installation.php)                                                 | Не поддерживает потокобезопасность | [javanile/php-imap2](https://github.com/javanile/php-imap2), [webklex/php-imap](https://github.com/Webklex/php-imap) |\n| [newrelic](https://docs.newrelic.com/docs/apm/agents/php-agent/getting-started/introduction-new-relic-php/) | Не поддерживает потокобезопасность | -                                                                                                                    |\n\n## Проблемные расширения PHP\n\nСледующие расширения имеют известные ошибки или могут вести себя непредсказуемо при использовании с FrankenPHP:\n\n| Название                                                      | Проблема                                                                                                                                                                                                                                                                                                                            |\n| ------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [ext-openssl](https://www.php.net/manual/en/book.openssl.php) | При использовании статической сборки FrankenPHP (на базе musl libc) расширение OpenSSL может аварийно завершаться при высокой нагрузке. Решение — использовать динамически связанную сборку (например, ту, что используется в Docker-образах). Ошибка [отслеживается сообществом PHP](https://github.com/php/php-src/issues/13648). |\n\n## `get_browser`\n\nФункция [get_browser()](https://www.php.net/manual/en/function.get-browser.php) начинает работать медленно через некоторое время. Решение — кэшировать результаты для каждого User-Agent, например, с помощью [APCu](https://www.php.net/manual/en/book.apcu.php), так как они статичны.\n\n## Автономные бинарные файлы и образы на базе Alpine\n\nАвтономные бинарные файлы и образы на базе Alpine (`dunglas/frankenphp:*-alpine`) используют [musl libc](https://musl.libc.org/) вместо [glibc](https://www.etalabs.net/compare_libcs.html) для уменьшения размера бинарных файлов. Это может вызвать проблемы совместимости. В частности, флаг `GLOB_BRACE` в функции glob [не поддерживается](https://www.php.net/manual/en/function.glob.php).\n\n## Использование `https://127.0.0.1` с Docker\n\nПо умолчанию FrankenPHP генерирует TLS-сертификат для `localhost`, что является самым простым и рекомендуемым вариантом для локальной разработки.\n\nЕсли вы всё же хотите использовать `127.0.0.1`, настройте генерацию сертификата, указав в переменной окружения `SERVER_NAME` значение `127.0.0.1`.\n\nОднако этого может не хватить при использовании Docker из-за [особенностей его сетевой системы](https://docs.docker.com/network/). Возможна ошибка TLS вида:  \n`curl: (35) LibreSSL/3.3.6: error:1404B438:SSL routines:ST_CONNECT:tlsv1 alert internal error`.\n\nЕсли вы используете Linux, можно воспользоваться [host-драйвером](https://docs.docker.com/network/network-tutorial-host/):\n\n```console\ndocker run \\\n    -e SERVER_NAME=\"127.0.0.1\" \\\n    -v $PWD:/app/public \\\n    --network host \\\n    dunglas/frankenphp\n```\n\nHost-драйвер не поддерживается на Mac и Windows. На этих платформах нужно определить IP-адрес контейнера и включить его в `SERVER_NAME`.\n\nВыполните команду `docker network inspect bridge`, найдите ключ `Containers` и определите последний присвоенный IP из `IPv4Address`. Увеличьте его на единицу. Если контейнеров нет, первый IP обычно `172.17.0.2`.\n\nВключите этот IP в переменную окружения `SERVER_NAME`:\n\n```console\ndocker run \\\n    -e SERVER_NAME=\"127.0.0.1, 172.17.0.3\" \\\n    -v $PWD:/app/public \\\n    -p 80:80 -p 443:443 -p 443:443/udp \\\n    dunglas/frankenphp\n```\n\n> [!CAUTION]\n> Обязательно замените `172.17.0.3` на IP, который будет присвоен вашему контейнеру.\n\nТеперь вы должны иметь доступ к `https://127.0.0.1`.\n\nЕсли это не так, запустите FrankenPHP в режиме отладки:\n\n```console\ndocker run \\\n    -e CADDY_GLOBAL_OPTIONS=\"debug\" \\\n    -e SERVER_NAME=\"127.0.0.1\" \\\n    -v $PWD:/app/public \\\n    -p 80:80 -p 443:443 -p 443:443/udp \\\n    dunglas/frankenphp\n```\n\n## Скрипты Composer с использованием `@php`\n\n[Скрипты Composer](https://getcomposer.org/doc/articles/scripts.md) могут вызывать PHP для выполнения задач, например, в [проекте Laravel](laravel.md) для команды `@php artisan package:discover --ansi`.  \nЭто [на данный момент не поддерживается](https://github.com/php/frankenphp/issues/483#issuecomment-1899890915) по двум причинам:\n\n- Composer не знает, как вызывать бинарный файл FrankenPHP;\n- Composer может добавлять настройки PHP через флаг `-d`, который FrankenPHP пока не поддерживает.\n\nРешение — создать shell-скрипт в `/usr/local/bin/php`, который удаляет неподдерживаемые параметры и вызывает FrankenPHP:\n\n```bash\n#!/usr/bin/env bash\nargs=(\"$@\")\nindex=0\nfor i in \"$@\"\ndo\n    if [ \"$i\" == \"-d\" ]; then\n        unset 'args[$index]'\n        unset 'args[$index+1]'\n    fi\n    index=$((index+1))\ndone\n\n/usr/local/bin/frankenphp php-cli ${args[@]}\n```\n\nЗатем установите переменную окружения `PHP_BINARY` на путь к нашему скрипту `php` и запустите Composer:\n\n```console\nexport PHP_BINARY=/usr/local/bin/php\ncomposer install\n```\n\n## TLS/SSL: проблемы со статическими бинарными файлами\n\nПри использовании статических бинарных файлов могут возникать следующие ошибки TLS, например, при отправке писем через STARTTLS:\n\n```text\nUnable to connect with STARTTLS: stream_socket_enable_crypto(): SSL operation failed with code 5. OpenSSL Error messages:\nerror:80000002:system library::No such file or directory\nerror:80000002:system library::No such file or directory\nerror:80000002:system library::No such file or directory\nerror:0A000086:SSL routines::certificate verify failed\n```\n\nСтатический бинарный файл не включает TLS-сертификаты, поэтому необходимо указать OpenSSL местоположение локальных сертификатов CA.\n\nВыполните [`openssl_get_cert_locations()`](https://www.php.net/manual/en/function.openssl-get-cert-locations.php), чтобы определить, где должны находиться сертификаты CA, и поместите их туда.\n\n> [!WARNING]\n> Веб и CLI контексты могут иметь разные настройки.  \n> Запустите `openssl_get_cert_locations()` в нужном контексте.\n\n[Сертификаты CA, извлечённые из Mozilla, можно скачать с сайта cURL](https://curl.se/docs/caextract.html).\n\nКроме того, многие дистрибутивы, такие как Debian, Ubuntu и Alpine, предоставляют пакеты `ca-certificates`, содержащие эти сертификаты.\n\nТакже можно использовать переменные `SSL_CERT_FILE` и `SSL_CERT_DIR`, чтобы указать OpenSSL, где искать сертификаты CA:\n\n```console\n# Установите переменные окружения для TLS-сертификатов\nexport SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt\nexport SSL_CERT_DIR=/etc/ssl/certs\n```\n"
  },
  {
    "path": "docs/ru/laravel.md",
    "content": "# Laravel\n\n## Docker\n\nЗапустить [Laravel](https://laravel.com) веб-приложение с FrankenPHP очень просто: достаточно смонтировать проект в директорию `/app` официального Docker-образа.\n\nВыполните эту команду из корневой директории вашего Laravel-приложения:\n\n```console\ndocker run -p 80:80 -p 443:443 -p 443:443/udp -v $PWD:/app dunglas/frankenphp\n```\n\nИ наслаждайтесь!\n\n## Локальная установка\n\nВы также можете запустить ваши Laravel-проекты с FrankenPHP на локальной машине:\n\n1. [Скачайте бинарный файл для вашей системы](README.md#автономный-бинарный-файл)\n2. Добавьте следующую конфигурацию в файл с именем `Caddyfile` в корневой директории вашего Laravel-проекта:\n\n   ```caddyfile\n   {\n   \tfrankenphp\n   }\n\n   # Доменное имя вашего сервера\n   localhost {\n   \t# Укажите веб-корень как директорию public/\n   \troot public/\n   \t# Включите сжатие (опционально)\n   \tencode zstd br gzip\n   \t# Выполняйте PHP-файлы из директории public/ и обслуживайте статические файлы\n   \tphp_server\n   }\n   ```\n\n3. Запустите FrankenPHP из корневой директории вашего Laravel-проекта: `frankenphp run`\n\n## Laravel Octane\n\nOctane можно установить с помощью менеджера пакетов Composer:\n\n```console\ncomposer require laravel/octane\n```\n\nПосле установки Octane выполните Artisan-команду `octane:install`, которая создаст конфигурационный файл Octane в вашем приложении:\n\n```console\nphp artisan octane:install --server=frankenphp\n```\n\nСервер Octane можно запустить с помощью Artisan-команды `octane:frankenphp`:\n\n```console\nphp artisan octane:frankenphp\n```\n\nКоманда `octane:frankenphp` поддерживает следующие опции:\n\n- `--host`: IP-адрес, к которому должен привязаться сервер (по умолчанию: `127.0.0.1`)\n- `--port`: Порт, на котором сервер будет доступен (по умолчанию: `8000`)\n- `--admin-port`: Порт, на котором будет доступен административный сервер (по умолчанию: `2019`)\n- `--workers`: Количество worker-скриптов для обработки запросов (по умолчанию: `auto`)\n- `--max-requests`: Количество запросов, обрабатываемых перед перезагрузкой сервера (по умолчанию: `500`)\n- `--caddyfile`: Путь к файлу `Caddyfile` FrankenPHP (по умолчанию: [stubbed `Caddyfile` в Laravel Octane](https://github.com/laravel/octane/blob/2.x/src/Commands/stubs/Caddyfile))\n- `--https`: Включить HTTPS, HTTP/2 и HTTP/3, а также автоматически генерировать и обновлять сертификаты\n- `--http-redirect`: Включить редирект с HTTP на HTTPS (включается только при передаче --https)\n- `--watch`: Автоматически перезагружать сервер при изменении приложения\n- `--poll`: Использовать опрос файловой системы для отслеживания изменений в файлах через сеть\n- `--log-level`: Установить уровень логирования, используя встроенный логгер Caddy\n\n> [!TIP]\n> Чтобы получить структурированные JSON-логи (полезно при использовании решений для анализа логов), явно укажите опцию `--log-level`.\n\nПодробнее о [Laravel Octane читайте в официальной документации](https://laravel.com/docs/octane).\n\n## Laravel-приложения как автономные бинарные файлы\n\nИспользуя [возможность встраивания приложений в FrankenPHP](embed.md), можно распространять Laravel-приложения как автономные бинарные файлы.\n\nСледуйте этим шагам, чтобы упаковать ваше Laravel-приложение в автономный бинарный файл для Linux:\n\n1. Создайте файл с именем `static-build.Dockerfile` в репозитории вашего приложения:\n\n   ```dockerfile\n   FROM --platform=linux/amd64 dunglas/frankenphp:static-builder-gnu\n   # Если вы планируете запускать бинарный файл на системах с musl-libc, используйте static-builder-musl\n\n   # Скопируйте ваше приложение\n   WORKDIR /go/src/app/dist/app\n   COPY . .\n\n   # Удалите тесты и другие ненужные файлы, чтобы сэкономить место\n   # В качестве альтернативы добавьте эти файлы в .dockerignore\n   RUN rm -Rf tests/\n\n   # Скопируйте файл .env\n   RUN cp .env.example .env\n   # Измените APP_ENV и APP_DEBUG для продакшна\n   RUN sed -i'' -e 's/^APP_ENV=.*/APP_ENV=production/' -e 's/^APP_DEBUG=.*/APP_DEBUG=false/' .env\n\n   # Внесите другие изменения в файл .env, если необходимо\n\n   # Установите зависимости\n   RUN composer install --ignore-platform-reqs --no-dev -a\n\n   # Соберите статический бинарный файл\n   WORKDIR /go/src/app/\n   RUN EMBED=dist/app/ ./build-static.sh\n   ```\n\n   > [!CAUTION]\n   >\n   > Некоторые `.dockerignore` файлы могут игнорировать директорию `vendor/` и файлы `.env`. Убедитесь, что вы скорректировали или удалили `.dockerignore` перед сборкой.\n\n2. Соберите:\n\n   ```console\n   docker build -t static-laravel-app -f static-build.Dockerfile .\n   ```\n\n3. Извлеките бинарный файл:\n\n   ```console\n   docker cp $(docker create --name static-laravel-app-tmp static-laravel-app):/go/src/app/dist/frankenphp-linux-x86_64 frankenphp ; docker rm static-laravel-app-tmp\n   ```\n\n4. Заполните кеши:\n\n   ```console\n   frankenphp php-cli artisan optimize\n   ```\n\n5. Запустите миграции базы данных (если есть):\n\n   ```console\n   frankenphp php-cli artisan migrate\n   ```\n\n6. Сгенерируйте секретный ключ приложения:\n\n   ```console\n   frankenphp php-cli artisan key:generate\n   ```\n\n7. Запустите сервер:\n\n   ```console\n   frankenphp php-server\n   ```\n\nВаше приложение готово!\n\nУзнайте больше о доступных опциях и о том, как собирать бинарные файлы для других ОС в [документации по встраиванию приложений](embed.md).\n\n### Изменение пути хранения\n\nПо умолчанию Laravel сохраняет загруженные файлы, кеши, логи и другие данные в директории `storage/` приложения. Это неудобно для встроенных приложений, так как каждая новая версия будет извлекаться в другую временную директорию.\n\nУстановите переменную окружения `LARAVEL_STORAGE_PATH` (например, в вашем `.env` файле) или вызовите метод `Illuminate\\Foundation\\Application::useStoragePath()`, чтобы использовать директорию за пределами временной директории.\n\n### Запуск Octane как автономный бинарный файл\n\nМожно даже упаковать приложения Laravel Octane как автономный бинарный файл!\n\nДля этого [установите Octane правильно](#laravel-octane) и следуйте шагам, описанным в [предыдущем разделе](#laravel-приложения-как-автономные-бинарные-файлы).\n\nЗатем, чтобы запустить FrankenPHP в worker-режиме через Octane, выполните:\n\n```console\nPATH=\"$PWD:$PATH\" frankenphp php-cli artisan octane:frankenphp\n```\n\n> [!CAUTION]\n> Для работы команды автономный бинарник **обязательно** должен быть назван `frankenphp`, так как Octane требует наличия программы с именем `frankenphp` в PATH.\n"
  },
  {
    "path": "docs/ru/mercure.md",
    "content": "# Real-time режим\n\nFrankenPHP поставляется с встроенным хабом [Mercure](https://mercure.rocks)!  \nMercure позволяет отправлять события в режиме реального времени на все подключённые устройства: они мгновенно получат JavaScript-событие.\n\nНе требуются JS-библиотеки или SDK!\n\n![Mercure](../mercure-hub.png)\n\nЧтобы включить хаб Mercure, обновите `Caddyfile` в соответствии с инструкциями [на сайте Mercure](https://mercure.rocks/docs/hub/config).\n\nДля отправки обновлений Mercure из вашего кода мы рекомендуем использовать [Symfony Mercure Component](https://symfony.com/components/Mercure) (для его использования не требуется полный стек Symfony).\n"
  },
  {
    "path": "docs/ru/metrics.md",
    "content": "# Метрики\n\nПри включении [метрик Caddy](https://caddyserver.com/docs/metrics) FrankenPHP предоставляет следующие метрики:\n\n- `frankenphp_[worker]_total_workers`: Общее количество worker-скриптов.\n- `frankenphp_[worker]_busy_workers`: Количество worker-скриптов, которые в данный момент обрабатывают запрос.\n- `frankenphp_[worker]_worker_request_time`: Время, затраченное всеми worker-скриптами на обработку запросов.\n- `frankenphp_[worker]_worker_request_count`: Количество запросов, обработанных всеми worker-скриптами.\n- `frankenphp_[worker]_ready_workers`: Количество worker-скриптов, которые вызвали `frankenphp_handle_request` хотя бы один раз.\n- `frankenphp_[worker]_worker_crashes`: Количество случаев неожиданного завершения worker-скриптов.\n- `frankenphp_[worker]_worker_restarts`: Количество случаев, когда worker-скрипт был перезапущен целенаправленно.\n- `frankenphp_total_threads`: Общее количество потоков PHP.\n- `frankenphp_busy_threads`: Количество потоков PHP, которые в данный момент обрабатывают запрос (работающие worker-скрипты всегда используют поток).\n\nДля метрик worker-скриптов плейсхолдер `[worker]` заменяется на путь к Worker-скрипту, указанному в Caddyfile.\n"
  },
  {
    "path": "docs/ru/performance.md",
    "content": "# Производительность\n\nПо умолчанию FrankenPHP стремится предложить хороший компромисс между производительностью и простотой использования. Однако, используя подходящую конфигурацию, можно существенно улучшить производительность.\n\n## Количество потоков и воркеров\n\nПо умолчанию FrankenPHP запускает в 2 раза больше потоков и воркеров (в режиме воркера), чем доступное количество ядер процессора.\n\nПодходящие значения сильно зависят от того, как написано ваше приложение, что оно делает, и от вашего оборудования. Мы настоятельно рекомендуем изменить эти значения. Для лучшей стабильности системы рекомендуется, чтобы `num_threads` x `memory_limit` < `available_memory`.\n\nЧтобы найти подходящие значения, лучше всего провести нагрузочные тесты, имитирующие реальный трафик. [k6](https://k6.io) и [Gatling](https://gatling.io) являются хорошими инструментами для этого.\n\nЧтобы настроить количество потоков, используйте опцию `num_threads` директив `php_server` и `php`. Для изменения количества воркеров используйте опцию `num` секции `worker` директивы `frankenphp`.\n\n### `max_threads`\n\nХотя всегда лучше точно знать, как будет выглядеть ваш трафик, реальные приложения, как правило, более непредсказуемы. [Конфигурация](config.md#конфигурация-caddyfile) `max_threads` позволяет FrankenPHP автоматически создавать дополнительные потоки во время выполнения до указанного предела. `max_threads` может помочь вам определить, сколько потоков требуется для обработки вашего трафика, и может сделать сервер более устойчивым к скачкам задержки. Если установлено значение `auto`, лимит будет оценен на основе `memory_limit` в вашем `php.ini`. Если это невозможно, `auto` вместо этого будет по умолчанию равно 2x `num_threads`. Имейте в виду, что `auto` может сильно недооценивать необходимое количество потоков. `max_threads` похожа на [pm.max_children](https://www.php.net/manual/en/install.fpm.configuration.php#pm.max-children) в PHP FPM. Основное отличие состоит в том, что FrankenPHP использует потоки вместо процессов и автоматически распределяет их между различными скриптами воркеров и 'классическим режимом' по мере необходимости.\n\n## Режим воркера\n\nВключение [режима воркера](worker.md) значительно улучшает производительность, но ваше приложение должно быть адаптировано для совместимости с этим режимом: необходимо создать скрипт воркера и убедиться, что приложение не имеет утечек памяти.\n\n## Избегайте использования musl\n\nВариант официальных Docker-образов для Alpine Linux и предоставляемые нами бинарные файлы по умолчанию используют [библиотеку musl libc](https://musl.libc.org).\n\nИзвестно, что PHP [работает медленнее](https://gitlab.alpinelinux.org/alpine/aports/-/issues/14381) при использовании этой альтернативной библиотеки C вместо традиционной библиотеки GNU, особенно при компиляции в режиме ZTS (потокобезопасном режиме), который требуется для FrankenPHP. Разница может быть существенной в сильно многопоточной среде.\n\nКроме того, [некоторые ошибки проявляются исключительно при использовании musl](https://github.com/php/php-src/issues?q=sort%3Aupdated-desc+is%3Aissue+is%3Aopen+label%3ABug+musl).\n\nВ производственных средах мы рекомендуем использовать FrankenPHP, скомпилированный с glibc и с соответствующим уровнем оптимизации.\n\nЭтого можно достичь, используя Docker-образы Debian, используя [пакеты .deb, .rpm или .apk от наших мейнтейнеров](https://pkgs.henderkes.com) или [компилируя FrankenPHP из исходников](compile.md).\n\nДля более компактных или безопасных контейнеров вы можете рассмотреть [усиленный образ Debian](docker.md#hardening-images) вместо Alpine.\n\n## Настройка среды выполнения Go\n\nFrankenPHP написан на языке Go.\n\nВ целом, среда выполнения Go не требует какой-либо особой настройки, но при определенных обстоятельствах специфическая конфигурация улучшает производительность.\n\nВероятно, вы захотите установить переменную окружения `GODEBUG` в значение `cgocheck=0` (по умолчанию в Docker-образах FrankenPHP).\n\nЕсли вы запускаете FrankenPHP в контейнерах (Docker, Kubernetes, LXC...) и ограничиваете память, доступную для контейнеров, установите переменную окружения `GOMEMLIMIT` в значение доступного объёма памяти.\n\nДля получения более подробной информации [страница документации Go, посвященная этой теме](https://pkg.go.dev/runtime#hdr-Environment_Variables), обязательна к прочтению, чтобы максимально эффективно использовать среду выполнения.\n\n## `file_server`\n\nПо умолчанию директива `php_server` автоматически настраивает файловый сервер для обслуживания статических файлов (ресурсов), хранящихся в корневой директории.\n\nЭта функция удобна, но имеет издержки. Чтобы отключить её, используйте следующую конфигурацию:\n\n```caddyfile\nphp_server {\n    file_server off\n}\n```\n\n## `try_files`\n\nПомимо статических файлов и файлов PHP, `php_server` также попытается обслужить индекс вашего приложения и индексные файлы директорий (`/path/` -> `/path/index.php`). Если вам не нужны индексы директорий, вы можете отключить их, явно определив `try_files` следующим образом:\n\n```caddyfile\nphp_server {\n    try_files {path} index.php\n    root /root/to/your/app # явное указание корневой директории здесь обеспечивает лучшее кеширование\n}\n```\n\nЭто может значительно сократить количество ненужных файловых операций. Эквивалент предыдущей конфигурации для воркера будет следующим:\n\n```caddyfile\nroute {\n    php_server { # используйте \"php\" вместо \"php_server\", если вам вообще не нужен файловый сервер\n        root /root/to/your/app\n        worker /path/to/worker.php {\n            match * # отправить все запросы напрямую воркеру\n        }\n    }\n}\n```\n\nАльтернативный подход с 0 ненужных операций файловой системы заключается в использовании директивы `php` и разделении файлов от PHP по пути. Этот подход хорошо работает, если все ваше приложение обслуживается одним входным файлом. Пример [конфигурации](config.md#конфигурация-caddyfile), которая обслуживает статические файлы за папкой `/assets`, может выглядеть так:\n\n```caddyfile\nroute {\n    @assets {\n        path /assets/*\n    }\n\n    # всё, что находится за /assets, обрабатывается файловым сервером\n    file_server @assets {\n        root /root/to/your/app\n    }\n\n    # всё, что не находится в /assets, обрабатывается вашим индексным или воркер PHP-файлом\n    rewrite index.php\n    php {\n        root /root/to/your/app # явное указание корневой директории здесь обеспечивает лучшее кеширование\n    }\n}\n```\n\n## Плейсхолдеры\n\nВы можете использовать [плейсхолдеры](https://caddyserver.com/docs/conventions#placeholders) в директивах `root` и `env`. Однако это предотвращает кеширование значений и существенно снижает производительность.\n\nПо возможности избегайте использования плейсхолдеров в этих директивах.\n\n## `resolve_root_symlink`\n\nПо умолчанию, если корневая директория документа является символьной ссылкой, она автоматически разрешается FrankenPHP (это необходимо для корректной работы PHP). Если корневая директория документа не является символьной ссылкой, вы можете отключить эту функцию.\n\n```caddyfile\nphp_server {\n    resolve_root_symlink false\n}\n```\n\nЭто улучшит производительность, если директива `root` содержит [плейсхолдеры](https://caddyserver.com/docs/conventions#placeholders). В остальных случаях прирост производительности будет минимальным.\n\n## Логи\n\nЛогирование, безусловно, очень полезно, но, по определению, оно требует операций ввода-вывода и выделения памяти, что значительно снижает производительность. Убедитесь, что вы [правильно настроили уровень логирования](https://caddyserver.com/docs/caddyfile/options#log) и логируете только то, что необходимо.\n\n## Производительность PHP\n\nFrankenPHP использует официальный интерпретатор PHP. Все обычные оптимизации производительности, связанные с PHP, применимы к FrankenPHP.\n\nВ частности:\n\n- убедитесь, что [OPcache](https://www.php.net/manual/en/book.opcache.php) установлен, включён и настроен должным образом;\n- включите [оптимизацию автозагрузки Composer](https://getcomposer.org/doc/articles/autoloader-optimization.md);\n- убедитесь, что кеш `realpath` достаточно велик для нужд вашего приложения;\n- используйте [предварительную загрузку](https://www.php.net/manual/en/opcache.preloading.php).\n\nДля получения более подробной информации ознакомьтесь с [разделом документации Symfony, посвященным производительности](https://symfony.com/doc/current/performance.html) (большинство советов полезны, даже если вы не используете Symfony).\n\n## Разделение пула потоков\n\nПриложениям часто приходится взаимодействовать с медленными внешними службами, такими как API, который имеет тенденцию быть ненадежным при высокой нагрузке или постоянно отвечает в течение 10+ секунд. В таких случаях может быть полезно разделить пул потоков, чтобы иметь выделенные \"медленные\" пулы. Это предотвращает потребление медленными конечными точками всех ресурсов/потоков сервера и ограничивает параллелизм запросов, направляемых к медленной конечной точке, подобно пулу соединений.\n\n```caddyfile\nexample.com {\n    php_server {\n        root /app/public # корень вашего приложения\n        worker index.php {\n            match /slow-endpoint/* # все запросы к /slow-endpoint/* обрабатываются этим пулом потоков\n            num 1 # минимум 1 поток для запросов к /slow-endpoint/*\n            max_threads 20 # при необходимости разрешить до 20 потоков для запросов к /slow-endpoint/*\n        }\n        worker index.php {\n            match * # все остальные запросы обрабатываются отдельно\n            num 1 # минимум 1 поток для остальных запросов, даже если медленные конечные точки начинают зависать\n            max_threads 20 # при необходимости разрешить до 20 потоков для остальных запросов\n        }\n    }\n}\n```\n\nВ целом, также рекомендуется обрабатывать очень медленные конечные точки асинхронно, используя соответствующие механизмы, такие как очереди сообщений.\n"
  },
  {
    "path": "docs/ru/production.md",
    "content": "# Деплой в продакшен\n\nВ этом руководстве мы рассмотрим, как развернуть PHP-приложение на одном сервере с использованием Docker Compose.\n\nЕсли вы используете Symfony, рекомендуется прочитать раздел \"[Deploy in production](https://github.com/dunglas/symfony-docker/blob/main/docs/production.md)\" документации проекта Symfony Docker (в котором используется FrankenPHP).\n\nЕсли вы используете API Platform (который также работает с FrankenPHP), ознакомьтесь с [документацией по деплою этого фреймворка](https://api-platform.com/docs/deployment/).\n\n## Подготовка приложения\n\nСначала создайте файл `Dockerfile` в корневой директории вашего PHP-проекта:\n\n```dockerfile\nFROM dunglas/frankenphp\n\n# Замените \"your-domain-name.example.com\" на ваш домен\nENV SERVER_NAME=your-domain-name.example.com\n# Если вы хотите отключить HTTPS, используйте вместо этого:\n#ENV SERVER_NAME=:80\n\n# Включите настройки PHP для продакшн\nRUN mv \"$PHP_INI_DIR/php.ini-production\" \"$PHP_INI_DIR/php.ini\"\n\n# Скопируйте файлы PHP вашего проекта в публичную директорию\nCOPY . /app/public\n# Если вы используете Symfony или Laravel, необходимо скопировать весь проект:\n#COPY . /app\n```\n\nОзнакомьтесь с разделом \"[Создание кастомных Docker-образов](docker.md)\" для получения дополнительных подробностей и настроек, а также для установки PHP-расширений и модулей Caddy.\n\nЕсли ваш проект использует Composer, убедитесь, что он включён в Docker-образ, и установите все зависимости.\n\nЗатем добавьте файл `compose.yaml`:\n\n```yaml\nservices:\n  php:\n    image: dunglas/frankenphp\n    restart: always\n    ports:\n      - \"80:80\" # HTTP\n      - \"443:443\" # HTTPS\n      - \"443:443/udp\" # HTTP/3\n    volumes:\n      - caddy_data:/data\n      - caddy_config:/config\n\n# Томы, необходимые для сертификатов и конфигурации Caddy\nvolumes:\n  caddy_data:\n  caddy_config:\n```\n\n> [!NOTE]\n>\n> Примеры выше предназначены для использования в продакшне.  \n> В процессе разработки вы можете использовать том для монтирования, другую конфигурацию PHP и другое значение для переменной окружения `SERVER_NAME`.\n>\n> Посмотрите проект [Symfony Docker](https://github.com/dunglas/symfony-docker) (который использует FrankenPHP) для более сложного примера с использованием мультистейдж-образов, Composer, дополнительных PHP-расширений и т.д.\n\nНаконец, если вы используете Git, закоммитьте эти файлы и отправьте их в репозиторий.\n\n## Подготовка сервера\n\nДля деплоя приложения в продакшн требуется сервер. В этом руководстве мы будем использовать виртуальную машину, предоставляемую DigitalOcean, но подойдёт любой Linux-сервер.  \nЕсли у вас уже есть Linux-сервер с установленным Docker, вы можете сразу перейти к [следующему разделу](#настройка-доменного-имени).\n\nВ противном случае, используйте [эту ссылку](https://m.do.co/c/5d8aabe3ab80), чтобы получить $200 на баланс, создайте аккаунт, затем нажмите \"Create a Droplet\".  \nПерейдите во вкладку \"Marketplace\" в разделе \"Choose an image\" и найдите приложение \"Docker\". Это создаст сервер на Ubuntu с установленными Docker и Docker Compose.\n\nДля тестов подойдут самые дешёвые тарифы. Для реального продакшна выберите тариф из раздела \"general purpose\" в зависимости от ваших потребностей.\n\n![Деплой FrankenPHP на DigitalOcean с Docker](../digitalocean-droplet.png)\n\nПосле этого подключитесь к серверу через SSH:\n\n```console\nssh root@<droplet-ip>\n```\n\n## Настройка доменного имени\n\nВ большинстве случаев вам потребуется связать доменное имя с вашим сайтом.  \nСоздайте запись DNS типа `A`, указывающую на IP вашего сервера:\n\n```dns\nyour-domain-name.example.com.  IN  A     207.154.233.113\n```\n\nПример настройки через DigitalOcean (\"Networking\" > \"Domains\"):\n\n![Настройка DNS в DigitalOcean](../digitalocean-dns.png)\n\n> [!NOTE]\n>\n> Let's Encrypt, сервис, используемый FrankenPHP для автоматической генерации TLS-сертификатов, не поддерживает использование IP-адресов. Для работы необходим домен.\n\n## Деплой\n\nСкопируйте ваш проект на сервер с помощью `git clone`, `scp` или любого другого инструмента.  \nЕсли вы используете GitHub, настройте [ключи развёртывания](https://docs.github.com/en/free-pro-team@latest/developers/overview/managing-deploy-keys#deploy-keys).\n\nПример с использованием Git:\n\n```console\ngit clone git@github.com:<username>/<project-name>.git\n```\n\nПерейдите в директорию проекта и запустите приложение в режиме продакшн:\n\n```console\ndocker compose up -d --wait\n```\n\nСервер готов, а HTTPS-сертификат был автоматически сгенерирован. Перейдите на `https://your-domain-name.example.com` и наслаждайтесь!\n\n> [!CAUTION]\n>\n> Docker может кэшировать слои. Убедитесь, что вы используете актуальную сборку, или используйте опцию `--no-cache` для предотвращения проблем с кэшем.\n\n## Деплой на несколько узлов\n\nЕсли вам нужно развернуть приложение на кластер машин, используйте [Docker Swarm](https://docs.docker.com/engine/swarm/stack-deploy/), который совместим с предоставленными файлами Compose.  \nДля деплоя на Kubernetes ознакомьтесь с [Helm-чартом API Platform](https://api-platform.com/docs/deployment/kubernetes/), который использует FrankenPHP.\n"
  },
  {
    "path": "docs/ru/static.md",
    "content": "# Создание статических бинарных файлов\n\nВместо использования локальной установки библиотеки PHP, можно создать статическую сборку FrankenPHP благодаря проекту [static-php-cli](https://github.com/crazywhalecc/static-php-cli) (несмотря на название, проект поддерживает все SAPI, а не только CLI).\n\nС помощью этого метода создаётся единый переносимый бинарник, который включает PHP-интерпретатор, веб-сервер Caddy и FrankenPHP!\n\nFrankenPHP также поддерживает [встраивание PHP-приложений в статический бинарный файл](embed.md).\n\n## Linux\n\nМы предоставляем Docker-образ для сборки статического бинарника для Linux:\n\n```console\ndocker buildx bake --load static-builder\ndocker cp $(docker create --name static-builder-musl dunglas/frankenphp:static-builder-musl):/go/src/app/dist/frankenphp-linux-$(uname -m) frankenphp ; docker rm static-builder\n```\n\nСозданный статический бинарный файл называется `frankenphp` и будет доступен в текущей директории.\n\nЧтобы собрать статический бинарный файл без Docker, используйте инструкции для macOS — они подходят и для Linux.\n\n### Дополнительные расширения\n\nПо умолчанию компилируются самые популярные PHP-расширения.\n\nЧтобы уменьшить размер бинарного файла и сократить возможные векторы атак, можно указать список расширений, которые следует включить в сборку, используя Docker-аргумент `PHP_EXTENSIONS`.\n\nНапример, выполните следующую команду, чтобы собрать только расширение `opcache`:\n\n```console\ndocker buildx bake --load --set static-builder.args.PHP_EXTENSIONS=opcache,pdo_sqlite static-builder\n# ...\n```\n\nЧтобы добавить библиотеки, расширяющие функциональность включённых расширений, используйте Docker-аргумент `PHP_EXTENSION_LIBS`:\n\n```console\ndocker buildx bake \\\n  --load \\\n  --set static-builder.args.PHP_EXTENSIONS=gd \\\n  --set static-builder.args.PHP_EXTENSION_LIBS=libjpeg,libwebp \\\n  static-builder\n```\n\n### Дополнительные модули Caddy\n\nЧтобы добавить дополнительные модули Caddy или передать аргументы в [xcaddy](https://github.com/caddyserver/xcaddy), используйте Docker-аргумент `XCADDY_ARGS`:\n\n```console\ndocker buildx bake \\\n  --load \\\n  --set static-builder.args.XCADDY_ARGS=\"--with github.com/darkweak/souin/plugins/caddy --with github.com/dunglas/caddy-cbrotli --with github.com/dunglas/mercure/caddy --with github.com/dunglas/vulcain/caddy\" \\\n  static-builder\n```\n\nВ этом примере добавляются модуль HTTP-кэширования [Souin](https://souin.io) для Caddy, а также модули [cbrotli](https://github.com/dunglas/caddy-cbrotli), [Mercure](https://mercure.rocks) и [Vulcain](https://vulcain.rocks).\n\n> [!TIP]\n>\n> Модули cbrotli, Mercure и Vulcain включены по умолчанию, если `XCADDY_ARGS` пуст или не установлен.  \n> Если вы изменяете значение `XCADDY_ARGS`, добавьте их явно, если хотите включить их в сборку.\n\nСм. также, как [настроить сборку](#настройка-сборки).\n\n### Токен GitHub\n\nЕсли вы достигли лимита запросов к API GitHub, задайте личный токен доступа GitHub в переменной окружения `GITHUB_TOKEN`:\n\n```console\nGITHUB_TOKEN=\"xxx\" docker --load buildx bake static-builder\n# ...\n```\n\n## macOS\n\nЗапустите следующий скрипт, чтобы создать статический бинарный файл для macOS (должен быть установлен [Homebrew](https://brew.sh/)):\n\n```console\ngit clone https://github.com/php/frankenphp\ncd frankenphp\n./build-static.sh\n```\n\nПримечание: этот скрипт также работает на Linux (и, вероятно, на других Unix-системах) и используется внутри предоставленного Docker-образа для статической сборки.\n\n## Настройка сборки\n\nСледующие переменные окружения можно передать в `docker build` и скрипт `build-static.sh`, чтобы настроить статическую сборку:\n\n- `FRANKENPHP_VERSION`: версия FrankenPHP\n- `PHP_VERSION`: версия PHP\n- `PHP_EXTENSIONS`: PHP-расширения для сборки ([список поддерживаемых расширений](https://static-php.dev/en/guide/extensions.html))\n- `PHP_EXTENSION_LIBS`: дополнительные библиотеки, добавляющие функциональность расширениям\n- `XCADDY_ARGS`: аргументы для [xcaddy](https://github.com/caddyserver/xcaddy), например, для добавления модулей Caddy\n- `EMBED`: путь к PHP-приложению для встраивания в бинарник\n- `CLEAN`: если задано, libphp и все его зависимости будут пересобраны с нуля (без кэша)\n- `NO_COMPRESS`: отключает сжатие результирующего бинарника с помощью UPX\n- `DEBUG_SYMBOLS`: если задано, отладочные символы не будут удалены и будут добавлены в бинарник\n- `MIMALLOC`: (экспериментально, только для Linux) заменяет musl's mallocng на [mimalloc](https://github.com/microsoft/mimalloc) для повышения производительности\n- `RELEASE`: (только для мейнтейнеров) если задано, бинарник будет загружен на GitHub\n"
  },
  {
    "path": "docs/ru/worker.md",
    "content": "# Использование воркеров FrankenPHP\n\nЗагрузите приложение один раз и держите его в памяти.\nFrankenPHP будет обрабатывать входящие запросы за несколько миллисекунд.\n\n## Запуск worker-скриптов\n\n### Docker\n\nУстановите значение переменной окружения `FRANKENPHP_CONFIG` на `worker /path/to/your/worker/script.php`:\n\n```console\ndocker run \\\n    -e FRANKENPHP_CONFIG=\"worker /app/path/to/your/worker/script.php\" \\\n    -v $PWD:/app \\\n    -p 80:80 -p 443:443 -p 443:443/udp \\\n    dunglas/frankenphp\n```\n\n### Автономный бинарный файл\n\nИспользуйте опцию `--worker` команды `php-server` для обслуживания содержимого текущей директории в режиме воркера:\n\n```console\nfrankenphp php-server --worker /path/to/your/worker/script.php\n```\n\nЕсли ваше PHP-приложение [встроено в бинарник](embed.md), вы можете добавить пользовательский `Caddyfile` в корневую директорию приложения.\nОн будет использоваться автоматически.\n\nТакже можно [перезапускать воркер при изменении файлов](config.md#watching-for-file-changes) с помощью опции `--watch`.\nСледующая команда выполнит перезапуск, если будет изменён любой файл с расширением `.php` в директории `/path/to/your/app/` или её поддиректориях:\n\n```console\nfrankenphp php-server --worker /path/to/your/worker/script.php --watch=\"/path/to/your/app/**/*.php\"\n```\n\nЭта функция часто используется в сочетании с [горячей перезагрузкой](hot-reload.md).\n\n## Symfony Runtime\n\n> [!TIP]\n> Следующий раздел актуален только для версий Symfony до 7.4, где была введена нативная поддержка режима воркеров FrankenPHP.\n\nWorker режим FrankenPHP поддерживается компонентом [Symfony Runtime](https://symfony.com/doc/current/components/runtime.html).\nЧтобы запустить любое Symfony-приложение в worker режиме, установите пакет FrankenPHP для [PHP Runtime](https://github.com/php-runtime/runtime):\n\n```console\ncomposer require runtime/frankenphp-symfony\n```\n\nЗапустите сервер приложения, задав переменную окружения `APP_RUNTIME` для использования FrankenPHP Symfony Runtime:\n\n```console\ndocker run \\\n    -e FRANKENPHP_CONFIG=\"worker ./public/index.php\" \\\n    -e APP_RUNTIME=Runtime\\\\FrankenPhpSymfony\\\\Runtime \\\n    -v $PWD:/app \\\n    -p 80:80 -p 443:443 -p 443:443/udp \\\n    dunglas/frankenphp\n```\n\n## Laravel Octane\n\nПодробнее см. в [отдельной документации](laravel.md#laravel-octane).\n\n## Пользовательские приложения\n\nСледующий пример показывает, как создать собственный worker-скрипт без использования сторонних библиотек:\n\n```php\n<?php\n// public/index.php\n\n// Инициализация вашего приложения\nrequire __DIR__.'/vendor/autoload.php';\n\n$myApp = new \\App\\Kernel();\n$myApp->boot();\n\n// Обработчик запросов за пределами цикла для повышения производительности (выполняет меньше работы)\n$handler = static function () use ($myApp) {\n    try {\n        // Выполняется при получении запроса,\n        // суперглобальные переменные, php://input и прочие сбрасываются\n        echo $myApp->handle($_GET, $_POST, $_COOKIE, $_FILES, $_SERVER);\n    } catch (\\Throwable $exception) {\n        // `set_exception_handler` вызывается только когда worker-скрипт завершается,\n        // что может быть не тем, что вы ожидаете, поэтому перехватывайте и обрабатывайте исключения здесь\n        (new \\MyCustomExceptionHandler)->handleException($exception);\n    }\n};\n\n$maxRequests = (int)($_SERVER['MAX_REQUESTS'] ?? 0);\nfor ($nbRequests = 0; !$maxRequests || $nbRequests < $maxRequests; ++$nbRequests) {\n    $keepRunning = \\frankenphp_handle_request($handler);\n\n    // Действия после отправки HTTP-ответа\n    $myApp->terminate();\n\n    // Вызов сборщика мусора, чтобы снизить вероятность его запуска в процессе генерации страницы\n    gc_collect_cycles();\n\n    if (!$keepRunning) break;\n}\n\n// Очистка\n$myApp->shutdown();\n```\n\nЗапустите ваше приложение и используйте переменную окружения `FRANKENPHP_CONFIG` для настройки вашего воркера:\n\n```console\ndocker run \\\n    -e FRANKENPHP_CONFIG=\"worker ./public/index.php\" \\\n    -v $PWD:/app \\\n    -p 80:80 -p 443:443 -p 443:443/udp \\\n    dunglas/frankenphp\n```\n\nПо умолчанию запускается 2 воркера на каждый CPU. Вы также можете настроить количество запускаемых воркеров:\n\n```console\ndocker run \\\n    -e FRANKENPHP_CONFIG=\"worker ./public/index.php 42\" \\\n    -v $PWD:/app \\\n    -p 80:80 -p 443:443 -p 443:443/udp \\\n    dunglas/frankenphp\n```\n\n### Перезапуск worker-скрипта после определённого количества запросов\n\nPHP изначально не предназначался для долгоживущих процессов, поэтому есть ещё много библиотек и устаревшего кода, которые вызывают утечки памяти.\nОбходной путь для использования такого кода в режиме воркера состоит в перезапуске скрипта воркера после обработки определённого количества запросов:\n\nПредыдущий сниппет воркера позволяет настроить максимальное количество запросов для обработки, установив переменную окружения с именем `MAX_REQUESTS`.\n\n### Перезапуск воркеров вручную\n\nХотя можно перезапускать воркеры [при изменении файлов](config.md#watching-for-file-changes), также можно корректно перезапустить все воркеры через [API администрирования Caddy](https://caddyserver.com/docs/api). Если администрирование включено в вашем [Caddyfile](config.md#caddyfile-config), вы можете отправить запрос POST на конечную точку перезапуска следующим образом:\n\n```console\ncurl -X POST http://localhost:2019/frankenphp/workers/restart\n```\n\n### Сбои worker-скрипта\n\nЕсли worker-скрипт завершится с ненулевым кодом выхода, FrankenPHP перезапустит его со стратегией экспоненциальной задержки.\nЕсли скрипт воркера остается активным дольше, чем время последней задержки \\* 2, FrankenPHP не будет применять штраф к worker-скрипту и перезапустит его снова.\nОднако, если worker-скрипт продолжает завершаться с ненулевым кодом выхода в течение короткого промежутка времени\n(например, из-за опечатки в скрипте), FrankenPHP завершит работу с ошибкой: `too many consecutive failures`.\n\nКоличество последовательных сбоев можно настроить в вашем [Caddyfile](config.md#caddyfile-config) с помощью опции `max_consecutive_failures`:\n\n```caddyfile\nfrankenphp {\n    worker {\n        # ...\n        max_consecutive_failures 10\n    }\n}\n```\n\n## Поведение суперглобальных переменных\n\n[PHP суперглобальные переменные](https://www.php.net/manual/en/language.variables.superglobals.php) (`$_SERVER`, `$_ENV`, `$_GET`...) ведут себя следующим образом:\n\n- до первого вызова `frankenphp_handle_request()`, суперглобальные переменные содержат значения, связанные с самим worker-скриптом\n- во время и после вызова `frankenphp_handle_request()`, суперглобальные переменные содержат значения, сгенерированные на основе обработанного HTTP-запроса, каждый вызов `frankenphp_handle_request()` изменяет значения суперглобальных переменных\n\nЧтобы получить доступ к суперглобальным переменным worker-скрипта внутри колбэка, необходимо скопировать их и импортировать копию в область видимости колбэка:\n\n```php\n<?php\n// Копирование суперглобальной переменной $_SERVER воркера перед первым вызовом frankenphp_handle_request()\n$workerServer = $_SERVER;\n\n$handler = static function () use ($workerServer) {\n    var_dump($_SERVER); // $_SERVER, привязанный к запросу\n    var_dump($workerServer); // $_SERVER worker-скрипта\n};\n\n// ...\n"
  },
  {
    "path": "docs/static.md",
    "content": "# Create a Static Build\n\nInstead of using a local installation of the PHP library,\nit's possible to create a static or mostly static build of FrankenPHP thanks to the great [static-php-cli project](https://github.com/crazywhalecc/static-php-cli) (despite its name, this project supports all SAPIs, not only CLI).\n\nWith this method, a single, portable, binary will contain the PHP interpreter, the Caddy web server, and FrankenPHP!\n\nFully static native executables require no dependencies at all and can even be run on [`scratch` Docker image](https://docs.docker.com/build/building/base-images/#create-a-minimal-base-image-using-scratch).\nHowever, they can't load dynamic PHP extensions (such as Xdebug) and have some limitations because they are using the musl libc.\n\nMostly static binaries only require `glibc` and can load dynamic extensions.\n\nWhen possible, we recommend using glibc-based, mostly static builds.\n\nFrankenPHP also supports [embedding the PHP app in the static binary](embed.md).\n\n## Linux\n\nWe provide Docker images to build static Linux binaries:\n\n### musl-Based, Fully Static Build\n\nFor a fully-static binary that runs on any Linux distribution without dependencies but doesn't support dynamic loading of extensions:\n\n```console\ndocker buildx bake --load static-builder-musl\ndocker cp $(docker create --name static-builder-musl dunglas/frankenphp:static-builder-musl):/go/src/app/dist/frankenphp-linux-$(uname -m) frankenphp ; docker rm static-builder-musl\n```\n\nFor better performance in heavily concurrent scenarios, consider using the [mimalloc](https://github.com/microsoft/mimalloc) allocator.\n\n```console\ndocker buildx bake --load --set static-builder-musl.args.MIMALLOC=1 static-builder-musl\n```\n\n### glibc-Based, Mostly Static Build (With Dynamic Extension Support)\n\nFor a binary that supports loading PHP extensions dynamically while still having the selected extensions compiled statically:\n\n```console\ndocker buildx bake --load static-builder-gnu\ndocker cp $(docker create --name static-builder-gnu dunglas/frankenphp:static-builder-gnu):/go/src/app/dist/frankenphp-linux-$(uname -m) frankenphp ; docker rm static-builder-gnu\n```\n\nThis binary supports all glibc versions 2.17 and superior but does not run on musl-based systems (like Alpine Linux).\n\nThe resulting mostly static (except `glibc`) binary is named `frankenphp` and is available in the current directory.\n\nIf you want to build the static binary without Docker, take a look at the macOS instructions, which also work for Linux.\n\n### Custom Extensions\n\nBy default, the most popular PHP extensions are compiled.\n\nTo reduce the size of the binary and to reduce the attack surface, you can choose the list of extensions to build using the `PHP_EXTENSIONS` Docker ARG.\n\nFor instance, run the following command to only build the `opcache` extension:\n\n```console\ndocker buildx bake --load --set static-builder-musl.args.PHP_EXTENSIONS=opcache,pdo_sqlite static-builder-musl\n# ...\n```\n\nTo add libraries enabling additional functionality to the extensions you've enabled, you can pass the `PHP_EXTENSION_LIBS` Docker ARG:\n\n```console\ndocker buildx bake \\\n  --load \\\n  --set static-builder-musl.args.PHP_EXTENSIONS=gd \\\n  --set static-builder-musl.args.PHP_EXTENSION_LIBS=libjpeg,libwebp \\\n  static-builder-musl\n```\n\n### Extra Caddy Modules\n\nTo add extra Caddy modules or pass other arguments to [xcaddy](https://github.com/caddyserver/xcaddy), use the `XCADDY_ARGS` Docker ARG:\n\n```console\ndocker buildx bake \\\n  --load \\\n  --set static-builder-musl.args.XCADDY_ARGS=\"--with github.com/darkweak/souin/plugins/caddy --with github.com/dunglas/caddy-cbrotli --with github.com/dunglas/mercure/caddy --with github.com/dunglas/vulcain/caddy\" \\\n  static-builder-musl\n```\n\nIn this example, we add the [Souin](https://souin.io) HTTP cache module for Caddy as well as the [cbrotli](https://github.com/dunglas/caddy-cbrotli), [Mercure](https://mercure.rocks) and [Vulcain](https://vulcain.rocks) modules.\n\n> [!TIP]\n>\n> The cbrotli, Mercure, and Vulcain modules are included by default if `XCADDY_ARGS` is empty or not set.\n> If you customize the value of `XCADDY_ARGS`, you must include them explicitly if you want them to be included.\n\nSee also how to [customize the build](#customizing-the-build)\n\n### GitHub Token\n\nIf you hit the GitHub API rate limit, set a GitHub Personal Access Token in an environment variable named `GITHUB_TOKEN`:\n\n```console\nGITHUB_TOKEN=\"xxx\" docker --load buildx bake static-builder-musl\n# ...\n```\n\n## macOS\n\nRun the following script to create a static binary for macOS (you must have [Homebrew](https://brew.sh/) installed):\n\n```console\ngit clone https://github.com/php/frankenphp\ncd frankenphp\n./build-static.sh\n```\n\nNote: this script also works on Linux (and probably on other Unixes), and is used internally by the Docker images we provide.\n\n## Customizing The Build\n\nThe following environment variables can be passed to `docker build` and to the `build-static.sh`\nscript to customize the static build:\n\n- `FRANKENPHP_VERSION`: the version of FrankenPHP to use\n- `PHP_VERSION`: the version of PHP to use\n- `PHP_EXTENSIONS`: the PHP extensions to build ([list of supported extensions](https://static-php.dev/en/guide/extensions.html))\n- `PHP_EXTENSION_LIBS`: extra libraries to build that add features to the extensions\n- `XCADDY_ARGS`: arguments to pass to [xcaddy](https://github.com/caddyserver/xcaddy), for instance to add extra Caddy modules\n- `EMBED`: path of the PHP application to embed in the binary\n- `CLEAN`: when set, libphp and all its dependencies are built from scratch (no cache)\n- `NO_COMPRESS`: don't compress the resulting binary using UPX\n- `DEBUG_SYMBOLS`: when set, debug-symbols will not be stripped and will be added to the binary\n- `MIMALLOC`: (experimental, Linux-only) replace musl's mallocng by [mimalloc](https://github.com/microsoft/mimalloc) for improved performance. We only recommend using this for musl targeting builds, for glibc prefer disabling this option and using [`LD_PRELOAD`](https://microsoft.github.io/mimalloc/overrides.html) when you run your binary instead.\n- `RELEASE`: (maintainers only) when set, the resulting binary will be uploaded on GitHub\n\n## Extensions\n\nWith the glibc or macOS-based binaries, you can load PHP extensions dynamically. However, these extensions will have to be compiled with ZTS support.\nSince most package managers do not currently offer ZTS versions of their extensions, you will have to compile them yourself.\n\nFor this, you can build and run the `static-builder-gnu` Docker container, remote into it, and compile the extensions with `./configure --with-php-config=/go/src/app/dist/static-php-cli/buildroot/bin/php-config`.\n\nExample steps for [the Xdebug extension](https://xdebug.org):\n\n```console\ndocker build -t gnu-ext -f static-builder-gnu.Dockerfile --build-arg FRANKENPHP_VERSION=1.0 .\ndocker create --name static-builder-gnu -it gnu-ext /bin/sh\ndocker start static-builder-gnu\ndocker exec -it static-builder-gnu /bin/sh\ncd /go/src/app/dist/static-php-cli/buildroot/bin\ngit clone https://github.com/xdebug/xdebug.git && cd xdebug\nsource scl_source enable devtoolset-10\n../phpize\n./configure --with-php-config=/go/src/app/dist/static-php-cli/buildroot/bin/php-config\nmake\nexit\ndocker cp static-builder-gnu:/go/src/app/dist/static-php-cli/buildroot/bin/xdebug/modules/xdebug.so xdebug-zts.so\ndocker cp static-builder-gnu:/go/src/app/dist/frankenphp-linux-$(uname -m) ./frankenphp\ndocker stop static-builder-gnu\ndocker rm static-builder-gnu\ndocker rmi gnu-ext\n```\n\nThis will have created `frankenphp` and `xdebug-zts.so` in the current directory.\nIf you move the `xdebug-zts.so` into your extension directory, add `zend_extension=xdebug-zts.so` to your php.ini and run FrankenPHP, it will load Xdebug.\n"
  },
  {
    "path": "docs/tr/CONTRIBUTING.md",
    "content": "# Katkıda Bulunmak\n\n## PHP Derleme\n\n### Docker ile (Linux)\n\nGeliştirme Ortamı için Docker İmajını Oluşturun:\n\n```console\ndocker build -t frankenphp-dev -f dev.Dockerfile .\ndocker run --cap-add=SYS_PTRACE --security-opt seccomp=unconfined -p 8080:8080 -p 443:443 -p 443:443/udp -v $PWD:/go/src/app -it frankenphp-dev\n```\n\nİmaj genel geliştirme araçlarını (Go, GDB, Valgrind, Neovim...) içerir ve aşağıdaki php ayar konumlarını kullanır\n\n- php.ini: `/etc/frankenphp/php.ini` Varsayılan olarak geliştirme ön ayarlarına sahip bir php.ini dosyası sağlanır.\n- ek yapılandırma dosyaları: `/etc/frankenphp/php.d/*.ini`\n- php uzantıları: `/usr/lib/frankenphp/modules/`\n\nDocker sürümünüz 23.0'dan düşükse, derleme dockerignore [pattern issue](https://github.com/moby/moby/pull/42676) nedeniyle başarısız olacaktır. Dizinleri `.dockerignore` dosyasına ekleyin.\n\n```patch\n !testdata/*.php\n !testdata/*.txt\n+!caddy\n+!internal\n```\n\n### Docker olmadan (Linux ve macOS)\n\n[Kaynaklardan derlemek için talimatları izleyin](https://frankenphp.dev/docs/compile/) ve `--debug` yapılandırma seçeneğini geçirin.\n\n## Test senaryolarını çalıştırma\n\n```console\ngo test -race -v ./...\n```\n\n## Caddy modülü\n\nFrankenPHP Caddy modülü ile Caddy'yi oluşturun:\n\n```console\ncd caddy/frankenphp/\ngo build\ncd ../../\n```\n\nCaddy'yi FrankenPHP Caddy modülü ile çalıştırın:\n\n```console\ncd testdata/\n../caddy/frankenphp/frankenphp run\n```\n\nSunucu `127.0.0.1:8080` adresini dinliyor:\n\n```console\ncurl -vk https://localhost/phpinfo.php\n```\n\n## Minimal test sunucusu\n\nMinimal test sunucusunu oluşturun:\n\n```console\ncd internal/testserver/\ngo build\ncd ../../\n```\n\nTest sunucusunu çalıştırın:\n\n```console\ncd testdata/\n../internal/testserver/testserver\n```\n\nSunucu `127.0.0.1:8080` adresini dinliyor:\n\n```console\ncurl -v http://127.0.0.1:8080/phpinfo.php\n```\n\n## Docker İmajlarını Yerel Olarak Oluşturma\n\nBake (pişirme) planını yazdırın:\n\n```console\ndocker buildx bake -f docker-bake.hcl --print\n```\n\nYerel olarak amd64 için FrankenPHP görüntüleri oluşturun:\n\n```console\ndocker buildx bake -f docker-bake.hcl --pull --load --set \"*.platform=linux/amd64\"\n```\n\nYerel olarak arm64 için FrankenPHP görüntüleri oluşturun:\n\n```console\ndocker buildx bake -f docker-bake.hcl --pull --load --set \"*.platform=linux/arm64\"\n```\n\nFrankenPHP imajlarını arm64 ve amd64 için sıfırdan oluşturun ve Docker Hub'a gönderin:\n\n```console\ndocker buildx bake -f docker-bake.hcl --pull --no-cache --push\n```\n\n## Statik Derlemelerle Segmentasyon Hatalarında Hata Ayıklama\n\n1. FrankenPHP binary dosyasının hata ayıklama sürümünü GitHub'dan indirin veya hata ayıklama seçeneklerini kullanarak özel statik derlemenizi oluşturun:\n\n   ```console\n   docker buildx bake \\\n       --load \\\n       --set static-builder.args.DEBUG_SYMBOLS=1 \\\n       --set \"static-builder.platform=linux/amd64\" \\\n       static-builder\n   docker cp $(docker create --name static-builder-musl dunglas/frankenphp:static-builder-musl):/go/src/app/dist/frankenphp-linux-$(uname -m) frankenphp\n   ```\n\n2. Mevcut `frankenphp` sürümünüzü hata ayıklama FrankenPHP çalıştırılabilir dosyasıyla değiştirin\n3. FrankenPHP'yi her zamanki gibi başlatın (alternatif olarak FrankenPHP'yi doğrudan GDB ile başlatabilirsiniz: `gdb --args frankenphp run`)\n4. GDB ile sürece bağlanın:\n\n   ```console\n   gdb -p `pidof frankenphp`\n   ```\n\n5. Gerekirse, GDB kabuğuna `continue` yazın\n6. FrankenPHP'nin çökmesini sağlayın\n7. GDB kabuğuna `bt` yazın\n8. Çıktıyı kopyalayın\n\n## GitHub Eylemlerinde Segmentasyon Hatalarında Hata Ayıklama\n\n1. `.github/workflows/tests.yml` dosyasını açın\n2. PHP hata ayıklama seçeneklerini etkinleştirin\n\n   ```patch\n       - uses: shivammathur/setup-php@v2\n         # ...\n         env:\n           phpts: ts\n   +       debug: true\n   ```\n\n3. Konteynere bağlanmak için `tmate`i etkinleştirin\n\n   ```patch\n       -\n         name: Set CGO flags\n         run: echo \"CGO_CFLAGS=$(php-config --includes)\" >> \"$GITHUB_ENV\"\n   +   -\n   +     run: |\n   +       sudo apt install gdb\n   +       mkdir -p /home/runner/.config/gdb/\n   +       printf \"set auto-load safe-path /\\nhandle SIG34 nostop noprint pass\" > /home/runner/.config/gdb/gdbinit\n   +   -\n   +     uses: mxschmitt/action-tmate@v3\n   ```\n\n4. Konteynere bağlanın\n5. `frankenphp.go` dosyasını açın\n6. `cgosymbolizer`'ı etkinleştirin\n\n   ```patch\n   -\t//_ \"github.com/ianlancetaylor/cgosymbolizer\"\n   +\t_ \"github.com/ianlancetaylor/cgosymbolizer\"\n   ```\n\n7. Modülü indirin: `go get`\n8. Konteynerde GDB ve benzerlerini kullanabilirsiniz:\n\n   ```console\n   go test -c -ldflags=-w\n   gdb --args frankenphp.test -test.run ^MyTest$\n   ```\n\n9. Hata düzeltildiğinde, tüm bu değişiklikleri geri alın\n\n## Misc Dev Resources\n\n- [uWSGI içine PHP gömme](https://github.com/unbit/uwsgi/blob/master/plugins/php/php_plugin.c)\n- [NGINX Unit'te PHP gömme](https://github.com/nginx/unit/blob/master/src/nxt_php_sapi.c)\n- [Go (go-php) içinde PHP gömme](https://github.com/deuill/go-php)\n- [Go'da PHP gömme (GoEmPHP)](https://github.com/mikespook/goemphp)\n- [C++'da PHP gömme](https://gist.github.com/paresy/3cbd4c6a469511ac7479aa0e7c42fea7)\n- [Sara Golemon tarafından PHP'yi Genişletme ve Yerleştirme](https://books.google.fr/books?id=zMbGvK17_tYC&pg=PA254&lpg=PA254#v=onepage&q&f=false)\n- [TSRMLS_CC de neyin nesi?](http://blog.golemon.com/2006/06/what-heck-is-tsrmlscc-anyway.html)\n- [Mac'te PHP gömme](https://gist.github.com/jonnywang/61427ffc0e8dde74fff40f479d147db4)\n- [SDL bağları](https://pkg.go.dev/github.com/veandco/go-sdl2@v0.4.21/sdl#Main)\n\n## Docker ile İlgili Kaynaklar\n\n- [Pişirme (bake) dosya tanımı](https://docs.docker.com/build/customize/bake/file-definition/)\n- [`docker buildx build`](https://docs.docker.com/engine/reference/commandline/buildx_build/)\n\n## Faydalı Komut\n\n```console\napk add strace util-linux gdb\nstrace -e 'trace=!futex,epoll_ctl,epoll_pwait,tgkill,rt_sigreturn' -p 1\n```\n"
  },
  {
    "path": "docs/tr/README.md",
    "content": "# FrankenPHP: PHP için Modern Uygulama Sunucusu\n\n<h1 align=\"center\"><a href=\"https://frankenphp.dev\"><img src=\"../../frankenphp.png\" alt=\"FrankenPHP\" width=\"600\"></a></h1>\n\nFrankenPHP, [Caddy](https://caddyserver.com/) web sunucusunun üzerine inşa edilmiş PHP için modern bir uygulama sunucusudur.\n\nFrankenPHP, çarpıcı özellikleri sayesinde PHP uygulamalarınıza süper güçler kazandırır: [Early Hints\\*](https://frankenphp.dev/docs/early-hints/), [worker modu](https://frankenphp.dev/docs/worker/), [real-time yetenekleri](https://frankenphp.dev/docs/mercure/), otomatik HTTPS, HTTP/2 ve HTTP/3 desteği...\n\nFrankenPHP herhangi bir PHP uygulaması ile çalışır ve worker modu ile resmi entegrasyonları sayesinde Laravel ve Symfony projelerinizi her zamankinden daha performanslı hale getirir.\n\nFrankenPHP, PHP'yi `net/http` kullanarak herhangi bir uygulamaya yerleştirmek için bağımsız bir Go kütüphanesi olarak da kullanılabilir.\n\n[_Frankenphp.dev_](https://frankenphp.dev) adresinden ve bu slayt üzerinden daha fazlasını öğrenin:\n\n<a href=\"https://dunglas.dev/2022/10/frankenphp-the-modern-php-app-server-written-in-go/\"><img src=\"https://dunglas.dev/wp-content/uploads/2022/10/frankenphp.png\" alt=\"Slides\" width=\"600\"></a>\n\n## Başlarken\n\nWindows üzerinde FrankenPHP çalıştırmak için [WSL](https://learn.microsoft.com/windows/wsl/) kullanın.\n\n### Kurulum Betiği\n\nPlatformunuza uygun sürümü otomatik olarak kurmak için bu satırı terminalinize kopyalayabilirsiniz:\n\n```console\ncurl https://frankenphp.dev/install.sh | sh\n```\n\n### Binary Çıktısı\n\nDocker kullanmayı tercih etmiyorsanız, Linux ve macOS için geliştirme amaçlı bağımsız (statik) FrankenPHP binary dosyaları sağlıyoruz;\n[PHP 8.4](https://www.php.net/releases/8.4/en.php) ve en popüler PHP eklentilerinin çoğu dahildir.\n\n[FrankenPHP'yi indirin](https://github.com/php/frankenphp/releases)\n\n**Eklenti kurulumu:** Yaygın eklentiler paketle birlikte gelir. Daha fazla eklenti yüklemek mümkün değildir.\n\n### rpm Paketleri\n\nBakımcılarımız `dnf` kullanan tüm sistemler için rpm paketleri sunuyor. Kurulum için:\n\n```console\nsudo dnf install https://rpm.henderkes.com/static-php-1-0.noarch.rpm\nsudo dnf module enable php-zts:static-8.4 # 8.2-8.5 mevcut\nsudo dnf install frankenphp\n```\n\n**Eklenti kurulumu:** `sudo dnf install php-zts-<extension>`\n\nVarsayılan olarak mevcut olmayan eklentiler için [PIE](https://github.com/php/pie) kullanın:\n\n```console\nsudo dnf install pie-zts\nsudo pie-zts install asgrim/example-pie-extension\n```\n\n### deb Paketleri\n\nBakımcılarımız `apt` kullanan tüm sistemler için deb paketleri sunuyor. Kurulum için:\n\n```console\nsudo curl -fsSL https://key.henderkes.com/static-php.gpg -o /usr/share/keyrings/static-php.gpg && \\\necho \"deb [signed-by=/usr/share/keyrings/static-php.gpg] https://deb.henderkes.com/ stable main\" | sudo tee /etc/apt/sources.list.d/static-php.list && \\\nsudo apt update\nsudo apt install frankenphp\n```\n\n**Eklenti kurulumu:** `sudo apt install php-zts-<extension>`\n\nVarsayılan olarak mevcut olmayan eklentiler için [PIE](https://github.com/php/pie) kullanın:\n\n```console\nsudo apt install pie-zts\nsudo pie-zts install asgrim/example-pie-extension\n```\n\n### Docker\n\n```console\ndocker run -v $PWD:/app/public \\\n    -p 80:80 -p 443:443 -p 443:443/udp \\\n    dunglas/frankenphp\n```\n\n`https://localhost` adresine gidin ve keyfini çıkarın!\n\n> [!TIP]\n>\n> `https://127.0.0.1` kullanmaya çalışmayın. `https://localhost` kullanın ve kendinden imzalı sertifikayı kabul edin.\n> Kullanılacak alan adını değiştirmek için [`SERVER_NAME` ortam değişkenini](https://frankenphp.dev/tr/docs/config#ortam-değişkenleri) kullanın.\n\n### Homebrew\n\nFrankenPHP, macOS ve Linux için [Homebrew](https://brew.sh) paketi olarak da mevcuttur.\n\n```console\nbrew install dunglas/frankenphp/frankenphp\n```\n\n**Eklenti kurulumu:** [PIE](https://github.com/php/pie) kullanın.\n\n### Kullanım\n\nGeçerli dizinin içeriğini sunmak için çalıştırın:\n\n```console\nfrankenphp php-server\n```\n\nKomut satırı betiklerini şu şekilde çalıştırabilirsiniz:\n\n```console\nfrankenphp php-cli /path/to/your/script.php\n```\n\ndeb ve rpm paketleri için systemd servisini de başlatabilirsiniz:\n\n```console\nsudo systemctl start frankenphp\n```\n\n## Docs\n\n- [Worker modu](worker.md)\n- [Early Hints desteği (103 HTTP durum kodu)](early-hints.md)\n- [Real-time](mercure.md)\n- [Konfigürasyon](config.md)\n- [Docker imajları](docker.md)\n- [Production'a dağıtım](production.md)\n- [**Bağımsız** kendiliğinden çalıştırılabilir PHP uygulamaları oluşturma](embed.md)\n- [Statik binary'leri oluşturma](static.md)\n- [Kaynak dosyalarından derleme](config.md)\n- [Laravel entegrasyonu](laravel.md)\n- [Bilinen sorunlar](known-issues.md)\n- [Demo uygulama (Symfony) ve kıyaslamalar](https://github.com/dunglas/frankenphp-demo)\n- [Go kütüphane dokümantasonu](https://pkg.go.dev/github.com/dunglas/frankenphp)\n- [Katkıda bulunma ve hata ayıklama](CONTRIBUTING.md)\n\n## Örnekler ve İskeletler\n\n- [Symfony](https://github.com/dunglas/symfony-docker)\n- [API Platform](https://api-platform.com/docs/distribution/)\n- [Laravel](https://frankenphp.dev/docs/laravel/)\n- [Sulu](https://sulu.io/blog/running-sulu-with-frankenphp)\n- [WordPress](https://github.com/StephenMiracle/frankenwp)\n- [Drupal](https://github.com/dunglas/frankenphp-drupal)\n- [Joomla](https://github.com/alexandreelise/frankenphp-joomla)\n- [TYPO3](https://github.com/ochorocho/franken-typo3)\n- [Magento2](https://github.com/ekino/frankenphp-magento2)\n"
  },
  {
    "path": "docs/tr/compile.md",
    "content": "# Kaynak Kodlardan Derleme\n\nBu doküman, PHP'yi dinamik bir kütüphane olarak yükleyecek bir FrankenPHP yapısının nasıl oluşturulacağını açıklamaktadır.\nÖnerilen yöntem bu şekildedir.\n\nAlternatif olarak, [statik yapılar oluşturma](static.md) da mümkündür.\n\n## PHP'yi yükleyin\n\nFrankenPHP, PHP 8.2 ve üstü ile uyumludur.\n\nİlk olarak, [PHP'nin kaynaklarını edinin](https://www.php.net/downloads.php) ve bunları çıkarın:\n\n```console\ntar xf php-*\ncd php-*/\n```\n\nArdından, PHP'yi platformunuz için yapılandırın.\n\nBu şekilde yapılandırma gereklidir, ancak başka opsiyonlar da ekleyebilirsiniz (örn. ekstra uzantılar)\nİhtiyaç halinde.\n\n### Linux\n\n```console\n./configure \\\n    --enable-embed \\\n    --enable-zts \\\n    --disable-zend-signals \\\n    --enable-zend-max-execution-timers\n```\n\n### Mac\n\nYüklemek için [Homebrew](https://brew.sh/) paket yöneticisini kullanın\n`libiconv`, `bison`, `re2c` ve `pkg-config`:\n\n```console\nbrew install libiconv bison re2c pkg-config\necho 'export PATH=\"/opt/homebrew/opt/bison/bin:$PATH\"' >> ~/.zshrc\n```\n\nArdından yapılandırma betiğini çalıştırın:\n\n```console\n./configure \\\n    --enable-embed=static \\\n    --enable-zts \\\n    --disable-zend-signals \\\n    --disable-opcache-jit \\\n    --enable-static \\\n    --enable-shared=no \\\n    --with-iconv=/opt/homebrew/opt/libiconv/\n```\n\n## PHP Derleyin\n\nSon olarak, PHP'yi derleyin ve kurun:\n\n```console\nmake -j\"$(getconf _NPROCESSORS_ONLN)\"\nsudo make install\n```\n\n## Go Uygulamasını Derleyin\n\nArtık Go kütüphanesini kullanabilir ve Caddy yapımızı derleyebilirsiniz:\n\n```console\ncurl -L https://github.com/php/frankenphp/archive/refs/heads/main.tar.gz | tar xz\ncd frankenphp-main/caddy/frankenphp\nCGO_CFLAGS=$(php-config --includes) CGO_LDFLAGS=\"$(php-config --ldflags) $(php-config --libs)\" go build\n```\n\n### Xcaddy kullanımı\n\nAlternatif olarak, FrankenPHP'yi [özel Caddy modülleri](https://caddyserver.com/docs/modules/) ile derlemek için [xcaddy](https://github.com/caddyserver/xcaddy) kullanın:\n\n```console\nCGO_ENABLED=1 \\\nXCADDY_GO_BUILD_FLAGS=\"-ldflags '-w -s'\" \\\nxcaddy build \\\n    --output frankenphp \\\n    --with github.com/dunglas/frankenphp/caddy \\\n    --with github.com/dunglas/caddy-cbrotli \\\n    --with github.com/dunglas/mercure/caddy \\\n    --with github.com/dunglas/vulcain/caddy\n    # Add extra Caddy modules here\n```\n\n> [!TIP]\n>\n> Eğer musl libc (Alpine Linux'ta varsayılan) ve Symfony kullanıyorsanız,\n> varsayılan yığın boyutunu artırmanız gerekebilir.\n> Aksi takdirde, şu tarz hatalar alabilirsiniz `PHP Fatal error: Maximum call stack size of 83360 bytes reached during compilation. Try splitting expression`\n>\n> Bunu yapmak için, `XCADDY_GO_BUILD_FLAGS` ortam değişkenini bu şekilde değiştirin\n> `XCADDY_GO_BUILD_FLAGS=$'-ldflags \"-w -s -extldflags \\'-Wl,-z,stack-size=0x80000\\'\"'`\n> (yığın boyutunun değerini uygulamanızın ihtiyaçlarına göre değiştirin).\n"
  },
  {
    "path": "docs/tr/config.md",
    "content": "# Konfigürasyon\n\nFrankenPHP, Caddy'nin yanı sıra [Mercure](mercure.md) ve [Vulcain](https://vulcain.rocks) modülleri [Caddy tarafından desteklenen formatlar](https://caddyserver.com/docs/getting-started#your-first-config) kullanılarak yapılandırılabilir.\n\nEn yaygın format, basit, insan tarafından okunabilir bir metin formatı olan `Caddyfile`'dır. Varsayılan olarak, FrankenPHP mevcut dizinde bir `Caddyfile` arar. Özel bir yol belirtmek için `-c` veya `--config` seçeneğini kullanabilirsiniz.\n\nBir PHP uygulamasını sunmak için minimal bir `Caddyfile` aşağıda gösterilmiştir:\n\n```caddyfile\n# Yanıt verilecek ana bilgisayar adı\nlocalhost\n\n# İsteğe bağlı olarak, dosyaların sunulacağı dizin, aksi takdirde mevcut dizin varsayılan olarak kullanılır\n#root public/\nphp_server\n```\n\nDaha fazla özellik sağlayan ve kullanışlı ortam değişkenleri sunan daha gelişmiş bir `Caddyfile`, [FrankenPHP deposunda](https://github.com/php/frankenphp/blob/main/caddy/frankenphp/Caddyfile) ve Docker imajlarıyla birlikte sağlanır.\n\nPHP'nin kendisi [bir `php.ini` dosyası kullanılarak yapılandırılabilir](https://www.php.net/manual/en/configuration.file.php).\n\nKurulum yönteminize bağlı olarak, FrankenPHP ve PHP yorumlayıcısı, aşağıda açıklanan konumlardaki yapılandırma dosyalarını arayacaktır.\n\n## Docker\n\nFrankenPHP:\n\n- `/etc/frankenphp/Caddyfile`: ana yapılandırma dosyası\n- `/etc/frankenphp/Caddyfile.d/*.caddyfile`: otomatik olarak yüklenen ek yapılandırma dosyaları\n\nPHP:\n\n- `php.ini`: `/usr/local/etc/php/php.ini` (varsayılan olarak bir `php.ini` sağlanmaz)\n- ek yapılandırma dosyaları: `/usr/local/etc/php/conf.d/*.ini`\n- PHP uzantıları: `/usr/local/lib/php/extensions/no-debug-zts-<YYYYMMDD>/`\n- PHP projesi tarafından sağlanan resmi bir şablonu kopyalamalısınız:\n\n```dockerfile\nFROM dunglas/frankenphp\n\n# Production:\nRUN cp $PHP_INI_DIR/php.ini-production $PHP_INI_DIR/php.ini\n\n# Veya development:\nRUN cp $PHP_INI_DIR/php.ini-development $PHP_INI_DIR/php.ini\n```\n\n## RPM ve Debian paketleri\n\nFrankenPHP:\n\n- `/etc/frankenphp/Caddyfile`: ana yapılandırma dosyası\n- `/etc/frankenphp/Caddyfile.d/*.caddyfile`: otomatik olarak yüklenen ek yapılandırma dosyaları\n\nPHP:\n\n- `php.ini`: `/etc/php-zts/php.ini` (varsayılan olarak üretim ön ayarlarına sahip bir `php.ini` dosyası sağlanır)\n- ek yapılandırma dosyaları: `/etc/php-zts/conf.d/*.ini`\n\n## Statik ikili\n\nFrankenPHP:\n\n- Mevcut çalışma dizininde: `Caddyfile`\n\nPHP:\n\n- `php.ini`: `frankenphp run` veya `frankenphp php-server` komutunun çalıştırıldığı dizin, ardından `/etc/frankenphp/php.ini`\n- ek yapılandırma dosyaları: `/etc/frankenphp/php.d/*.ini`\n- PHP uzantıları: yüklenemez, bunları doğrudan ikili dosyaya dahil edin\n- [PHP kaynak kodu](https://github.com/php/php-src/) ile birlikte verilen `php.ini-production` veya `php.ini-development` dosyalarından birini kopyalayın.\n\n## Caddyfile Konfigürasyonu\n\nPHP uygulamanızı sunmak için site blokları içinde `php_server` veya `php` [HTTP yönergeleri](https://caddyserver.com/docs/caddyfile/concepts#directives) kullanılabilir.\n\nMinimal örnek:\n\n```caddyfile\nlocalhost {\n\t# Sıkıştırmayı etkinleştir (isteğe bağlı)\n\tencode zstd br gzip\n\t# Geçerli dizindeki PHP dosyalarını çalıştırın ve varlıkları sunun\n\tphp_server\n}\n```\n\nAyrıca, FrankenPHP'yi [global seçenek](https://caddyserver.com/docs/caddyfile/concepts#global-options) olan `frankenphp` kullanarak açıkça yapılandırabilirsiniz:\n\n```caddyfile\n{\n\tfrankenphp {\n\t\tnum_threads <num_threads> # Başlatılacak PHP iş parçacığı sayısını ayarlar. Varsayılan: Mevcut CPU'ların 2 katı.\n\t\tmax_threads <num_threads> # Çalışma zamanında başlatılabilecek ek PHP iş parçacığı sayısını sınırlar. Varsayılan: num_threads. 'auto' olarak ayarlanabilir.\n\t\tmax_wait_time <duration> # Bir isteğin boş bir PHP iş parçacığı bekleme süresinin zaman aşımına uğramadan önceki maksimum süresini ayarlar. Varsayılan: devre dışı.\n\t\tmax_idle_time <duration> # Otomatik ölçeklenen bir iş parçacığının devre dışı bırakılmadan önce ne kadar süre boş kalabileceğini ayarlar. Varsayılan: 5s.\n\t\tphp_ini <key> <value> # Bir php.ini yönergesi ayarlar. Birden fazla yönerge ayarlamak için birden fazla kez kullanılabilir.\n\t\tworker {\n\t\t\tfile <path> # Çalışan komut dosyasının yolunu ayarlar.\n\t\t\tnum <num> # Başlatılacak PHP iş parçacığı sayısını ayarlar, varsayılan değer mevcut CPU'ların 2 katıdır.\n\t\t\tenv <key> <value> # Ek bir ortam değişkenini verilen değere ayarlar. Birden fazla ortam değişkeni için birden fazla kez belirtilebilir.\n\t\t\twatch <path> # Dosya değişikliklerini izlemek için yolu ayarlar. Birden fazla yol için birden fazla kez belirtilebilir.\n\t\t\tname <name> # İşçinin adını ayarlar, günlüklerde ve metriklerde kullanılır. Varsayılan: işçi dosyasının mutlak yolu.\n\t\t\tmax_consecutive_failures <num> # İşçinin sağlıksız kabul edilmeden önceki maksimum ardışık hata sayısını ayarlar, -1 işçinin her zaman yeniden başlayacağı anlamına gelir. Varsayılan: 6.\n\t\t}\n\t}\n}\n\n# ...\n```\n\nAlternatif olarak, `worker` seçeneğinin tek satırlık kısa formunu kullanabilirsiniz:\n\n```caddyfile\n{\n\tfrankenphp {\n\t\tworker <file> <num>\n\t}\n}\n\n# ...\n```\n\nAynı sunucuda birden fazla uygulamaya hizmet veriyorsanız birden fazla işçi de tanımlayabilirsiniz:\n\n```caddyfile\napp.example.com {\n    root /path/to/app/public\n\tphp_server {\n\t\troot /path/to/app/public # daha iyi önbelleğe almayı sağlar\n\t\tworker index.php <num>\n\t}\n}\n\nother.example.com {\n    root /path/to/other/public\n\tphp_server {\n\t\troot /path/to/other/public\n\t\tworker index.php <num>\n\t}\n}\n\n# ...\n```\n\nGenellikle ihtiyacınız olan şey `php_server` yönergesini kullanmaktır,\nancak tam kontrole ihtiyacınız varsa, daha düşük seviyeli `php` yönergesini kullanabilirsiniz.\n`php` yönergesi, önce bir PHP dosyası olup olmadığını kontrol etmek yerine tüm girdiyi PHP'ye iletir. Daha fazla bilgiyi [performans sayfasında](performance.md#try_files) okuyun.\n\n`php_server` yönergesini kullanmak bu yapılandırmayla aynıdır:\n\n```caddyfile\nroute {\n\t# Dizin istekleri için sondaki eğik çizgiyi ekleyin\n\t@canonicalPath {\n\t\tfile {path}/index.php\n\t\tnot path */\n\t}\n\tredir @canonicalPath {path}/ 308\n\t# İstenen dosya mevcut değilse, dizin dosyalarını deneyin\n\t@indexFiles file {\n\t\ttry_files {path} {path}/index.php index.php\n\t\tsplit_path .php\n\t}\n\trewrite @indexFiles {http.matchers.file.relative}\n\t# FrankenPHP!\n\t@phpFiles path *.php\n\tphp @phpFiles\n\tfile_server\n}\n```\n\n`php_server` ve `php` yönergeleri aşağıdaki seçeneklere sahiptir:\n\n```caddyfile\nphp_server [<matcher>] {\n\troot <directory> # Sitenin kök klasörünü ayarlar. Varsayılan: `root` yönergesi.\n\tsplit_path <delim...> # URI'yi iki parçaya bölmek için alt dizgeleri ayarlar. İlk eşleşen alt dizge \"yol bilgisini\" yoldan ayırmak için kullanılır. İlk parça eşleşen alt dizeyle sonlandırılır ve gerçek kaynak (CGI betiği) adı olarak kabul edilir. İkinci parça betiğin kullanması için PATH_INFO olarak ayarlanacaktır. Varsayılan: `.php`\n\tresolve_root_symlink false # Varsa, sembolik bir bağlantıyı değerlendirerek `root` dizininin gerçek değerine çözümlenmesini devre dışı bırakır (varsayılan olarak etkindir).\n\tenv <key> <value> # Ek bir ortam değişkenini verilen değere ayarlar. Birden fazla ortam değişkeni için birden fazla kez belirtilebilir.\n\tfile_server off # Yerleşik file_server yönergesini devre dışı bırakır.\n\tworker { # Bu sunucuya özgü bir worker oluşturur. Birden fazla worker için birden fazla kez belirtilebilir.\n\t\tfile <path> # Worker betiğinin yolunu ayarlar, php_server köküne göre göreceli olabilir\n\t\tnum <num> # Başlatılacak PHP iş parçacığı sayısını ayarlar, varsayılan değer mevcut CPU'ların 2 katıdır.\n\t\tname <name> # Worker için günlüklerde ve metriklerde kullanılan bir ad ayarlar. Varsayılan: worker dosyasının mutlak yolu. Bir php_server bloğunda tanımlandığında her zaman m# ile başlar.\n\t\twatch <path> # Dosya değişikliklerini izlemek için yolu ayarlar. Birden fazla yol için birden fazla kez belirtilebilir.\n\t\tenv <key> <value> # Ek bir ortam değişkenini verilen değere ayarlar. Birden fazla ortam değişkeni için birden fazla kez belirtilebilir. Bu worker için ortam değişkenleri ayrıca php_server üst öğesinden devralınır, ancak burada geçersiz kılınabilir.\n\t\tmatch <path> # İşçiyi bir yol desenine eşleştirir. try_files'ı geçersiz kılar ve yalnızca php_server yönergesinde kullanılabilir.\n\t}\n\tworker <other_file> <num> # Global frankenphp bloğundaki gibi kısa formu da kullanabilirsiniz.\n}\n```\n\n### Dosya Değişikliklerini İzleme\n\nWorkers yalnızca uygulamanızı bir kez başlatır ve bellekte tutar, bu nedenle PHP dosyalarınızdaki herhangi bir değişiklik hemen yansımaz.\n\nBunun yerine işçiler, `watch` yönergesi aracılığıyla dosya değişikliklerinde yeniden başlatılabilir. Bu, geliştirme ortamları için kullanışlıdır.\n\n```caddyfile\n{\n\tfrankenphp {\n\t\tworker {\n\t\t\tfile  /path/to/app/public/worker.php\n\t\t\twatch\n\t\t}\n\t}\n}\n```\n\nBu özellik genellikle [hot reload](hot-reload.md) ile birlikte kullanılır.\n\nEğer `watch` dizini belirtilmezse, FrankenPHP sürecinin başlatıldığı dizin ve alt dizinlerdeki tüm `.env`, `.php`, `.twig`, `.yaml` ve `.yml` dosyalarını izleyen `./**/*.{env,php,twig,yaml,yml}` değerine geri döner. Bunun yerine, bir veya daha fazla dizini [kabuk dosya adı deseni](https://pkg.go.dev/path/filepath#Match) aracılığıyla da belirtebilirsiniz:\n\n```caddyfile\n{\n\tfrankenphp {\n\t\tworker {\n\t\t\tfile  /path/to/app/public/worker.php\n\t\t\twatch /path/to/app # /path/to/app'ın tüm alt dizinlerindeki tüm dosyaları izler\n\t\t\twatch /path/to/app/*.php # /path/to/app'da .php ile biten dosyaları izler\n\t\t\twatch /path/to/app/**/*.php # /path/to/app ve alt dizinlerindeki PHP dosyalarını izler\n\t\t\twatch /path/to/app/**/*.{php,twig} # /path/to/app ve alt dizinlerindeki PHP ve Twig dosyalarını izler\n\t\t}\n\t}\n}\n```\n\n- `**` deseni, özyinelemeli izlemeyi belirtir\n- Dizinler göreceli de olabilir (FrankenPHP sürecinin başlatıldığı yere göre)\n- Birden fazla işçi tanımladıysanız, bir dosya değiştiğinde hepsi yeniden başlatılacaktır.\n- Çalışma zamanında oluşturulan dosyaları (günlükler gibi) izlerken dikkatli olun, zira bunlar istenmeyen işçi yeniden başlatmalarına neden olabilir.\n\nDosya izleyici [e-dant/watcher](https://github.com/e-dant/watcher) üzerine kuruludur.\n\n## İşçiyi Yola Eşleştirme\n\nGeleneksel PHP uygulamalarında, betikler her zaman public dizininde bulunur. Bu, diğer tüm PHP betikleri gibi ele alınan işçi betikleri için de geçerlidir. İşçi betiğini public dizininin dışına koymak isterseniz, bunu `match` yönergesi aracılığıyla yapabilirsiniz.\n\n`match` yönergesi, yalnızca `php_server` ve `php` içinde bulunan `try_files`'a optimize edilmiş bir alternatiftir. Aşağıdaki örnek, varsa her zaman public dizinindeki bir dosyayı sunacak, aksi takdirde isteği yol desenine uyan işçiye iletecektir.\n\n```caddyfile\n{\n\tfrankenphp {\n\t\tphp_server {\n\t\t\tworker {\n\t\t\t\tfile /path/to/worker.php # dosya public yolunun dışında olabilir\n\t\t\t\tmatch /api/* # /api/ ile başlayan tüm istekler bu worker tarafından işlenecektir\n\t\t\t}\n\t\t}\n\t}\n}\n```\n\n## Ortam Değişkenleri\n\nAşağıdaki ortam değişkenleri `Caddyfile` içinde değişiklik yapmadan Caddy yönergelerini entegre etmek için kullanılabilir:\n\n- `SERVER_NAME`: [dinlenecek adresleri](https://caddyserver.com/docs/caddyfile/concepts#addresses) değiştirir, sağlanan ana bilgisayar adları oluşturulan TLS sertifikası için de kullanılacaktır\n- `SERVER_ROOT`: sitenin kök dizinini değiştirir, varsayılan olarak `public/`dir\n- `CADDY_GLOBAL_OPTIONS`: [global seçenekleri](https://caddyserver.com/docs/caddyfile/options) entegre eder\n- `FRANKENPHP_CONFIG`: `frankenphp` yönergesi altına yapılandırma entegre eder\n\nFPM ve CLI SAPI'lerinde olduğu gibi, ortam değişkenleri varsayılan olarak `$_SERVER` süper globalinde gösterilir.\n\n[`variables_order`'a ait PHP yönergesinin](https://www.php.net/manual/en/ini.core.php#ini.variables-order) `S` değeri bu yönergede `E`'nin başka bir yere yerleştirilmesinden bağımsız olarak her zaman `ES` ile eş değerdir.\n\n## PHP konfigürasyonu\n\nEk olarak [PHP yapılandırma dosyalarını](https://www.php.net/manual/en/configuration.file.php#configuration.file.scan) yüklemek için,\n`PHP_INI_SCAN_DIR` ortam değişkeni kullanılabilir.\nAyarlandığında, PHP verilen dizinlerde bulunan `.ini` uzantılı tüm dosyaları yükleyecektir.\n\nPHP yapılandırmasını `Caddyfile` içindeki `php_ini` yönergesini kullanarak da değiştirebilirsiniz:\n\n```caddyfile\n{\n    frankenphp {\n        php_ini memory_limit 256M\n\n        # veya\n\n        php_ini {\n            memory_limit 256M\n            max_execution_time 15\n        }\n    }\n}\n```\n\n### HTTPS'i Devre Dışı Bırakma\n\nVarsayılan olarak, FrankenPHP tüm ana bilgisayar adları için (localhost dahil) HTTPS'i otomatik olarak etkinleştirir. HTTPS'i devre dışı bırakmak isterseniz (örneğin bir geliştirme ortamında), `SERVER_NAME` ortam değişkenini `http://` veya `:80` olarak ayarlayabilirsiniz:\n\nAlternatif olarak, [Caddy belgelerinde](https://caddyserver.com/docs/automatic-https#activation) açıklanan diğer tüm yöntemleri kullanabilirsiniz.\n\nEğer `localhost` ana bilgisayar adı yerine `127.0.0.1` IP adresiyle HTTPS kullanmak isterseniz, lütfen [bilinen sorunlar](known-issues.md#using-https127001-with-docker) bölümünü okuyun.\n\n### Tam Çift Yönlü (HTTP/1)\n\nHTTP/1.x kullanırken, tüm gövde okunmadan önce bir yanıt yazmaya izin vermek için tam çift yönlü modun etkinleştirilmesi istenebilir. (örneğin: [Mercure](mercure.md), WebSocket, Sunucu Tarafından Gönderilen Olaylar vb.)\n\nBu, `Caddyfile`'daki global seçeneklere eklenmesi gereken isteğe bağlı bir yapılandırmadır:\n\n```caddyfile\n{\n  servers {\n    enable_full_duplex\n  }\n}\n```\n\n> [!CAUTION]\n>\n> Bu seçeneği etkinleştirmek, tam çift yönlü desteği olmayan eski HTTP/1.x istemcilerinin kilitlenmesine neden olabilir.\n> Bu, `CADDY_GLOBAL_OPTIONS` ortam yapılandırması kullanılarak da yapılandırılabilir:\n\n```sh\nCADDY_GLOBAL_OPTIONS=\"servers {\n  enable_full_duplex\n}\"\n```\n\nBu ayar hakkında daha fazla bilgiyi [Caddy belgelerinde](https://caddyserver.com/docs/caddyfile/options#enable-full-duplex) bulabilirsiniz.\n\n## Hata Ayıklama Modunu Etkinleştirin\n\nDocker imajını kullanırken, hata ayıklama modunu etkinleştirmek için `CADDY_GLOBAL_OPTIONS` ortam değişkenini `debug` olarak ayarlayın:\n\n```console\ndocker run -v $PWD:/app/public \\\n    -e CADDY_GLOBAL_OPTIONS=debug \\\n    -p 80:80 -p 443:443 -p 443:443/udp \\\n    dunglas/frankenphp\n```\n\n## Shell Completion\n\nFrankenPHP, Bash, Zsh, Fish ve PowerShell için yerleşik kabuk tamamlama desteği sağlar. Bu, tüm komutlar ( `php-server`, `php-cli` ve `extension-init` gibi özel komutlar dahil) ve bunların bayrakları için otomatik tamamlama sağlar.\n\n### Bash\n\nGeçerli kabuk oturumunuzda tamamlamaları yüklemek için:\n\n```console\nsource <(frankenphp completion bash)\n```\n\nHer yeni oturum için tamamlamaları yüklemek için şunu çalıştırın:\n\n**Linux:**\n\n```console\nfrankenphp completion bash > /usr/share/bash-completion/completions/frankenphp\n```\n\n**macOS:**\n\n```console\nfrankenphp completion bash > $(brew --prefix)/share/bash-completion/completions/frankenphp\n```\n\n### Zsh\n\nOrtamınızda kabuk tamamlama zaten etkin değilse, bunu etkinleştirmeniz gerekecektir. Aşağıdakini bir kez çalıştırabilirsiniz:\n\n```console\necho \"autoload -U compinit; compinit\" >> ~/.zshrc\n```\n\nHer oturum için tamamlamaları yüklemek için, bir kez çalıştırın:\n\n```console\nfrankenphp completion zsh > \"${fpath[1]}/_frankenphp\"\n```\n\nBu kurulumun etkili olması için yeni bir kabuk başlatmanız gerekecektir.\n\n### Fish\n\nGeçerli kabuk oturumunuzda tamamlamaları yüklemek için:\n\n```console\nfrankenphp completion fish | source\n```\n\nHer yeni oturum için tamamlamaları yüklemek için, bir kez çalıştırın:\n\n```console\nfrankenphp completion fish > ~/.config/fish/completions/frankenphp.fish\n```\n\n### PowerShell\n\nGeçerli kabuk oturumunuzda tamamlamaları yüklemek için:\n\n```powershell\nfrankenphp completion powershell | Out-String | Invoke-Expression\n```\n\nHer yeni oturum için tamamlamaları yüklemek için, bir kez çalıştırın:\n\n```powershell\nfrankenphp completion powershell | Out-File -FilePath (Join-Path (Split-Path $PROFILE) \"frankenphp.ps1\")\nAdd-Content -Path $PROFILE -Value '. (Join-Path (Split-Path $PROFILE) \"frankenphp.ps1\")'\n```\n\nBu kurulumun etkili olması için yeni bir kabuk başlatmanız gerekecektir.\n\nBu kurulumun etkili olması için yeni bir kabuk başlatmanız gerekecektir.\n"
  },
  {
    "path": "docs/tr/docker.md",
    "content": "# Özel Docker İmajı Oluşturma\n\n[FrankenPHP Docker imajları](https://hub.docker.com/r/dunglas/frankenphp), [resmi PHP imajları](https://hub.docker.com/_/php/) temel alınarak hazırlanmıştır. Popüler mimariler için Debian ve Alpine Linux varyantları sağlanmıştır. Debian varyantları tavsiye edilir.\n\nPHP 8.2, 8.3, 8.4 ve 8.5 için varyantlar sağlanmıştır.\n\nEtiketler şu deseni takip eder: `dunglas/frankenphp:<frankenphp-version>-php<php-version>-<os>`\n\n- `<frankenphp-version>` ve `<php-version>`, sırasıyla FrankenPHP ve PHP'nin ana (örn. `1`), ikincil (örn. `1.2`) ve yama sürümlerine (örn. `1.2.3`) kadar değişen sürüm numaralarıdır.\n- `<os>` ise `trixie` (Debian Trixie için), `bookworm` (Debian Bookworm için) veya `alpine` (Alpine'ın en son kararlı sürümü için) olabilir.\n\n[Etiketlere göz atın](https://hub.docker.com/r/dunglas/frankenphp/tags).\n\n## İmajlar Nasıl Kullanılır\n\nProjenizde bir `Dockerfile` oluşturun:\n\n```dockerfile\nFROM dunglas/frankenphp\n\nCOPY . /app/public\n```\n\nArdından, Docker imajını oluşturmak ve çalıştırmak için bu komutları çalıştırın:\n\n```console\ndocker build -t my-php-app .\ndocker run -it --rm --name my-running-app my-php-app\n```\n\n## Yapılandırma Nasıl Ayarlanır\n\nKolaylık sağlamak için, faydalı ortam değişkenleri içeren [varsayılan bir `Caddyfile`](https://github.com/php/frankenphp/blob/main/caddy/frankenphp/Caddyfile) imajda sağlanmıştır.\n\n## Daha Fazla PHP Eklentisi Nasıl Kurulur\n\n[`docker-php-extension-installer`](https://github.com/mlocati/docker-php-extension-installer) betiği temel imajda sağlanmıştır.\nEk PHP eklentileri eklemek basittir:\n\n```dockerfile\nFROM dunglas/frankenphp\n\n# ek eklentileri buraya ekleyin:\nRUN install-php-extensions \\\n\tpdo_mysql \\\n\tgd \\\n\tintl \\\n\tzip \\\n\topcache\n```\n\n## Daha Fazla Caddy Modülü Nasıl Kurulur\n\nFrankenPHP, Caddy'nin üzerine inşa edilmiştir ve tüm [Caddy modülleri](https://caddyserver.com/docs/modules/) FrankenPHP ile kullanılabilir.\n\nÖzel Caddy modüllerini kurmanın en kolay yolu [xcaddy](https://github.com/caddyserver/xcaddy) kullanmaktır:\n\n```dockerfile\nFROM dunglas/frankenphp:builder AS builder\n\n# xcaddy'yi builder imajına kopyalayın\nCOPY --from=caddy:builder /usr/bin/xcaddy /usr/bin/xcaddy\n\n# CGO, FrankenPHP oluşturmak için etkinleştirilmelidir\nRUN CGO_ENABLED=1 \\\n    XCADDY_SETCAP=1 \\\n    XCADDY_GO_BUILD_FLAGS=\"-ldflags='-w -s' -tags=nobadger,nomysql,nopgx\" \\\n    CGO_CFLAGS=$(php-config --includes) \\\n    CGO_LDFLAGS=\"$(php-config --ldflags) $(php-config --libs)\" \\\n    xcaddy build \\\n        --output /usr/local/bin/frankenphp \\\n        --with github.com/dunglas/frankenphp=./ \\\n        --with github.com/dunglas/frankenphp/caddy=./caddy/ \\\n        --with github.com/dunglas/caddy-cbrotli \\\n        # Mercure ve Vulcain resmi derlemeye dahildir, ancak bunları kaldırmaktan çekinmeyin\n        --with github.com/dunglas/mercure/caddy \\\n        --with github.com/dunglas/vulcain/caddy\n        # Ek Caddy modüllerini buraya ekleyin\n\nFROM dunglas/frankenphp AS runner\n\n# Resmi binary dosyayı özel modüllerinizi içeren binary dosyayla değiştirin\nCOPY --from=builder /usr/local/bin/frankenphp /usr/local/bin/frankenphp\n```\n\nFrankenPHP tarafından sağlanan `builder` imajı `libphp`'nin derlenmiş bir sürümünü içerir.\n[Builder imajları](https://hub.docker.com/r/dunglas/frankenphp/tags?name=builder) hem Debian hem de Alpine için FrankenPHP ve PHP'nin tüm sürümleri için sağlanmıştır.\n\n> [!TIP]\n>\n> Eğer Alpine Linux ve Symfony kullanıyorsanız,\n> [varsayılan yığın boyutunu artırmanız](compile.md#xcaddy-kullanımı) gerekebilir.\n\n## Varsayılan Olarak Worker Modunun Etkinleştirilmesi\n\nFrankenPHP'yi bir worker betiği ile başlatmak için `FRANKENPHP_CONFIG` ortam değişkenini ayarlayın:\n\n```dockerfile\nFROM dunglas/frankenphp\n\n# ...\n\nENV FRANKENPHP_CONFIG=\"worker ./public/index.php\"\n```\n\n## Geliştirme Sürecinde Volume Kullanma\n\nFrankenPHP ile kolayca geliştirme yapmak için, uygulamanın kaynak kodunu içeren dizini ana bilgisayarınızdan Docker konteynerine bir volume olarak bağlayın:\n\n```console\ndocker run -v $PWD:/app/public -p 80:80 -p 443:443 -p 443:443/udp --tty my-php-app\n```\n\n> [!TIP]\n>\n> `--tty` seçeneği JSON günlükleri yerine insan tarafından okunabilir güzel günlüklere sahip olmayı sağlar.\n\nDocker Compose ile:\n\n```yaml\n# compose.yaml\n\nservices:\n  php:\n    image: dunglas/frankenphp\n    # özel bir Dockerfile kullanmak istiyorsanız aşağıdaki satırın yorumunu kaldırın\n    #build: .\n    # bunu bir üretim ortamında çalıştırmak istiyorsanız aşağıdaki satırın yorumunu kaldırın\n    # restart: always\n    ports:\n      - \"80:80\" # HTTP\n      - \"443:443\" # HTTPS\n      - \"443:443/udp\" # HTTP/3\n    volumes:\n      - ./:/app/public\n      - caddy_data:/data\n      - caddy_config:/config\n    # üretimde aşağıdaki satırı yorum satırı yapın; geliştirme ortamında ise güzel, insan tarafından okunabilir günlükler sağlar\n    tty: true\n\n# Caddy sertifikaları ve yapılandırması için gereken volume'ler\nvolumes:\n  caddy_data:\n  caddy_config:\n```\n\n## Root Olmayan Kullanıcı Olarak Çalıştırma\n\nFrankenPHP, Docker'da root olmayan kullanıcı olarak çalışabilir.\n\nİşte bunu yapan örnek bir `Dockerfile`:\n\n```dockerfile\nFROM dunglas/frankenphp\n\nARG USER=appuser\n\nRUN \\\n\t# Alpine tabanlı dağıtımlar için \"adduser -D ${USER}\" kullanın\n\tuseradd ${USER}; \\\n\t# 80 ve 443 numaralı bağlantı noktalarına bağlanmak için ek özellik ekleyin\n\tsetcap CAP_NET_BIND_SERVICE=+eip /usr/local/bin/frankenphp; \\\n\t# /config/caddy ve /data/caddy dosyalarına yazma erişimi verin\n\tchown -R ${USER}:${USER} /config/caddy /data/caddy\n\nUSER ${USER}\n```\n\n### Yetenek Olmadan Çalıştırma\n\nFrankenPHP, root yetkisi olmadan çalışırken bile, web sunucusunu ayrıcalıklı bağlantı noktalarında (80 ve 443) bağlamak için `CAP_NET_BIND_SERVICE` yeteneğine ihtiyaç duyar.\n\nFrankenPHP'yi ayrıcalıklı olmayan bir bağlantı noktasında (1024 ve üzeri) çalıştırırsanız, web sunucusunu root olmayan bir kullanıcı olarak ve herhangi bir yeteneğe ihtiyaç duymadan çalıştırmak mümkündür:\n\n```dockerfile\nFROM dunglas/frankenphp\n\nARG USER=appuser\n\nRUN \\\n\t# Alpine tabanlı dağıtımlar için \"adduser -D ${USER}\" kullanın\n\tuseradd ${USER}; \\\n\t# Varsayılan yeteneği kaldırın\n\tsetcap -r /usr/local/bin/frankenphp; \\\n\t# /config/caddy ve /data/caddy dosyalarına yazma erişimi verin\n\tchown -R ${USER}:${USER} /config/caddy /data/caddy\n\nUSER ${USER}\n```\n\nArdından, ayrıcalıklı olmayan bir bağlantı noktası kullanmak için `SERVER_NAME` ortam değişkenini ayarlayın.\nÖrnek: `:8000`\n\n## Güncellemeler\n\nDocker imajları oluşturulur:\n\n- Yeni bir sürüm etiketlendiğinde\n- Her gün UTC ile sabah 4'te, resmi PHP imajlarının yeni sürümleri mevcutsa\n\n## İmajları Sertleştirme\n\nFrankenPHP Docker imajlarınızın saldırı yüzeyini ve boyutunu daha da azaltmak için, onları [Google distroless](https://github.com/GoogleContainerTools/distroless) veya [Docker hardened](https://www.docker.com/products/hardened-images) bir imaj üzerine inşa etmek de mümkündür.\n\n> [!WARNING]\n> Bu minimal temel imajlar, hata ayıklamayı zorlaştıran bir kabuk veya paket yöneticisi içermez.\n> Bu nedenle, güvenlik yüksek öncelikliyse yalnızca üretim için önerilirler.\n\nEk PHP eklentileri eklerken, bir ara derleme aşamasına ihtiyacınız olacaktır:\n\n```dockerfile\nFROM dunglas/frankenphp AS builder\n\n# Ek PHP eklentilerini buraya ekleyin\nRUN install-php-extensions pdo_mysql pdo_pgsql #...\n\n# frankenphp'nin paylaşılan kütüphanelerini ve kurulu tüm eklentileri geçici bir konuma kopyalayın\n# Bu adımı, frankenphp binary'sinin ve her bir eklenti .so dosyasının ldd çıktısını analiz ederek manuel olarak da yapabilirsiniz\nRUN apt-get update && apt-get install -y libtree && \\\n    EXT_DIR=\"$(php -r 'echo ini_get(\"extension_dir\");')\" && \\\n    FRANKENPHP_BIN=\"$(which frankenphp)\"; \\\n    LIBS_TMP_DIR=\"/tmp/libs\"; \\\n    mkdir -p \"$LIBS_TMP_DIR\"; \\\n    for target in \"$FRANKENPHP_BIN\" $(find \"$EXT_DIR\" -maxdepth 2 -type f -name \"*.so\"); do \\\n        libtree -pv \"$target\" | sed 's/.*── \\(.*\\) \\[.*/\\1/' | grep -v \"^$target\" | while IFS= read -r lib; do \\\n            [ -z \"$lib\" ] && continue; \\\n            base=$(basename \"$lib\"); \\\n            destfile=\"$LIBS_TMP_DIR/$base\"; \\\n            if [ ! -f \"$destfile\" ]; then \\\n                cp \"$lib\" \"$destfile\"; \\\n            fi; \\\n        done; \\\n    done\n\n\n# Distroless debian temel imajı, bunun temel imajla aynı debian sürümü olduğundan emin olun\nFROM gcr.io/distroless/base-debian13\n# Docker hardened imaj alternatifi\n# FROM dhi.io/debian:13\n\n# Uygulamanızın ve Caddyfile'ınızın konteynere kopyalanacak konumu\nARG PATH_TO_APP=\".\"\nARG PATH_TO_CADDYFILE=\"./Caddyfile\"\n\n# Uygulamanızı /app'e kopyalayın\n# Daha fazla sertleştirme için, yalnızca yazılabilir yolların nonroot kullanıcısına ait olduğundan emin olun\nCOPY --chown=nonroot:nonroot \"$PATH_TO_APP\" /app\nCOPY \"$PATH_TO_CADDYFILE\" /etc/caddy/Caddyfile\n\n# frankenphp'yi ve gerekli kütüphaneleri kopyalayın\nCOPY --from=builder /usr/local/bin/frankenphp /usr/local/bin/frankenphp\nCOPY --from=builder /usr/local/lib/php/extensions /usr/local/lib/php/extensions\nCOPY --from=builder /tmp/libs /usr/lib\n\n# php.ini yapılandırma dosyalarını kopyalayın\nCOPY --from=builder /usr/local/etc/php/conf.d /usr/local/etc/php/conf.d\nCOPY --from=builder /usr/local/etc/php/php.ini-production /usr/local/etc/php/php.ini\n\n# Caddy veri dizinleri — salt okunur bir kök dosya sisteminde bile nonroot için yazılabilir olmalıdır\nENV XDG_CONFIG_HOME=/config \\\n    XDG_DATA_HOME=/data\nCOPY --from=builder --chown=nonroot:nonroot /data/caddy /data/caddy\nCOPY --from=builder --chown=nonroot:nonroot /config/caddy /config/caddy\n\nUSER nonroot\n\nWORKDIR /app\n\n# Sağlanan Caddyfile ile frankenphp'yi çalıştırmak için giriş noktası\nENTRYPOINT [\"/usr/local/bin/frankenphp\", \"run\", \"-c\", \"/etc/caddy/Caddyfile\"]\n```\n\n## Geliştirme Sürümleri\n\nGeliştirme sürümleri [`dunglas/frankenphp-dev`](https://hub.docker.com/repository/docker/dunglas/frankenphp-dev) Docker deposunda mevcuttur.\nGitHub deposunun `main` dalına her commit gönderildiğinde yeni bir derleme tetiklenir.\n\n`latest*` etiketleri `main` dalının başına işaret eder.\n`sha-<git-commit-hash>` biçimindeki etiketler de mevcuttur.\n"
  },
  {
    "path": "docs/tr/early-hints.md",
    "content": "# Early Hints\n\nFrankenPHP [103 Early Hints durum kodunu](https://developer.chrome.com/blog/early-hints/) yerel olarak destekler.\nEarly Hints kullanmak web sayfalarınızın yüklenme süresini %30 oranında artırabilir.\n\n```php\n<?php\n\nheader('Link: </style.css>; rel=preload; as=style');\nheaders_send(103);\n\n// yavaş algoritmalarınız ve SQL sorgularınız 🤪\n\necho <<<'HTML'\n<!DOCTYPE html>\n<title>Hello FrankenPHP</title>\n<link rel=\"stylesheet\" href=\"style.css\">\nHTML;\n```\n\nEarly Hints hem normal hem de [worker](worker.md) modları tarafından desteklenir.\n"
  },
  {
    "path": "docs/tr/embed.md",
    "content": "# Binary Dosyası Olarak PHP Uygulamaları\n\nFrankenPHP, PHP uygulamalarının kaynak kodunu ve varlıklarını statik, kendi kendine yeten bir binary dosyaya yerleştirme yeteneğine sahiptir.\n\nBu özellik sayesinde PHP uygulamaları, uygulamanın kendisini, PHP yorumlayıcısını ve üretim düzeyinde bir web sunucusu olan Caddy'yi içeren bağımsız bir binary dosyalar olarak çıktısı alınabilir ve dağıtılabilir.\n\nBu özellik hakkında daha fazla bilgi almak için [Kévin tarafından SymfonyCon 2023'te yapılan sunuma](https://dunglas.dev/2023/12/php-and-symfony-apps-as-standalone-binaries/) göz atabilirsiniz.\n\n## Preparing Your App\n\nBağımsız binary dosyayı oluşturmadan önce uygulamanızın gömülmeye hazır olduğundan emin olun.\n\nÖrneğin muhtemelen şunları yapmak istersiniz:\n\n- Uygulamanın üretim bağımlılıklarını yükleyin\n- Otomatik yükleyiciyi boşaltın\n- Uygulamanızın üretim modunu etkinleştirin (varsa)\n- Nihai binary dosyanızın boyutunu küçültmek için `.git` veya testler gibi gerekli olmayan dosyaları çıkarın\n\nÖrneğin, bir Symfony uygulaması için aşağıdaki komutları kullanabilirsiniz:\n\n```console\n# .git/, vb. dosyalarından kurtulmak için projeyi dışa aktarın\nmkdir $TMPDIR/my-prepared-app\ngit archive HEAD | tar -x -C $TMPDIR/my-prepared-app\ncd $TMPDIR/my-prepared-app\n\n# Uygun ortam değişkenlerini ayarlayın\necho APP_ENV=prod > .env.local\necho APP_DEBUG=0 >> .env.local\n\n# Testleri kaldırın\nrm -Rf tests/\n\n# Bağımlılıkları yükleyin\ncomposer install --ignore-platform-reqs --no-dev -a\n\n# .env'yi optimize edin\ncomposer dump-env prod\n```\n\n## Linux Binary'si Oluşturma\n\nBir Linux binary çıktısı almanın en kolay yolu, sağladığımız Docker tabanlı derleyiciyi kullanmaktır.\n\n1. Hazırladığınız uygulamanın deposunda `static-build.Dockerfile` adlı bir dosya oluşturun:\n\n   ```dockerfile\n   FROM --platform=linux/amd64 dunglas/frankenphp:static-builder-gnu\n   # İkili dosyayı musl-libc sistemlerinde çalıştırmayı düşünüyorsanız static-builder-musl kullanın\n\n   # Uygulamanızı kopyalayın\n   WORKDIR /go/src/app/dist/app\n   COPY . .\n\n   # Statik binary dosyasını oluşturun, yalnızca istediğiniz PHP eklentilerini seçtiğinizden emin olun\n   WORKDIR /go/src/app/\n   RUN EMBED=dist/app/ \\\n       ./build-static.sh\n   ```\n\n   > [!CAUTION]\n   >\n   > Bazı `.dockerignore` dosyaları (örneğin varsayılan [Symfony Docker `.dockerignore`](https://github.com/dunglas/symfony-docker/blob/main/.dockerignore))\n   > `vendor/` dizinini ve `.env` dosyalarını yok sayacaktır. Derlemeden önce `.dockerignore` dosyasını ayarladığınızdan veya kaldırdığınızdan emin olun.\n\n2. Derleyin:\n\n   ```console\n   docker build -t static-app -f static-build.Dockerfile .\n   ```\n\n3. Binary dosyasını çıkarın:\n\n   ```console\n   docker cp $(docker create --name static-app-tmp static-app):/go/src/app/dist/frankenphp-linux-x86_64 my-app ; docker rm static-app-tmp\n   ```\n\nElde edilen binary dosyası, geçerli dizindeki `my-app` adlı dosyadır.\n\n## Diğer İşletim Sistemleri için Binary Çıktısı Alma\n\nDocker kullanmak istemiyorsanız veya bir macOS binary dosyası oluşturmak istiyorsanız, sağladığımız kabuk betiğini kullanın:\n\n```console\ngit clone https://github.com/php/frankenphp\ncd frankenphp\nEMBED=/path/to/your/app \\\n    ./build-static.sh\n```\n\nElde edilen binary dosyası `dist/` dizinindeki `frankenphp-<os>-<arch>` adlı dosyadır.\n\n## Binary Dosyasını Kullanma\n\nİşte bu kadar! `my-app` dosyası (veya diğer işletim sistemlerinde `dist/frankenphp-<os>-<arch>`) bağımsız uygulamanızı içerir!\n\nWeb uygulamasını başlatmak için çalıştırın:\n\n```console\n./my-app php-server\n```\n\nUygulamanız bir [worker betiği](worker.md) içeriyorsa, worker'ı aşağıdaki gibi bir şeyle başlatın:\n\n```console\n./my-app php-server --worker public/index.php\n```\n\nHTTPS (Let's Encrypt sertifikası otomatik olarak oluşturulur), HTTP/2 ve HTTP/3'ü etkinleştirmek için kullanılacak alan adını belirtin:\n\n```console\n./my-app php-server --domain localhost\n```\n\nAyrıca binary dosyanıza gömülü PHP CLI betiklerini de çalıştırabilirsiniz:\n\n```console\n./my-app php-cli bin/console\n```\n\n## Yapıyı Özelleştirme\n\nBinary dosyasının nasıl özelleştirileceğini (uzantılar, PHP sürümü...) görmek için [Statik derleme dokümanını okuyun](static.md).\n\n## Binary Dosyasının Dağıtılması\n\nLinux'ta, oluşturulan ikili dosya [UPX](https://upx.github.io) kullanılarak sıkıştırılır.\n\nMac'te, göndermeden önce dosyanın boyutunu küçültmek için sıkıştırabilirsiniz.\nBiz `xz` öneririz.\n"
  },
  {
    "path": "docs/tr/extension-workers.md",
    "content": "# Uzantı İşçileri\n\nUzantı İşçileri, [FrankenPHP uzantınızın](https://frankenphp.dev/docs/extensions/) arka plan görevlerini yürütmek, eşzamansız olayları işlemek veya özel protokolleri uygulamak için özel bir PHP iş parçacığı havuzunu yönetmesini sağlar. Kuyruk sistemleri, olay dinleyicileri, zamanlayıcılar vb. için kullanışlıdır.\n\n## İşçiyi Kaydetme\n\n### Statik Kayıt\n\nİşçiyi kullanıcı tarafından yapılandırılabilir hale getirmeniz gerekmiyorsa (sabit komut dosyası yolu, sabit iş parçacığı sayısı), işçiyi `init()` fonksiyonunda basitçe kaydedebilirsiniz.\n\n```go\npackage myextension\n\nimport (\n\t\"github.com/dunglas/frankenphp\"\n\t\"github.com/dunglas/frankenphp/caddy\"\n)\n\n// İşçi havuzuyla iletişim kurmak için genel tanıtıcı\nvar worker frankenphp.Workers\n\nfunc init() {\n\t// Modül yüklendiğinde işçiyi kaydet.\n\tworker = caddy.RegisterWorkers(\n\t\t\"my-internal-worker\", // Benzersiz isim\n\t\t\"worker.php\",         // Komut dosyası yolu (çalışmaya göre veya mutlak)\n\t\t2,                    // Sabit iş parçacığı sayısı\n\t\t// İsteğe bağlı Yaşam Döngüsü Kancaları\n\t\tfrankenphp.WithWorkerOnServerStartup(func() {\n\t\t\t// Genel kurulum mantığı...\n\t\t}),\n\t)\n}\n```\n\n### Bir Caddy Modülünde (Kullanıcı tarafından yapılandırılabilir)\n\nUzantınızı paylaşmayı planlıyorsanız (genel bir kuyruk veya olay dinleyici gibi), onu bir Caddy modülüne sarmalısınız. Bu, kullanıcıların `Caddyfile` aracılığıyla komut dosyası yolunu ve iş parçacığı sayısını yapılandırmasına olanak tanır. Bu, `caddy.Provisioner` arayüzünü uygulamayı ve Caddyfile'ı ayrıştırmayı gerektirir ([bir örnek görmek için](https://github.com/dunglas/frankenphp-queue/blob/989120d394d66dd6c8e2101cac73dd622fade334/caddy.go)).\n\n### Saf Bir Go Uygulamasında (Gömme)\n\nFrankenPHP'yi [Caddy olmadan standart bir Go uygulamasına gömüyorsanız](https://pkg.go.dev/github.com/dunglas/frankenphp#example-ServeHTTP), seçenekleri başlatırken `frankenphp.WithExtensionWorkers` kullanarak uzantı işçilerini kaydedebilirsiniz.\n\n## İşçilerle Etkileşim Kurma\n\nİşçi havuzu aktif hale geldiğinde, ona görevler gönderebilirsiniz. Bu, [PHP'ye dışa aktarılan yerel fonksiyonlar](https://frankenphp.dev/docs/extensions/#writing-the-extension) içinde veya bir cron zamanlayıcı, bir olay dinleyicisi (MQTT, Kafka) veya herhangi başka bir goroutine gibi herhangi bir Go mantığından yapılabilir.\n\n### Başsız Mod : `SendMessage`\n\nDoğrudan işçi komut dosyanıza ham veri geçirmek için `SendMessage` kullanın. Bu, kuyruklar veya basit komutlar için idealdir.\n\n#### Örnek: Asenkron Bir Kuyruk Uzantısı\n\n```go\n// #include <Zend/zend_types.h>\nimport \"C\"\nimport (\n\t\"context\"\n\t\"unsafe\"\n\t\"github.com/dunglas/frankenphp\"\n)\n\n//export_php:function my_queue_push(mixed $data): bool\nfunc my_queue_push(data *C.zval) bool {\n\t// 1. İşçinin hazır olduğundan emin olun\n\tif worker == nil {\n\t\treturn false\n\t}\n\n\t// 2. Arka plan işçisine gönder\n\t_, err := worker.SendMessage(\n\t\tcontext.Background(), // Standart Go bağlamı\n\t\tunsafe.Pointer(data), // İşçiye iletilecek veri\n\t\tnil, // İsteğe bağlı http.ResponseWriter\n\t)\n\n\treturn err == nil\n}\n```\n\n### HTTP Emülasyonu :`SendRequest`\n\nUzantınızın standart bir web ortamı bekleyen ( `$_SERVER`, `$_GET` vb. dolduran) bir PHP komut dosyasını çağırması gerekiyorsa `SendRequest` kullanın.\n\n```go\n// #include <Zend/zend_types.h>\nimport \"C\"\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"unsafe\"\n\t\"github.com/dunglas/frankenphp\"\n)\n\n//export_php:function my_worker_http_request(string $path): string\nfunc my_worker_http_request(path *C.zend_string) unsafe.Pointer {\n\t// 1. İsteği ve kaydediciyi hazırla\n\turl := frankenphp.GoString(unsafe.Pointer(path))\n\treq, _ := http.NewRequest(\"GET\", url, http.NoBody)\n\trr := httptest.NewRecorder()\n\n\t// 2. İşçiye gönder\n\tif err := worker.SendRequest(rr, req); err != nil {\n\t\treturn nil\n\t}\n\n\t// 3. Yakalanan yanıtı döndür\n\treturn frankenphp.PHPString(rr.Body.String(), false)\n}\n```\n\n## İşçi Komut Dosyası\n\nPHP işçi komut dosyası bir döngüde çalışır ve hem ham mesajları hem de HTTP isteklerini işleyebilir.\n\n```php\n<?php\n// Hem ham mesajları hem de HTTP isteklerini aynı döngüde işle\n$handler = function ($payload = null) {\n    // Durum 1: Mesaj Modu\n    if ($payload !== null) {\n        return \"Received payload: \" . $payload;\n    }\n\n    // Durum 2: HTTP Modu (standart PHP süper küreselleri doldurulur)\n    echo \"Hello from page: \" . $_SERVER['REQUEST_URI'];\n};\n\nwhile (frankenphp_handle_request($handler)) {\n    gc_collect_cycles();\n}\n```\n\n## Yaşam Döngüsü Kancaları\n\nFrankenPHP, yaşam döngüsünün belirli noktalarında Go kodunu yürütmek için kancalar sağlar.\n\n| Kanca Türü | Seçenek Adı | İmza | Bağlam ve Kullanım Durumu |\n| :--------- | :--------------------------- | :------------------- | :--------------------------------------------------------------------- |\n| **Sunucu** | `WithWorkerOnServerStartup`  | `func()`             | Genel kurulum. **Bir Kez** çalışır. Örnek: NATS/Redis'e bağlanma. |\n| **Sunucu** | `WithWorkerOnServerShutdown` | `func()`             | Genel temizleme. **Bir Kez** çalışır. Örnek: Paylaşılan bağlantıları kapatma. |\n| **İş Parçacığı** | `WithWorkerOnReady`          | `func(threadID int)` | İş parçacığı başına kurulum. Bir iş parçacığı başladığında çağrılır. İş Parçacığı Kimliğini alır. |\n| **İş Parçacığı** | `WithWorkerOnShutdown`       | `func(threadID int)` | İş parçacığı başına temizleme. İş Parçacığı Kimliğini alır. |\n\n### Örnek\n\n```go\npackage myextension\n\nimport (\n    \"fmt\"\n    \"github.com/dunglas/frankenphp\"\n    frankenphpCaddy \"github.com/dunglas/frankenphp/caddy\"\n)\n\nfunc init() {\n    workerHandle = frankenphpCaddy.RegisterWorkers(\n        \"my-worker\", \"worker.php\", 2,\n\n        // Sunucu Başlatma (Genel)\n        frankenphp.WithWorkerOnServerStartup(func() {\n            fmt.Println(\"Uzantı: Sunucu başlıyor...\")\n        }),\n\n        // İş Parçacığı Hazır (İş Parçacığı Başına)\n        // Not: Fonksiyon, İş Parçacığı Kimliğini temsil eden bir tamsayı kabul eder\n        frankenphp.WithWorkerOnReady(func(id int) {\n            fmt.Printf(\"Uzantı: İşçi iş parçacığı #%d hazır.\\n\", id)\n        }),\n    )\n}\n"
  },
  {
    "path": "docs/tr/github-actions.md",
    "content": "# GitHub Actions Kullanma\n\nBu depo Docker imajını [Docker Hub](https://hub.docker.com/r/dunglas/frankenphp) üzerinde derler ve dağıtır.\nBu durum onaylanan her çekme (pull) isteğinde veya çatallandıktan (fork) sonra gerçekleşir.\n\n## GitHub Eylemlerini Ayarlama\n\nDepo ayarlarında, gizli değerler altında aşağıdaki gizli değerleri ekleyin:\n\n- `REGISTRY_LOGIN_SERVER`: Kullanılacak Docker Registry bilgisi (örneğin `docker.io`).\n- `REGISTRY_USERNAME`: Giriş yapmak için kullanılacak kullanıcı adı (örn. `dunglas`).\n- `REGISTRY_PASSWORD`: Oturum açmak için kullanılacak parola (örn. bir erişim anahtarı).\n- `IMAGE_NAME`: İmajın adı (örn. `dunglas/frankenphp`).\n\n## İmajı Oluşturma ve Dağıtma\n\n1. Bir Çekme (pull) İsteği oluşturun veya çatala (forka) dağıtın.\n2. GitHub Actions imajı oluşturacak ve tüm testleri çalıştıracaktır.\n3. Derleme başarılı olursa, görüntü `pr-x` (burada `x` PR numarasıdır) etiketi kullanılarak ilgili saklanan yere (registry'e) gönderilir.\n\n## İmajı Dağıtma\n\n1. Çekme (pull) isteği birleştirildikten sonra, GitHub Actions testleri tekrar çalıştıracak ve yeni bir imaj oluşturacaktır.\n2. Derleme başarılı olursa, `main` etiketi Docker Registry'de güncellenecektir.\n\n## Bültenler\n\n1. Depoda yeni bir etiket oluşturun.\n2. GitHub Actions imajı oluşturacak ve tüm testleri çalıştıracaktır.\n3. Derleme başarılı olursa, etiket adı etiket olarak kullanılarak imaj saklanan yere (registry'e) gönderilir (örneğin `v1.2.3` ve `v1.2` oluşturulur).\n4. `latest` etiketi de güncellenecektir.\n"
  },
  {
    "path": "docs/tr/hot-reload.md",
    "content": "# Sıcak Yeniden Yükleme\n\nFrankenPHP, geliştirici deneyimini büyük ölçüde iyileştirmek için tasarlanmış yerleşik bir **sıcak yeniden yükleme** özelliğine sahiptir.\n\n![Hot Reload](hot-reload.png)\n\nBu özellik, Vite veya webpack gibi modern JavaScript araçlarındaki **Sıcak Modül Değişimi (HMR)** ile benzer bir iş akışı sunar.\nHer dosya değişikliğinden (PHP kodu, şablonlar, JavaScript ve CSS dosyaları...) sonra tarayıcıyı manuel olarak yenilemek yerine,\nFrankenPHP sayfa içeriğini gerçek zamanlı olarak günceller.\n\nSıcak Yeniden Yükleme, WordPress, Laravel, Symfony ve diğer tüm PHP uygulamaları veya framework'leri ile yerel olarak çalışır.\n\nEtkinleştirildiğinde, FrankenPHP dosya sistemi değişiklikleri için mevcut çalışma dizininizi izler.\nBir dosya değiştirildiğinde, tarayıcıya bir [Mercure](mercure.md) güncellemesi gönderir.\n\nKurulumunuza bağlı olarak, tarayıcı ya:\n\n- [Idiomorph](https://github.com/bigskysoftware/idiomorph) yüklüyse **DOM'u dönüştürür** (kaydırma konumunu ve girdi durumunu koruyarak).\n- Idiomorph mevcut değilse **sayfayı yeniden yükler** (standart canlı yeniden yükleme).\n\n## Yapılandırma\n\nSıcak yeniden yüklemeyi etkinleştirmek için Mercure'ü etkinleştirin, ardından `Caddyfile` dosyanızdaki `php_server` yönergesine `hot_reload` alt yönergesini ekleyin.\n\n> [!WARNING]\n>\n> Bu özellik **yalnızca geliştirme ortamları** içindir.\n> `hot_reload`'u üretimde etkinleştirmeyin, zira bu özellik güvenli değildir (hassas dahili ayrıntıları açığa çıkarır) ve uygulamanın yavaşlamasına neden olur.\n>\n```caddyfile\nlocalhost\n\nmercure {\n    anonymous\n}\n\nroot public/\nphp_server {\n    hot_reload\n}\n```\n\nVarsayılan olarak, FrankenPHP mevcut çalışma dizinindeki şu glob desenine uyan tüm dosyaları izleyecektir: `./**/*.{css,env,gif,htm,html,jpg,jpeg,js,mjs,php,png,svg,twig,webp,xml,yaml,yml}`\n\nİzlenecek dosyaları glob sözdizimi kullanarak açıkça ayarlamak mümkündür:\n\n```caddyfile\nlocalhost\n\nmercure {\n    anonymous\n}\n\nroot public/\nphp_server {\n    hot_reload src/**/*{.php,.js} config/**/*.yaml\n}\n```\n\nKullanılacak Mercure konusunu ve izlenecek dizin veya dosyaları belirtmek için `hot_reload`'un uzun biçimini kullanın:\n\n```caddyfile\nlocalhost\n\nmercure {\n    anonymous\n}\n\nroot public/\nphp_server {\n    hot_reload {\n        topic hot-reload-topic\n        watch src/**/*.php\n        watch assets/**/*.{ts,json}\n        watch templates/\n        watch public/css/\n    }\n}\n```\n\n## İstemci Tarafı Entegrasyonu\n\nSunucu değişiklikleri algılarken, tarayıcının sayfayı güncellemek için bu olaylara abone olması gerekir.\nFrankenPHP, dosya değişikliklerine abone olmak için kullanılacak Mercure Hub URL'sini `$_SERVER['FRANKENPHP_HOT_RELOAD']` ortam değişkeni aracılığıyla gösterir.\n\nİstemci tarafı mantığını yönetmek için kullanışlı bir JavaScript kütüphanesi olan [frankenphp-hot-reload](https://www.npmjs.com/package/frankenphp-hot-reload) da mevcuttur.\nKullanmak için ana düzeninize aşağıdakileri ekleyin:\n\n```php\n<!DOCTYPE html>\n<title>FrankenPHP Hot Reload</title>\n<?php if (isset($_SERVER['FRANKENPHP_HOT_RELOAD'])): ?>\n<meta name=\"frankenphp-hot-reload:url\" content=\"<?=$_SERVER['FRANKENPHP_HOT_RELOAD']?>\">\n<script src=\"https://cdn.jsdelivr.net/npm/idiomorph\"></script>\n<script src=\"https://cdn.jsdelivr.net/npm/frankenphp-hot-reload/+esm\" type=\"module\"></script>\n<?php endif ?>\n```\n\nKütüphane otomatik olarak Mercure hub'ına abone olacak, bir dosya değişikliği algılandığında arka planda mevcut URL'yi getirecek ve DOM'u dönüştürecektir.\nBir [npm](https://www.npmjs.com/package/frankenphp-hot-reload) paketi olarak ve [GitHub](https://github.com/dunglas/frankenphp-hot-reload) üzerinden edinilebilir.\n\nAlternatif olarak, `EventSource` yerel JavaScript sınıfını kullanarak doğrudan Mercure hub'ına abone olarak kendi istemci tarafı mantığınızı uygulayabilirsiniz.\n\n### Mevcut DOM Düğümlerini Korumak\n\nNadir durumlarda, [Symfony web hata ayıklama araç çubuğu gibi](https://github.com/symfony/symfony/pull/62970) geliştirme araçları kullanırken olduğu gibi,\nbelirli DOM düğümlerini korumak isteyebilirsiniz.\nBunu yapmak için, ilgili HTML öğesine `data-frankenphp-hot-reload-preserve` özniteliğini ekleyin:\n\n```html\n<div data-frankenphp-hot-reload-preserve><!-- Hata ayıklama çubuğum --></div>\n```\n\n## Çalışan Modu\n\nUygulamanızı [Çalışan Modunda](https://frankenphp.dev/docs/worker/) çalıştırıyorsanız, uygulama betiğiniz bellekte kalır.\nBu, tarayıcı yeniden yüklense bile PHP kodunuzdaki değişikliklerin hemen yansımayacağı anlamına gelir.\n\nEn iyi geliştirici deneyimi için `hot_reload`'u [worker yönergesindeki `watch` alt yönergesiyle](config.md#watching-for-file-changes) birleştirmelisiniz.\n\n- `hot_reload`: dosyalar değiştiğinde **tarayıcıyı** yeniler\n- `worker.watch`: dosyalar değiştiğinde çalışanı yeniden başlatır\n\n```caddy\nlocalhost\n\nmercure {\n    anonymous\n}\n\nroot public/\nphp_server {\n    hot_reload\n    worker {\n        file /path/to/my_worker.php\n        watch\n    }\n}\n```\n\n## Nasıl Çalışır\n\n1. **İzleme**: FrankenPHP, arka planda [`e-dant/watcher` kütüphanesini](https://github.com/e-dant/watcher) kullanarak (Go bağlayıcısını biz geliştirdik) dosya sistemindeki değişiklikleri izler.\n2. **Yeniden Başlatma (Çalışan Modu)**: Çalışan yapılandırmasında `watch` etkinse, yeni kodu yüklemek için PHP çalışanı yeniden başlatılır.\n3. **Gönderme**: Değiştirilen dosyaların listesini içeren bir JSON yükü, yerleşik [Mercure hub'ına](https://mercure.rocks) gönderilir.\n4. **Alma**: JavaScript kütüphanesi aracılığıyla dinleyen tarayıcı, Mercure olayını alır.\n5. **Güncelleme**:\n\n- **Idiomorph** algılanırsa, güncellenmiş içeriği getirir ve mevcut HTML'i yeni duruma uydurmak için dönüştürerek, durum kaybetmeden değişiklikleri anında uygular.\n- Aksi takdirde, sayfayı yenilemek için `window.location.reload()` çağrılır.\n"
  },
  {
    "path": "docs/tr/known-issues.md",
    "content": "# Bilinen Sorunlar\n\n## Desteklenmeyen PHP Eklentileri\n\nAşağıdaki eklentilerin FrankenPHP ile uyumlu olmadığı bilinmektedir:\n\n| Adı                                                         | Nedeni                     | Alternatifleri                                                                                                       |\n| ----------------------------------------------------------- | -------------------------- | -------------------------------------------------------------------------------------------------------------------- |\n| [imap](https://www.php.net/manual/en/imap.installation.php) | İş parçacığı güvenli değil | [javanile/php-imap2](https://github.com/javanile/php-imap2), [webklex/php-imap](https://github.com/Webklex/php-imap) |\n\n## Sorunlu PHP Eklentileri\n\nAşağıdaki eklentiler FrankenPHP ile kullanıldığında bilinen hatalara ve beklenmeyen davranışlara sahiptir:\n\n| Adı | Problem |\n| --- | ------- |\n\n## get_browser\n\n[get_browser()](https://www.php.net/manual/en/function.get-browser.php) fonksiyonu bir süre sonra kötü performans gösteriyor gibi görünüyor. Geçici bir çözüm, statik oldukları için User-Agent başına sonuçları önbelleğe almaktır (örneğin [APCu](https://www.php.net/manual/en/book.apcu.php) ile).\n\n## Binary Çıktısı ve Alpine Tabanlı Docker İmajları\n\nBinary çıktısı ve Alpine tabanlı Docker imajları (dunglas/frankenphp:\\*-alpine), daha küçük bir binary boyutu korumak için glibc ve arkadaşları yerine musl libc kullanır. Bu durum bazı uyumluluk sorunlarına yol açabilir. Özellikle, glob seçeneği GLOB_BRACE mevcut değildir.\n\n## Docker ile `https://127.0.0.1` Kullanımı\n\nFrankenPHP varsayılan olarak `localhost` için bir TLS sertifikası oluşturur.\nBu, yerel geliştirme için en kolay ve önerilen seçenektir.\n\nBunun yerine ana bilgisayar olarak `127.0.0.1` kullanmak istiyorsanız, sunucu adını `127.0.0.1` şeklinde ayarlayarak bunun için bir sertifika oluşturacak yapılandırma yapmak mümkündür.\n\nNe yazık ki, [ağ sistemi](https://docs.docker.com/network/) nedeniyle Docker kullanırken bu yeterli değildir.\n`Curl: (35) LibreSSL/3.3.6: error:1404B438:SSL routines:ST_CONNECT:tlsv1 alert internal error`'a benzer bir TLS hatası alırsınız.\n\nLinux kullanıyorsanız, [ana bilgisayar ağ sürücüsünü](https://docs.docker.com/network/network-tutorial-host/) kullanmak bir çözümdür:\n\n```console\ndocker run \\\n    -e SERVER_NAME=\"127.0.0.1\" \\\n    -v $PWD:/app/public \\\n    --network host \\\n    dunglas/frankenphp\n```\n\nAna bilgisayar ağ sürücüsü Mac ve Windows'ta desteklenmez. Bu platformlarda, konteynerin IP adresini tahmin etmeniz ve bunu sunucu adlarına dahil etmeniz gerekecektir.\n\n`docker network inspect bridge`'i çalıştırın ve `IPv4Address` anahtarının altındaki son atanmış IP adresini belirlemek için `Containers` anahtarına bakın ve bir artırın. Eğer hiçbir konteyner çalışmıyorsa, ilk atanan IP adresi genellikle `172.17.0.2`dir.\n\nArdından, bunu `SERVER_NAME` ortam değişkenine ekleyin:\n\n```console\ndocker run \\\n    -e SERVER_NAME=\"127.0.0.1, 172.17.0.3\" \\\n    -v $PWD:/app/public \\\n    -p 80:80 -p 443:443 -p 443:443/udp \\\n    dunglas/frankenphp\n```\n\n> [!CAUTION]\n>\n> 172.17.0.3`ü konteynerinize atanacak IP ile değiştirdiğinizden emin olun.\n\nArtık ana makineden `https://127.0.0.1` adresine erişebilmeniz gerekir.\n\nEğer durum böyle değilse, sorunu anlamaya çalışmak için FrankenPHP'yi hata ayıklama modunda başlatın:\n\n```console\ndocker run \\\n    -e CADDY_GLOBAL_OPTIONS=\"debug\" \\\n    -e SERVER_NAME=\"127.0.0.1\" \\\n    -v $PWD:/app/public \\\n    -p 80:80 -p 443:443 -p 443:443/udp \\\n    dunglas/frankenphp\n```\n\n## `@php` Referanslı Composer Betikler\n\n[Composer betikleri](https://getcomposer.org/doc/articles/scripts.md) bazı görevler için bir PHP binary çalıştırmak isteyebilir, örneğin [bir Laravel projesinde](laravel.md) `@php artisan package:discover --ansi` çalıştırmak. Bu [şu anda mümkün değil](https://github.com/php/frankenphp/issues/483#issuecomment-1899890915) ve 2 nedeni var:\n\n- Composer FrankenPHP binary dosyasını nasıl çağıracağını bilmiyor;\n- Composer, FrankenPHP'nin henüz desteklemediği `-d` bayrağını kullanarak PHP ayarlarını komuta ekleyebilir.\n\nGeçici bir çözüm olarak, `/usr/local/bin/php` içinde desteklenmeyen parametreleri silen ve ardından FrankenPHP'yi çağıran bir kabuk betiği oluşturabiliriz:\n\n```bash\n#!/bin/bash\nargs=(\"$@\")\nindex=0\nfor i in \"$@\"\ndo\n    if [ \"$i\" == \"-d\" ]; then\n        unset 'args[$index]'\n        unset 'args[$index+1]'\n    fi\n    index=$((index+1))\ndone\n\n/usr/local/bin/frankenphp php-cli ${args[@]}\n```\n\nArdından `PHP_BINARY` ortam değişkenini PHP betiğimizin yoluna ayarlayın ve Composer bu yolla çalışacaktır:\n\n```bash\nexport PHP_BINARY=/usr/local/bin/php\ncomposer install\n```\n"
  },
  {
    "path": "docs/tr/laravel.md",
    "content": "# Laravel\n\n## Docker\n\nBir [Laravel](https://laravel.com) web uygulamasını FrankenPHP ile çalıştırmak, projeyi resmi Docker imajının `/app` dizinine monte etmek kadar kolaydır.\n\nBu komutu Laravel uygulamanızın ana dizininden çalıştırın:\n\n```console\ndocker run -p 80:80 -p 443:443 -p 443:443/udp -v $PWD:/app dunglas/frankenphp\n```\n\nVe tadını çıkarın!\n\n## Yerel Kurulum\n\nAlternatif olarak, Laravel projelerinizi FrankenPHP ile yerel makinenizden çalıştırabilirsiniz:\n\n1. [Sisteminize karşılık gelen ikili dosyayı indirin](../#standalone-binary)\n2. Aşağıdaki yapılandırmayı Laravel projenizin kök dizinindeki `Caddyfile` adlı bir dosyaya ekleyin:\n\n   ```caddyfile\n   {\n   \tfrankenphp\n   }\n\n   # Sunucunuzun alan adı\n   localhost {\n   \t# Webroot'u public/ dizinine ayarlayın\n   \troot public/\n   \t# Sıkıştırmayı etkinleştir (isteğe bağlı)\n   \tencode zstd br gzip\n   \t# public/ dizininden PHP dosyalarını çalıştırın ve statik dosyaları servis edin\n   \tphp_server {\n   \t\ttry_files {path} index.php\n   \t}\n   }\n   ```\n\n3. FrankenPHP'yi Laravel projenizin kök dizininden başlatın: `frankenphp run`\n\n## Laravel Octane\n\nOctane, Composer paket yöneticisi aracılığıyla kurulabilir:\n\n```console\ncomposer require laravel/octane\n```\n\nOctane'ı kurduktan sonra, Octane'ın yapılandırma dosyasını uygulamanıza yükleyecek olan `octane:install` Artisan komutunu çalıştırabilirsiniz:\n\n```console\nphp artisan octane:install --server=frankenphp\n```\n\nOctane sunucusu `octane:frankenphp` Artisan komutu aracılığıyla başlatılabilir.\n\n```console\nphp artisan octane:frankenphp\n```\n\n`octane:frankenphp` komutu aşağıdaki seçenekleri alabilir:\n\n- `--host`: Sunucunun bağlanması gereken IP adresi (varsayılan: `127.0.0.1`)\n- `--port`: Sunucunun erişilebilir olması gereken port (varsayılan: `8000`)\n- `--admin-port`: Yönetici sunucusunun erişilebilir olması gereken port (varsayılan: `2019`)\n- `--workers`: İstekleri işlemek için hazır olması gereken worker sayısı (varsayılan: `auto`)\n- `--max-requests`: Sunucu yeniden yüklenmeden önce işlenecek istek sayısı (varsayılan: `500`)\n- `--caddyfile`: FrankenPHP `Caddyfile` dosyasının yolu (varsayılan: [Laravel Octane içinde bulunan şablon `Caddyfile`](https://github.com/laravel/octane/blob/2.x/src/Commands/stubs/Caddyfile))\n- `--https`: HTTPS, HTTP/2 ve HTTP/3'ü etkinleştirin ve sertifikaları otomatik olarak oluşturup yenileyin\n- `--http-redirect`: HTTP'den HTTPS'ye yeniden yönlendirmeyi etkinleştir (yalnızca --https ile birlikte geçilirse etkinleşir)\n- `--watch`: Uygulama değiştirildiğinde sunucuyu otomatik olarak yeniden yükle\n- `--poll`: Dosyaları bir ağ üzerinden izlemek için izleme sırasında dosya sistemi yoklamasını kullanın\n- `--log-level`: Yerel Caddy günlüğünü kullanarak belirtilen günlük seviyesinde veya üzerinde mesajları kaydedin\n\n> [!TIP]\n> Yapılandırılmış JSON günlükleri elde etmek için (log analitik çözümleri kullanırken faydalıdır), `--log-level` seçeneğini açıkça geçin.\n\n[Laravel Octane hakkında daha fazla bilgiyi resmi belgelerde bulabilirsiniz](https://laravel.com/docs/octane).\n\n## Laravel Uygulamalarını Bağımsız Çalıştırılabilir Dosyalar Olarak Dağıtma\n\n[FrankenPHP'nin uygulama gömme özelliğini](embed.md) kullanarak, Laravel\nuygulamalarını bağımsız çalıştırılabilir dosyalar olarak dağıtmak mümkündür.\n\nLinux için Laravel uygulamanızı bağımsız bir çalıştırılabilir olarak paketlemek için şu adımları izleyin:\n\n1. Uygulamanızın deposunda `static-build.Dockerfile` adında bir dosya oluşturun:\n\n   ```dockerfile\n   FROM --platform=linux/amd64 dunglas/frankenphp:static-builder-gnu\n   # İkiliyi musl-libc sistemlerinde çalıştırmayı düşünüyorsanız, bunun yerine static-builder-musl kullanın\n\n   # Uygulamanızı kopyalayın\n   WORKDIR /go/src/app/dist/app\n   COPY . .\n\n   # Yer kaplamamak için testleri ve diğer gereksiz dosyaları kaldırın\n   # Alternatif olarak, bu dosyaları bir .dockerignore dosyasına ekleyin\n   RUN rm -Rf tests/\n\n   # .env dosyasını kopyalayın\n   RUN cp .env.example .env\n   # APP_ENV ve APP_DEBUG değerlerini production için uygun hale getirin\n   RUN sed -i'' -e 's/^APP_ENV=.*/APP_ENV=production/' -e 's/^APP_DEBUG=.*/APP_DEBUG=false/' .env\n\n   # Gerekirse .env dosyanıza diğer değişiklikleri yapın\n\n   # Bağımlılıkları yükleyin\n   RUN composer install --ignore-platform-reqs --no-dev -a\n\n   # Statik ikiliyi derleyin\n   WORKDIR /go/src/app/\n   RUN EMBED=dist/app/ ./build-static.sh\n   ```\n\n   > [!CAUTION]\n   > Bazı `.dockerignore` dosyaları\n   > `vendor/` dizinini ve `.env` dosyalarını yok sayar. Derlemeden önce `.dockerignore` dosyasını buna göre ayarladığınızdan veya kaldırdığınızdan emin olun.\n\n2. İmajı oluşturun:\n\n   ```console\n   docker build -t static-laravel-app -f static-build.Dockerfile .\n   ```\n\n3. İkili dosyayı dışa aktarın:\n\n   ```console\n   docker cp $(docker create --name static-laravel-app-tmp static-laravel-app):/go/src/app/dist/frankenphp-linux-x86_64 frankenphp ; docker rm static-laravel-app-tmp\n   ```\n\n4. Önbellekleri doldurun:\n\n   ```console\n   frankenphp php-cli artisan optimize\n   ```\n\n5. Veritabanı migration'larını çalıştırın (varsa):\n\n   ```console\n   frankenphp php-cli artisan migrate\n   ```\n\n6. Uygulamanın gizli anahtarını oluşturun:\n\n   ```console\n   frankenphp php-cli artisan key:generate\n   ```\n\n7. Sunucuyu başlatın:\n\n   ```console\n   frankenphp php-server\n   ```\n\nUygulamanız artık hazır!\n\nMevcut seçenekler hakkında daha fazla bilgi edinin ve diğer işletim sistemleri için nasıl ikili derleneceğini [uygulama gömme](embed.md)\nbelgelerinde öğrenin.\n\n### Depolama Yolunu Değiştirme\n\nVarsayılan olarak, Laravel yüklenen dosyaları, önbellekleri, logları vb. uygulamanın `storage/` dizininde saklar.\nGömülü uygulamalar için bu uygun değildir, çünkü her yeni sürüm farklı bir geçici dizine çıkarılacaktır.\n\nGeçici dizin dışında bir dizin kullanmak için `LARAVEL_STORAGE_PATH` ortam değişkenini ayarlayın (örneğin, `.env` dosyanızda) veya `Illuminate\\Foundation\\Application::useStoragePath()` metodunu çağırın.\n\n### Bağımsız Çalıştırılabilir Dosyalarla Octane'i Çalıştırma\n\nLaravel Octane uygulamalarını bağımsız çalıştırılabilir dosyalar olarak paketlemek bile mümkündür!\n\nBunu yapmak için, [Octane'i doğru şekilde kurun](#laravel-octane) ve [önceki bölümde](#laravel-uygulamalarını-bağımsız-çalıştırılabilir-dosyalar-olarak-dağıtma) açıklanan adımları izleyin.\n\nArdından, Octane üzerinden FrankenPHP'yi worker modunda başlatmak için şunu çalıştırın:\n\n```console\nPATH=\"$PWD:$PATH\" frankenphp php-cli artisan octane:frankenphp\n```\n\n> [!CAUTION]\n> Komutun çalışması için, bağımsız ikili dosya mutlaka `frankenphp` olarak adlandırılmış olmalıdır,\n> çünkü Octane, yol üzerinde `frankenphp` adlı bir programın mevcut olmasını bekler.\n"
  },
  {
    "path": "docs/tr/mercure.md",
    "content": "# Gerçek Zamanlı\n\nFrankenPHP yerleşik bir [Mercure](https://mercure.rocks) hub ile birlikte gelir!\nMercure, olayları tüm bağlı cihazlara gerçek zamanlı olarak göndermeye olanak tanır: anında bir JavaScript olayı alırlar.\n\nJS kütüphanesi veya SDK gerekmez!\n\n![Mercure](../mercure-hub.png)\n\nMercure hub'ını etkinleştirmek için [Mercure'ün sitesinde](https://mercure.rocks/docs/hub/config) açıklandığı gibi `Caddyfile`'ı güncelleyin.\n\nMercure güncellemelerini kodunuzdan göndermek için [Symfony Mercure Bileşenini](https://symfony.com/components/Mercure) öneririz (kullanmak için Symfony tam yığın çerçevesine ihtiyacınız yoktur).\n"
  },
  {
    "path": "docs/tr/performance.md",
    "content": "# Performans\n\nVarsayılan olarak, FrankenPHP performans ve kullanım kolaylığı arasında iyi bir denge sunmaya çalışır.\nAncak, uygun bir yapılandırma kullanılarak performansı önemli ölçüde artırmak mümkündür.\n\n## İş Parçacığı ve İşçi Sayısı\n\nVarsayılan olarak, FrankenPHP mevcut CPU çekirdeği sayısının 2 katı kadar iş parçacığı ve işçi (işçi modunda) başlatır.\n\nUygun değerler, uygulamanızın nasıl yazıldığına, ne yaptığına ve donanımınıza büyük ölçüde bağlıdır.\nBu değerleri değiştirmenizi şiddetle tavsiye ederiz. En iyi sistem kararlılığı için, `num_threads` x `memory_limit` < `available_memory` olması önerilir.\n\nDoğru değerleri bulmak için gerçek trafiği simüle eden yük testleri yapmak en iyisidir.\n[k6](https://k6.io) ve [Gatling](https://gatling.io) bunun için iyi araçlardır.\n\nİş parçacığı sayısını yapılandırmak için `php_server` ve `php` yönergelerinin `num_threads` seçeneğini kullanın.\nİşçi sayısını değiştirmek için `frankenphp` yönergesinin `worker` bölümünün `num` seçeneğini kullanın.\n\n### `max_threads`\n\nTrafiğinizin neye benzeyeceğini tam olarak bilmek her zaman daha iyi olsa da, gerçek dünya uygulamaları daha\ntahmin edilemez olma eğilimindedir. `max_threads` [yapılandırması](config.md#caddyfile-konfigürasyonu), FrankenPHP'nin çalışma zamanında belirtilen sınıra kadar ek iş parçacıkları otomatik olarak oluşturmasına olanak tanır.\n`max_threads`, trafiğinizi yönetmek için kaç iş parçacığına ihtiyacınız olduğunu anlamanıza yardımcı olabilir ve sunucuyu gecikme artışlarına karşı daha dirençli hale getirebilir.\nEğer `auto` olarak ayarlanırsa, sınır `php.ini` dosyanızdaki `memory_limit` değerine göre tahmin edilecektir. Bunu yapamazsa,\n`auto` bunun yerine varsayılan olarak 2x `num_threads` olacaktır. `auto`'nun ihtiyaç duyulan iş parçacığı sayısını büyük ölçüde küçümseyebileceğini unutmayın.\n`max_threads`, PHP FPM'nin [pm.max_children](https://www.php.net/manual/en/install.fpm.configuration.php#pm.max-children) ile benzerdir. Temel fark, FrankenPHP'nin süreçler yerine\niş parçacıkları kullanması ve gerektiğinde bunları farklı işçi komut dosyaları ve 'klasik mod' arasında otomatik olarak devretmesidir.\n\n## İşçi Modu\n\n[İşçi modunu](worker.md) etkinleştirmek performansı önemli ölçüde artırır,\nancak uygulamanızın bu modla uyumlu olacak şekilde uyarlanması gerekir:\nbir işçi komut dosyası oluşturmanız ve uygulamanın bellek sızdırmadığından emin olmanız gerekir.\n\n## musl Kullanmayın\n\nResmi Docker imajlarının Alpine Linux varyantı ve sağladığımız varsayılan ikili dosyalar [musl libc](https://musl.libc.org) kullanmaktadır.\n\nPHP'nin, geleneksel GNU kitaplığı yerine bu alternatif C kitaplığını kullandığında [daha yavaş olduğu](https://gitlab.alpinelinux.org/alpine/aports/-/issues/14381) bilinmektedir,\nözellikle de FrankenPHP için gerekli olan ZTS modunda (iş parçacığı güvenli) derlendiğinde. Fark, yoğun iş parçacıklı bir ortamda önemli olabilir.\n\nAyrıca, [bazı hatalar yalnızca musl kullanıldığında ortaya çıkar](https://github.com/php/php-src/issues?q=sort%3Aupdated-desc+is%3Aissue+is%3Aopen+label%3ABug+musl).\n\nÜretim ortamlarında, glibc'ye bağlı, uygun bir optimizasyon seviyesiyle derlenmiş FrankenPHP kullanmanızı öneririz.\n\nBu, Debian Docker imajlarını kullanarak, [bakımcılarımızın .deb, .rpm veya .apk paketlerini](https://pkgs.henderkes.com) kullanarak veya [FrankenPHP'yi kaynak koddan derleyerek](compile.md) başarılabilir.\n\nDaha yalın veya daha güvenli konteynerler için Alpine yerine [güçlendirilmiş bir Debian imajı](docker.md#hardening-images) kullanmayı düşünebilirsiniz.\n\n## Go Çalışma Zamanı Yapılandırması\n\nFrankenPHP Go ile yazılmıştır.\n\nGenel olarak, Go çalışma zamanı özel bir yapılandırma gerektirmez, ancak belirli durumlarda,\nözel yapılandırma performansı artırır.\n\nMuhtemelen `GODEBUG` ortam değişkenini `cgocheck=0` olarak ayarlamak isteyeceksiniz (FrankenPHP Docker imajlarındaki varsayılan değer).\n\nFrankenPHP'yi konteynerlerde (Docker, Kubernetes, LXC...) çalıştırıyorsanız ve konteynerler için ayrılan belleği sınırlıyorsanız,\n`GOMEMLIMIT` ortam değişkenini mevcut bellek miktarına ayarlayın.\n\nDaha fazla ayrıntı için, [Go dokümantasyon sayfasının bu konuya ayrılmış bölümünü](https://pkg.go.dev/runtime#hdr-Environment_Variables) okumanız, çalışma zamanından en iyi şekilde yararlanmak için zorunludur.\n\n## `file_server`\n\nVarsayılan olarak, `php_server` yönergesi,\nkök dizinde depolanan statik dosyaları (varlıkları) sunmak için otomatik olarak bir dosya sunucusu kurar.\n\nBu özellik kullanışlıdır, ancak bir maliyeti vardır.\nBunu devre dışı bırakmak için aşağıdaki yapılandırmayı kullanın:\n\n```caddyfile\nphp_server {\n    file_server off\n}\n```\n\n## `try_files`\n\nStatik dosyalar ve PHP dosyaları dışında, `php_server` uygulamanızın dizin dizini ve dizin dizini dosyalarını (`/path/` -> `/path/index.php`) da sunmaya çalışacaktır. Dizin dizinlerine ihtiyacınız yoksa,\n`try_files` değerini açıkça şu şekilde tanımlayarak bunları devre dışı bırakabilirsiniz:\n\n```caddyfile\nphp_server {\n    try_files {path} index.php\n    root /root/to/your/app # kökü buraya açıkça eklemek daha iyi önbelleğe alma sağlar\n}\n```\n\nBu, gereksiz dosya işlemlerinin sayısını önemli ölçüde azaltabilir.\nÖnceki yapılandırmanın bir işçi eşdeğeri şöyle olacaktır:\n\n```caddyfile\nroute {\n    php_server { # dosya sunucusuna hiç ihtiyacınız yoksa \"php_server\" yerine \"php\" kullanın\n        root /root/to/your/app\n        worker /path/to/worker.php {\n            match * # tüm istekleri doğrudan işçiye gönder\n        }\n    }\n}\n```\n\n0 gereksiz dosya sistemi işlemiyle alternatif bir yaklaşım, bunun yerine `php` yönergesini kullanmak ve dosyaları PHP'den yola göre ayırmaktır. Bu yaklaşım, tüm uygulamanızın tek bir giriş dosyası tarafından sunulması durumunda iyi çalışır.\nStatik dosyaları bir `/assets` klasörünün arkasında sunan bir örnek [yapılandırma](config.md#caddyfile-konfigürasyonu) şöyle görünebilir:\n\n```caddyfile\nroute {\n    @assets {\n        path /assets/*\n    }\n\n    # /assets arkasındaki her şey dosya sunucusu tarafından işlenir\n    file_server @assets {\n        root /root/to/your/app\n    }\n\n    # /assets içinde olmayan her şey dizininiz veya işçi PHP dosyanız tarafından işlenir\n    rewrite index.php\n    php {\n        root /root/to/your/app # kökü buraya açıkça eklemek daha iyi önbelleğe alma sağlar\n    }\n}\n```\n\n## Yer Tutucular\n\n`root` ve `env` yönergelerinde [yer tutucular](https://caddyserver.com/docs/conventions#placeholders) kullanabilirsiniz.\nAncak bu, bu değerlerin önbelleğe alınmasını engeller ve önemli bir performans maliyetiyle birlikte gelir.\n\nMümkünse, bu yönergelerde yer tutuculardan kaçının.\n\n## `resolve_root_symlink`\n\nVarsayılan olarak, belge kökü sembolik bir bağlantıysa, FrankenPHP tarafından otomatik olarak çözümlenir (PHP'nin düzgün çalışması için bu gereklidir).\nBelge kökü bir sembolik bağlantı değilse, bu özelliği devre dışı bırakabilirsiniz.\n\n```caddyfile\nphp_server {\n    resolve_root_symlink false\n}\n```\n\nBu, `root` yönergesi [yer tutucular](https://caddyserver.com/docs/conventions#placeholders) içeriyorsa performansı artıracaktır.\nDiğer durumlarda kazanç ihmal edilebilir olacaktır.\n\n## Günlükler\n\nGünlük kaydı açıkça çok faydalıdır, ancak tanım gereği,\ngiriş/çıkış işlemleri ve bellek ayırmaları gerektirir, bu da performansı önemli ölçüde azaltır.\n[Günlük seviyesini](https://caddyserver.com/docs/caddyfile/options#log) doğru bir şekilde ayarladığınızdan emin olun,\nve yalnızca gerekli olanı günlüğe kaydedin.\n\n## PHP Performansı\n\nFrankenPHP resmi PHP yorumlayıcısını kullanır.\nTüm olağan PHP ile ilgili performans optimizasyonları FrankenPHP ile de geçerlidir.\n\nÖzellikle:\n\n- [OPcache](https://www.php.net/manual/en/book.opcache.php)'in kurulu, etkin ve doğru şekilde yapılandırıldığını kontrol edin\n- [Composer otomatik yükleyici optimizasyonlarını](https://getcomposer.org/doc/articles/autoloader-optimization.md) etkinleştirin\n- `realpath` önbelleğinin uygulamanızın ihtiyaçları için yeterince büyük olduğundan emin olun\n- [ön yüklemeyi](https://www.php.net/manual/en/opcache.preloading.php) kullanın\n\nDaha fazla ayrıntı için, [Symfony'nin bu konuya ayrılmış dokümantasyon girişini](https://symfony.com/doc/current/performance.html) okuyun\n(ipuçlarının çoğu Symfony kullanmasanız bile faydalıdır).\n\n## İş Parçacığı Havuzunu Bölme\n\nUygulamaların, yüksek yük altında güvenilmez olma eğiliminde olan veya sürekli olarak 10 saniyeden fazla yanıt veren bir\nAPI gibi yavaş harici hizmetlerle etkileşime girmesi yaygındır.\nBu gibi durumlarda, özel \"yavaş\" havuzlara sahip olmak için iş parçacığı havuzunu bölmek faydalı olabilir.\nBu, yavaş uç noktaların tüm sunucu kaynaklarını/iş parçacıklarını tüketmesini önler ve\nbağlantı havuzuna benzer şekilde, yavaş uç noktaya giden isteklerin eş zamanlılığını sınırlar.\n\n```caddyfile\nexample.com {\n    php_server {\n        root /app/public # uygulamanızın kök dizini\n        worker index.php {\n            match /slow-endpoint/* # /slow-endpoint/* yoluyla eşleşen tüm istekler bu iş parçacığı havuzu tarafından işlenir\n            num 1 # /slow-endpoint/* ile eşleşen istekler için minimum 1 iş parçacığı\n            max_threads 20 # /slow-endpoint/* ile eşleşen istekler için gerektiğinde 20 iş parçacığına kadar izin ver\n        }\n        worker index.php {\n            match * # diğer tüm istekler ayrı ayrı işlenir\n            num 1 # diğer istekler için minimum 1 iş parçacığı, yavaş uç noktalar asılı kalmaya başlasa bile\n            max_threads 20 # diğer istekler için gerektiğinde 20 iş parçacığına kadar izin ver\n        }\n    }\n}\n```\n\nGenel olarak, çok yavaş uç noktaları, mesaj kuyrukları gibi ilgili mekanizmalar kullanarak eşzamansız olarak ele almak da tavsiye edilir.\n"
  },
  {
    "path": "docs/tr/production.md",
    "content": "# Production Ortamına Dağıtım\n\nBu dokümanda, Docker Compose kullanarak bir PHP uygulamasını tek bir sunucuya nasıl dağıtacağımızı öğreneceğiz.\n\nSymfony kullanıyorsanız, Symfony Docker projesinin (FrankenPHP kullanan) \"[Production ortamına dağıtım](https://github.com/dunglas/symfony-docker/blob/main/docs/production.md)\" dokümanını okumayı tercih edebilirsiniz.\n\nAPI Platform (FrankenPHP de kullanır) tercih ediyorsanız, [çerçevenin dağıtım dokümanına](https://api-platform.com/docs/deployment/) bakabilirsiniz.\n\n## Uygulamanızı Hazırlama\n\nİlk olarak, PHP projenizin kök dizininde bir `Dockerfile` oluşturun:\n\n```dockerfile\nFROM dunglas/frankenphp\n\n# \"your-domain-name.example.com\" yerine kendi alan adınızı yazdığınızdan emin olun\nENV SERVER_NAME=your-domain-name.example.com\n# HTTPS'yi devre dışı bırakmak istiyorsanız, bunun yerine bu değeri kullanın:\n#ENV SERVER_NAME=:80\n\n# PHP production ayarlarını etkinleştirin\nRUN mv \"$PHP_INI_DIR/php.ini-production\" \"$PHP_INI_DIR/php.ini\"\n\n# Projenizin PHP dosyalarını genel dizine kopyalayın\nCOPY . /app/public\n# Symfony veya Laravel kullanıyorsanız, bunun yerine tüm projeyi kopyalamanız gerekir:\n#COPY . /app\n```\n\nDaha fazla ayrıntı ve seçenek için \"[Özel Docker İmajı Oluşturma](docker.md)\" bölümüne bakın,\nve yapılandırmayı nasıl özelleştireceğinizi öğrenmek için PHP eklentilerini ve Caddy modüllerini yükleyin.\n\nProjeniz Composer kullanıyorsa,\nDocker imajına dahil ettiğinizden ve bağımlılıklarınızı yüklediğinizden emin olun.\n\nArdından, bir `compose.yaml` dosyası ekleyin:\n\n```yaml\nservices:\n  php:\n    image: dunglas/frankenphp\n    restart: always\n    ports:\n      - \"80:80\" # HTTP\n      - \"443:443\" # HTTPS\n      - \"443:443/udp\" # HTTP/3\n    volumes:\n      - caddy_data:/data\n      - caddy_config:/config\n\n# Caddy sertifikaları ve yapılandırması için gereken yığınlar (volumes)\nvolumes:\n  caddy_data:\n  caddy_config:\n```\n\n> [!NOTE]\n>\n> Önceki örnekler production kullanımı için tasarlanmıştır.\n> Geliştirme aşamasında, bir yığın (volume), farklı bir PHP yapılandırması ve `SERVER_NAME` ortam değişkeni için farklı bir değer kullanmak isteyebilirsiniz.\n>\n> (FrankenPHP kullanan) çok aşamalı Composer, ekstra PHP eklentileri vb. içeren imajlara başvuran daha gelişmiş bir örnek için [Symfony Docker](https://github.com/dunglas/symfony-docker) projesine bir göz atın.\n\nSon olarak, eğer Git kullanıyorsanız, bu dosyaları commit edin ve push edin.\n\n## Sunucu Hazırlama\n\nUygulamanızı production ortamına dağıtmak için bir sunucuya ihtiyacınız vardır.\nBu dokümanda, DigitalOcean tarafından sağlanan bir sanal makine kullanacağız, ancak herhangi bir Linux sunucusu çalışabilir.\nDocker yüklü bir Linux sunucunuz varsa, doğrudan [bir sonraki bölüme](#alan-adı-yapılandırma) geçebilirsiniz.\n\nAksi takdirde, 200 $ ücretsiz kredi almak için [bu ortaklık bağlantısını](https://m.do.co/c/5d8aabe3ab80) kullanın, bir hesap oluşturun ve ardından \"Create a Droplet\" seçeneğine tıklayın.\nArdından, \"Bir imaj seçin\" bölümünün altındaki \"Marketplace\" sekmesine tıklayın ve \"Docker\" adlı uygulamayı bulun.\nBu, Docker ve Docker Compose'un en son sürümlerinin zaten yüklü olduğu bir Ubuntu sunucusu sağlayacaktır!\n\nTest amaçlı kullanım için en ucuz planlar yeterli olacaktır.\nGerçek production kullanımı için, muhtemelen ihtiyaçlarınıza uyacak şekilde \"genel amaçlı\" bölümünden bir plan seçmek isteyeceksiniz.\n\n![Docker ile DigitalOcean FrankenPHP](../digitalocean-droplet.png)\n\nDiğer ayarlar için varsayılanları koruyabilir veya ihtiyaçlarınıza göre değiştirebilirsiniz.\nSSH anahtarınızı eklemeyi veya bir parola oluşturmayı unutmayın, ardından \"Sonlandır ve oluştur\" düğmesine basın.\n\nArdından, Droplet'iniz hazırlanırken birkaç saniye bekleyin.\nDroplet'iniz hazır olduğunda, bağlanmak için SSH kullanın:\n\n```console\nssh root@<droplet-ip>\n```\n\n## Alan Adı Yapılandırma\n\nÇoğu durumda sitenizle bir alan adını ilişkilendirmek isteyeceksiniz.\nHenüz bir alan adınız yoksa, bir kayıt şirketi aracılığıyla bir alan adı satın almanız gerekir.\n\nDaha sonra alan adınız için sunucunuzun IP adresini işaret eden `A` türünde bir DNS kaydı oluşturun:\n\n```dns\nyour-domain-name.example.com.  IN  A     207.154.233.113\n```\n\nDigitalOcean Alan Adları hizmetiyle ilgili örnek (\"Networking\" > \"Domains\"):\n\n![DigitalOcean'da DNS Yapılandırma](../digitalocean-dns.png)\n\n> [!NOTE]\n>\n> FrankenPHP tarafından varsayılan olarak otomatik olarak TLS sertifikası oluşturmak için kullanılan hizmet olan Let's Encrypt, direkt IP adreslerinin kullanılmasını desteklemez. Let's Encrypt'i kullanmak için alan adı kullanmak zorunludur.\n\n## Dağıtım\n\nProjenizi `git clone`, `scp` veya ihtiyacınıza uygun başka bir araç kullanarak sunucuya kopyalayın.\nGitHub kullanıyorsanız [bir dağıtım anahtarı](https://docs.github.com/en/free-pro-team@latest/developers/overview/managing-deploy-keys#deploy-keys) kullanmak isteyebilirsiniz.\nDağıtım anahtarları ayrıca [GitLab tarafından desteklenir](https://docs.gitlab.com/ee/user/project/deploy_keys/).\n\nGit ile örnek:\n\n```console\ngit clone git@github.com:<username>/<project-name>.git\n```\n\nProjenizi içeren dizine gidin (`<proje-adı>`) ve uygulamayı production modunda başlatın:\n\n```console\ndocker compose up -d --wait\n```\n\nSunucunuz hazır ve çalışıyor. Sizin için otomatik olarak bir HTTPS sertifikası oluşturuldu.\n`https://your-domain-name.example.com` adresine gidin ve keyfini çıkarın!\n\n> [!CAUTION]\n>\n> Docker bir önbellek katmanına sahip olabilir, her dağıtım için doğru derlemeye sahip olduğunuzdan emin olun veya önbellek sorununu önlemek için projenizi `--no-cache` seçeneği ile yeniden oluşturun.\n\n## Birden Fazla Düğümde Dağıtım\n\nUygulamanızı bir makine kümesine dağıtmak istiyorsanız, [Docker Swarm](https://docs.docker.com/engine/swarm/stack-deploy/) kullanabilirsiniz,\nsağlanan Compose dosyaları ile uyumludur.\nKubernetes üzerinde dağıtım yapmak için FrankenPHP kullanan [API Platformu ile sağlanan Helm grafiğine](https://api-platform.com/docs/deployment/kubernetes/) göz atın.\n"
  },
  {
    "path": "docs/tr/static.md",
    "content": "# Statik Yapı Oluşturun\n\nPHP kütüphanesinin yerel kurulumunu kullanmak yerine,\nharika [static-php-cli projesi](https://github.com/crazywhalecc/static-php-cli) sayesinde FrankenPHP'nin statik bir yapısını oluşturmak mümkündür (adına rağmen, bu proje sadece CLI'yi değil, tüm SAPI'leri destekler).\n\nBu yöntemle, tek, taşınabilir bir ikili PHP yorumlayıcısını, Caddy web sunucusunu ve FrankenPHP'yi içerecektir!\n\nFrankenPHP ayrıca [PHP uygulamasının statik binary gömülmesini](embed.md) destekler.\n\n## Linux\n\nLinux statik binary dosyası oluşturmak için bir Docker imajı sağlıyoruz:\n\n```console\ndocker buildx bake --load static-builder\ndocker cp $(docker create --name static-builder-musl dunglas/frankenphp:static-builder-musl):/go/src/app/dist/frankenphp-linux-$(uname -m) frankenphp ; docker rm static-builder\n```\n\nElde edilen statik binary `frankenphp` olarak adlandırılır ve geçerli dizinde kullanılabilir.\n\nStatik binary dosyasını Docker olmadan oluşturmak istiyorsanız, Linux için de çalışan macOS talimatlarına bir göz atın.\n\n### Özel Eklentiler\n\nVarsayılan olarak, en popüler PHP eklentileri zaten derlenir.\n\nBinary dosyanın boyutunu küçültmek ve saldırı yüzeyini azaltmak için `PHP_EXTENSIONS` Docker ARG'sini kullanarak derlenecek eklentilerin listesini seçebilirsiniz.\n\nÖrneğin, yalnızca `opcache` eklentisini derlemek için aşağıdaki komutu çalıştırın:\n\n```console\ndocker buildx bake --load --set static-builder.args.PHP_EXTENSIONS=opcache,pdo_sqlite static-builder\n# ...\n```\n\nEtkinleştirdiğiniz eklentilere ek işlevler sağlayan kütüphaneler eklemek için `PHP_EXTENSION_LIBS` Docker ARG'sini kullanabilirsiniz:\n\n```console\ndocker buildx bake \\\n  --load \\\n  --set static-builder.args.PHP_EXTENSIONS=gd \\\n  --set static-builder.args.PHP_EXTENSION_LIBS=libjpeg,libwebp \\\n  static-builder\n```\n\n### Ekstra Caddy Modülleri\n\nEkstra Caddy modülleri eklemek veya [xcaddy](https://github.com/caddyserver/xcaddy) adresine başka argümanlar iletmek için `XCADDY_ARGS` Docker ARG'sini kullanın:\n\n```console\ndocker buildx bake \\\n  --load \\\n  --set static-builder.args.XCADDY_ARGS=\"--with github.com/darkweak/souin/plugins/caddy --with github.com/dunglas/caddy-cbrotli --with github.com/dunglas/mercure/caddy --with github.com/dunglas/vulcain/caddy\" \\\n  static-builder\n```\n\nBu örnekte, Caddy için [Souin](https://souin.io) HTTP önbellek modülünün yanı sıra [cbrotli](https://github.com/dunglas/caddy-cbrotli), [Mercure](https://mercure.rocks) ve [Vulcain](https://vulcain.rocks) modüllerini ekliyoruz.\n\n> [!TIP]\n>\n> cbrotli, Mercure ve Vulcain modülleri, `XCADDY_ARGS` boşsa veya ayarlanmamışsa varsayılan olarak dahil edilir.\n> Eğer `XCADDY_ARGS` değerini özelleştirirseniz, dahil edilmelerini istiyorsanız bunları açıkça dahil etmelisiniz.\n\nDerlemeyi nasıl [özelleştireceğinize](#yapıyı-özelleştirme) de bakın.\n\n### GitHub Token\n\nGitHub API kullanım limitine ulaşırsanız, `GITHUB_TOKEN` adlı bir ortam değişkeninde bir GitHub Personal Access Token ayarlayın:\n\n```console\nGITHUB_TOKEN=\"xxx\" docker --load buildx bake static-builder\n# ...\n```\n\n## macOS\n\nmacOS için statik bir binary oluşturmak için aşağıdaki betiği çalıştırın ([Homebrew](https://brew.sh/) yüklü olmalıdır):\n\n```console\ngit clone https://github.com/php/frankenphp\ncd frankenphp\n./build-static.sh\n```\n\nNot: Bu betik Linux'ta (ve muhtemelen diğer Unix'lerde) da çalışır ve sağladığımız Docker tabanlı statik derleyici tarafından dahili olarak kullanılır.\n\n## Yapıyı Özelleştirme\n\nAşağıdaki ortam değişkenleri `docker build` ve `build-static.sh` dosyalarına aktarılabilir\nstatik derlemeyi özelleştirmek için betik:\n\n- `FRANKENPHP_VERSION`: kullanılacak FrankenPHP sürümü\n- `PHP_VERSION`: kullanılacak PHP sürümü\n- `PHP_EXTENSIONS`: oluşturulacak PHP eklentileri ([desteklenen eklentiler listesi](https://static-php.dev/en/guide/extensions.html))\n- `PHP_EXTENSION_LIBS`: eklentilere özellikler ekleyen oluşturulacak ekstra kütüphaneler\n- `XCADDY_ARGS`: [xcaddy](https://github.com/caddyserver/xcaddy) adresine iletilecek argümanlar, örneğin ekstra Caddy modülleri eklemek için\n- `EMBED`: binary dosyaya gömülecek PHP uygulamasının yolu\n- `CLEAN`: ayarlandığında, libphp ve tüm bağımlılıkları sıfırdan oluşturulur (önbellek yok)\n- `DEBUG_SYMBOLS`: ayarlandığında, hata ayıklama sembolleri ayıklanmayacak ve binary dosyaya eklenecektir\n- `RELEASE`: (yalnızca bakımcılar) ayarlandığında, ortaya çıkan binary dosya GitHub'a yüklenecektir\n"
  },
  {
    "path": "docs/tr/worker.md",
    "content": "# FrankenPHP Worker'ları Kullanma\n\nUygulamanızı bir kez önyükleyin ve bellekte tutun.\nFrankenPHP gelen istekleri birkaç milisaniye içinde halledecektir.\n\n## Çalışan Komut Dosyalarının Başlatılması\n\n### Docker\n\n`FRANKENPHP_CONFIG` ortam değişkeninin değerini `worker /path/to/your/worker/script.php` olarak ayarlayın:\n\n```console\ndocker run \\\n    -e FRANKENPHP_CONFIG=\"worker /app/path/to/your/worker/script.php\" \\\n    -v $PWD:/app \\\n    -p 80:80 -p 443:443 -p 443:443/udp \\\n    dunglas/frankenphp\n```\n\n### Bağımsız İkili\n\nGeçerli dizinin içeriğini bir worker kullanarak sunmak için `php-server` komutunun `--worker` seçeneğini kullanın:\n\n```console\nfrankenphp php-server --worker /path/to/your/worker/script.php\n```\n\nPHP uygulamanız [ikili dosyaya gömülü](embed.md) ise, uygulamanın kök dizinine özel bir `Caddyfile` ekleyebilirsiniz.\nOtomatik olarak kullanılacaktır.\n\nDosya değişikliklerinde worker'ı yeniden başlatmak ([dosya değişikliklerini izleme](config.md#watching-for-file-changes)) `--watch` seçeneğiyle de mümkündür.\nAşağıdaki komut, `/path/to/your/app/` dizininde veya alt dizinlerde `.php` ile biten herhangi bir dosya değiştirilirse yeniden başlatmayı tetikleyecektir:\n\n```console\nfrankenphp php-server --worker /path/to/your/worker/script.php --watch=\"/path/to/your/app/**/*.php\"\n```\n\nBu özellik genellikle [hot reloading](hot-reload.md) ile birlikte kullanılır.\n\n## Symfony Çalışma Zamanı\n\n> [!TIP]\n> Bu bölüm, FrankenPHP worker moduna yerel desteğin sunulduğu Symfony 7.4 öncesi için gereklidir.\n\nFrankenPHP'nin worker modu [Symfony Runtime Component](https://symfony.com/doc/current/components/runtime.html) tarafından desteklenmektedir.\nHerhangi bir Symfony uygulamasını bir worker'da başlatmak için [PHP Runtime](https://github.com/php-runtime/runtime)'ın FrankenPHP paketini yükleyin:\n\n```console\ncomposer require runtime/frankenphp-symfony\n```\n\nFrankenPHP Symfony Runtime'ı kullanmak için `APP_RUNTIME` ortam değişkenini tanımlayarak uygulama sunucunuzu başlatın:\n\n```console\ndocker run \\\n    -e FRANKENPHP_CONFIG=\"worker ./public/index.php\" \\\n    -e APP_RUNTIME=Runtime\\\\FrankenPhpSymfony\\\\Runtime \\\n    -v $PWD:/app \\\n    -p 80:80 -p 443:443 -p 443:443/udp \\\n    dunglas/frankenphp\n```\n\n## Laravel Octane\n\nBkz. [özel dokümantasyon](laravel.md#laravel-octane).\n\n## Özel Uygulamalar\n\nAşağıdaki örnek, üçüncü taraf bir kütüphaneye güvenmeden kendi worker betiğinizi nasıl oluşturacağınızı göstermektedir:\n\n```php\n<?php\n// public/index.php\n\n// Uygulamanızı önyükleyin\nrequire __DIR__.'/vendor/autoload.php';\n\n$myApp = new \\App\\Kernel();\n$myApp->boot();\n\n// Daha iyi performans için döngü dışında işleyici (daha az iş yapıyor)\n$handler = static function () use ($myApp) {\n    try {\n        // Bir istek alındığında çağrılır,\n        // süper küresel değişkenler, php://input ve benzerleri sıfırlanır\n        echo $myApp->handle($_GET, $_POST, $_COOKIE, $_FILES, $_SERVER);\n    } catch (\\Throwable $exception) {\n        // `set_exception_handler` yalnızca worker betiği sona erdiğinde çağrılır,\n        // bu beklediğiniz gibi olmayabilir, bu yüzden istisnaları burada yakalayın ve ele alın\n        (new \\MyCustomExceptionHandler)->handleException($exception);\n    }\n};\n\n$maxRequests = (int)($_SERVER['MAX_REQUESTS'] ?? 0);\nfor ($nbRequests = 0; !$maxRequests || $nbRequests < $maxRequests; ++$nbRequests) {\n    $keepRunning = \\frankenphp_handle_request($handler);\n\n    // HTTP yanıtını gönderdikten sonra bir şey yapın\n    $myApp->terminate();\n\n    // Bir sayfa oluşturmanın ortasında tetiklenme olasılığını azaltmak için çöp toplayıcıyı çağırın\n    gc_collect_cycles();\n\n    if (!$keepRunning) break;\n}\n\n// Temizleme\n$myApp->shutdown();\n```\n\nArdından, uygulamanızı başlatın ve worker'ınızı yapılandırmak için `FRANKENPHP_CONFIG` ortam değişkenini kullanın:\n\n```console\ndocker run \\\n    -e FRANKENPHP_CONFIG=\"worker ./public/index.php\" \\\n    -v $PWD:/app \\\n    -p 80:80 -p 443:443 -p 443:443/udp \\\n    dunglas/frankenphp\n```\n\nVarsayılan olarak, CPU başına 2 worker başlatılır.\nBaşlatılacak worker sayısını da yapılandırabilirsiniz:\n\n```console\ndocker run \\\n    -e FRANKENPHP_CONFIG=\"worker ./public/index.php 42\" \\\n    -v $PWD:/app \\\n    -p 80:80 -p 443:443 -p 443:443/udp \\\n    dunglas/frankenphp\n```\n\n### Belirli Sayıda İstekten Sonra Worker'ı Yeniden Başlatın\n\nPHP başlangıçta uzun süreli işlemler için tasarlanmadığından, hala bellek sızdıran birçok kütüphane ve eski kod vardır.\nBu tür kodları worker modunda kullanmak için geçici bir çözüm, belirli sayıda isteği işledikten sonra worker betiğini yeniden başlatmaktır:\n\nÖnceki worker kod parçacığı, `MAX_REQUESTS` adlı bir ortam değişkeni ayarlayarak işlenecek maksimum istek sayısını yapılandırmaya izin verir.\n\n### Worker'ları Manuel Olarak Yeniden Başlatma\n\nWorker'ları [dosya değişikliklerinde](config.md#watching-for-file-changes) yeniden başlatmak mümkünken, tüm worker'ları [Caddy admin API](https://caddyserver.com/docs/api) aracılığıyla sorunsuz bir şekilde yeniden başlatmak da mümkündür. Yönetici [Caddyfile](config.md#caddyfile-config)'ınızda etkinleştirilmişse, yeniden başlatma uç noktasına aşağıdaki gibi basit bir POST isteği gönderebilirsiniz:\n\n```console\ncurl -X POST http://localhost:2019/frankenphp/workers/restart\n```\n\n### Worker Hataları\n\nBir worker betiği sıfır olmayan bir çıkış koduyla çökerse, FrankenPHP onu üstel bir geri çekilme (exponential backoff) stratejisiyle yeniden başlatacaktır. Worker betiği, son geri çekilme süresinin 2 katından daha uzun süre çalışır durumda kalırsa, worker betiğini cezalandırmayacak ve tekrar yeniden başlatacaktır. Ancak, worker betiği kısa bir süre içinde sıfır olmayan bir çıkış koduyla başarısız olmaya devam ederse (örneğin, bir betikte yazım hatası olması durumunda), FrankenPHP `too many consecutive failures` hatasıyla çökecektir.\n\nArdışık hata sayısı, [Caddyfile](config.md#caddyfile-config)'ınızda `max_consecutive_failures` seçeneği ile yapılandırılabilir:\n\n```caddyfile\nfrankenphp {\n    worker {\n        # ...\n        max_consecutive_failures 10\n    }\n}\n```\n\n## Süper Küresel Değişkenlerin Davranışı\n\n[PHP süper küresel değişkenleri](https://www.php.net/manual/en/language.variables.superglobals.php) (`$_SERVER`, `$_ENV`, `$_GET`...) aşağıdaki gibi davranır:\n\n- `frankenphp_handle_request()`'e ilk çağrıdan önce, süper küresel değişkenler worker betiğinin kendisine bağlı değerleri içerir\n- `frankenphp_handle_request()` çağrısı sırasında ve sonrasında, süper küresel değişkenler işlenen HTTP isteğinden üretilen değerleri içerir, `frankenphp_handle_request()`'e yapılan her çağrı süper küresel değişken değerlerini değiştirir\n\nGeri çağırım içinde worker betiğinin süper küresel değişkenlerine erişmek için, bunları kopyalamalı ve kopyayı geri çağırımın kapsamına aktarmalısınız:\n\n```php\n<?php\n// frankenphp_handle_request()'e ilk çağrıdan önce worker'ın $_SERVER süper küresel değişkenini kopyalayın\n$workerServer = $_SERVER;\n\n$handler = static function () use ($workerServer) {\n    var_dump($_SERVER); // HTTP isteğine bağlı $_SERVER\n    var_dump($workerServer); // Worker betiğinin $_SERVER'ı\n};\n\n// ...\n"
  },
  {
    "path": "docs/translate.php",
    "content": "<?php\n\n# update all translations to match the english docs\n# usage: php docs/translate.php [specific-file.md]\n# needs: php with openssl and gemini api key\n\nconst MODEL = 'gemini-2.5-flash';\nconst SLEEP_SECONDS_BETWEEN_REQUESTS = 10;\nconst LANGUAGES = [\n    'cn' => 'Chinese',\n    'fr' => 'French',\n    'ja' => 'Japanese',\n    'pt-br' => 'Portuguese (Brazilian)',\n    'ru' => 'Russian',\n    'tr' => 'Turkish',\n];\nconst SYSTEM_PROMPT = <<<PROMPT\n    You are translating the docs of the FrankenPHP server from english to other languages.\n    You will receive the english version (authoritative) and a translation (possibly incomplete or incorrect).\n    Your task is to produce a corrected and complete translation in the target language.\n    You must strictly follow these rules:\n    - You must not change the structure of the document (headings, code blocks, etc.).\n    - You must not translate code, only comments inside the code.\n    - You must not translate link urls, only links texts.\n    - You may translate anchors to translation pages (config.md#translated-anchor), keep existing anchors as they are.\n    - You must not add or remove any content, only translate what is present.\n    - You must ensure that the translation is accurate and faithful to the original meaning.\n    - You must write in a natural and fluent style, appropriate for technical documentation.\n    - You must use the correct terminology for technical terms in the target language, don't translate technical terms if unsure.\n    - You must not include any explanations or notes, only the translated document.\n    PROMPT;\n\nfunction makeGeminiRequest(string $systemPrompt, string $userPrompt, string $model, string $apiKey, int $reties = 2): string\n{\n    $url = \"https://generativelanguage.googleapis.com/v1beta/models/$model:generateContent\";\n    $body = json_encode([\n        \"contents\" => [\n            [\"role\" => \"model\", \"parts\" => ['text' => $systemPrompt]],\n            [\"role\" => \"user\", \"parts\" => ['text' => $userPrompt]]\n        ],\n    ]);\n\n    $response = @file_get_contents($url, false, stream_context_create([\n        'http' => [\n            'method' => 'POST',\n            'header' => \"Content-Type: application/json\\r\\nX-Goog-Api-Key: $apiKey\\r\\nContent-Length: \" . strlen($body) . \"\\r\\n\",\n            'content' => $body,\n            'timeout' => 300,\n        ]\n    ]));\n    $generatedDocs = json_decode($response, true)['candidates'][0]['content']['parts'][0]['text'] ?? '';\n\n    if (!$response || !$generatedDocs) {\n        print_r(error_get_last());\n        print_r($response);\n        if ($reties > 0) {\n            echo \"Retrying... ($reties retries left)\\n\";\n            sleep(SLEEP_SECONDS_BETWEEN_REQUESTS);\n            return makeGeminiRequest($systemPrompt, $userPrompt, $model, $apiKey, $reties - 1);\n        }\n        exit(1);\n    }\n\n    return $generatedDocs;\n}\n\nfunction createPrompt(string $language, string $englishFile, string $currentTranslation): array\n{\n    $languageName = LANGUAGES[$language];\n    $userPrompt = <<<PROMPT\n        Here is the english version of the document:\n        \n        ```markdown\n        $englishFile\n        ```\n        \n        Here is the current translation in $languageName:\n        \n        ```markdown\n        $currentTranslation\n        ```\n        \n        Here is the corrected and completed translation in $languageName:\n        \n        ```markdown\n        PROMPT;\n\n    return [SYSTEM_PROMPT, $userPrompt];\n}\n\nfunction sanitizeMarkdown(string $markdown): string\n{\n    if (str_starts_with($markdown, '```markdown')) {\n        $markdown = substr($markdown, strlen('```markdown'));\n    }\n    $markdown = rtrim($markdown, '`');\n    return trim($markdown) . \"\\n\";\n}\n\n$fileToTranslate = $argv;\narray_shift($fileToTranslate);\n$fileToTranslate = array_map(fn($filename) => trim($filename), $fileToTranslate);\n$apiKey = $_SERVER['GEMINI_API_KEY'] ?? $_ENV['GEMINI_API_KEY'] ?? '';\nif (!$apiKey) {\n    echo 'Enter gemini api key ($GEMINI_API_KEY): ';\n    $apiKey = trim(fgets(STDIN));\n}\n\n$files = array_filter(scandir(__DIR__), fn($filename) => str_ends_with($filename, '.md'));\nforeach ($files as $file) {\n    $englishFile = file_get_contents(__DIR__ . \"/$file\");\n    if ($fileToTranslate && !in_array($file, $fileToTranslate)) {\n        continue;\n    }\n    foreach (LANGUAGES as $language => $languageName) {\n        echo \"Translating $file to $languageName\\n\";\n        $currentTranslation = file_get_contents(__DIR__ . \"/$language/$file\") ?: '';\n        [$systemPrompt, $userPrompt] = createPrompt($language, $englishFile, $currentTranslation);\n        $markdown = makeGeminiRequest($systemPrompt, $userPrompt, MODEL, $apiKey);\n\n        echo \"Writing translated file to $language/$file\\n\";\n        file_put_contents(__DIR__ . \"/$language/$file\", sanitizeMarkdown($markdown));\n\n        echo \"sleeping to avoid rate limiting...\\n\";\n        sleep(SLEEP_SECONDS_BETWEEN_REQUESTS);\n    }\n}\n"
  },
  {
    "path": "docs/wordpress.md",
    "content": "# WordPress\n\nRun [WordPress](https://wordpress.org/) with FrankenPHP to enjoy a modern, high-performance stack with automatic HTTPS, HTTP/3, and Zstandard compression.\n\n## Minimal Installation\n\n1. [Download WordPress](https://wordpress.org/download/)\n2. Extract the ZIP archive and open a terminal in the extracted directory\n3. Run:\n\n   ```console\n   frankenphp php-server\n   ```\n\n4. Go to `http://localhost/wp-admin/` and follow the installation instructions\n5. Enjoy!\n\nFor a production-ready setup, prefer using `frankenphp run` with a `Caddyfile` like this one:\n\n```caddyfile\nexample.com\n\nphp_server\nencode zstd br gzip\nlog\n```\n\n## Hot Reload\n\nTo use the [hot reload](hot-reload.md) feature with WordPress, enable [Mercure](mercure.md) and add the `hot_reload` sub-directive to the `php_server` directive in your `Caddyfile`:\n\n```caddyfile\nlocalhost\n\nmercure {\n    anonymous\n}\n\nphp_server {\n    hot_reload\n}\n```\n\nThen, add the code needed to load the JavaScript libraries in the `functions.php` file of your WordPress theme:\n\n```php\nfunction hot_reload() {\n    ?>\n    <?php if (isset($_SERVER['FRANKENPHP_HOT_RELOAD'])): ?>\n        <meta name=\"frankenphp-hot-reload:url\" content=\"<?=$_SERVER['FRANKENPHP_HOT_RELOAD']?>\">\n        <script src=\"https://cdn.jsdelivr.net/npm/idiomorph\"></script>\n        <script src=\"https://cdn.jsdelivr.net/npm/frankenphp-hot-reload/+esm\" type=\"module\"></script>\n    <?php endif ?>\n    <?php\n}\nadd_action('wp_head', 'hot_reload');\n```\n\nFinally, run `frankenphp run` from the WordPress root directory.\n"
  },
  {
    "path": "docs/worker.md",
    "content": "# Using FrankenPHP Workers\n\nBoot your application once and keep it in memory.\nFrankenPHP will handle incoming requests in a few milliseconds.\n\n## Starting Worker Scripts\n\n### Docker\n\nSet the value of the `FRANKENPHP_CONFIG` environment variable to `worker /path/to/your/worker/script.php`:\n\n```console\ndocker run \\\n    -e FRANKENPHP_CONFIG=\"worker /app/path/to/your/worker/script.php\" \\\n    -v $PWD:/app \\\n    -p 80:80 -p 443:443 -p 443:443/udp \\\n    dunglas/frankenphp\n```\n\n### Standalone Binary\n\nUse the `--worker` option of the `php-server` command to serve the content of the current directory using a worker:\n\n```console\nfrankenphp php-server --worker /path/to/your/worker/script.php\n```\n\nIf your PHP app is [embedded in the binary](embed.md), you can add a custom `Caddyfile` in the root directory of the app.\nIt will be used automatically.\n\nIt's also possible to [restart the worker on file changes](config.md#watching-for-file-changes) with the `--watch` option.\nThe following command will trigger a restart if any file ending in `.php` in the `/path/to/your/app/` directory or subdirectories is modified:\n\n```console\nfrankenphp php-server --worker /path/to/your/worker/script.php --watch=\"/path/to/your/app/**/*.php\"\n```\n\nThis feature is often used in combination with [hot reloading](hot-reload.md).\n\n## Symfony Runtime\n\n> [!TIP]\n> The following section is only necessary prior to Symfony 7.4, where native support for FrankenPHP worker mode was introduced.\n\nThe worker mode of FrankenPHP is supported by the [Symfony Runtime Component](https://symfony.com/doc/current/components/runtime.html).\nTo start any Symfony application in a worker, install the FrankenPHP package of [PHP Runtime](https://github.com/php-runtime/runtime):\n\n```console\ncomposer require runtime/frankenphp-symfony\n```\n\nStart your app server by defining the `APP_RUNTIME` environment variable to use the FrankenPHP Symfony Runtime:\n\n```console\ndocker run \\\n    -e FRANKENPHP_CONFIG=\"worker ./public/index.php\" \\\n    -e APP_RUNTIME=Runtime\\\\FrankenPhpSymfony\\\\Runtime \\\n    -v $PWD:/app \\\n    -p 80:80 -p 443:443 -p 443:443/udp \\\n    dunglas/frankenphp\n```\n\n## Laravel Octane\n\nSee [the dedicated documentation](laravel.md#laravel-octane).\n\n## Custom Apps\n\nThe following example shows how to create your own worker script without relying on a third-party library:\n\n```php\n<?php\n// public/index.php\n\n// Boot your app\nrequire __DIR__.'/vendor/autoload.php';\n\n$myApp = new \\App\\Kernel();\n$myApp->boot();\n\n// Handler outside the loop for better performance (doing less work)\n$handler = static function () use ($myApp) {\n    try {\n        // Called when a request is received,\n        // superglobals, php://input and the like are reset\n        echo $myApp->handle($_GET, $_POST, $_COOKIE, $_FILES, $_SERVER);\n    } catch (\\Throwable $exception) {\n        // `set_exception_handler` is called only when the worker script ends,\n        // which may not be what you expect, so catch and handle exceptions here\n        (new \\MyCustomExceptionHandler)->handleException($exception);\n    }\n};\n\n$maxRequests = (int)($_SERVER['MAX_REQUESTS'] ?? 0);\nfor ($nbRequests = 0; !$maxRequests || $nbRequests < $maxRequests; ++$nbRequests) {\n    $keepRunning = \\frankenphp_handle_request($handler);\n\n    // Do something after sending the HTTP response\n    $myApp->terminate();\n\n    // Call the garbage collector to reduce the chances of it being triggered in the middle of a page generation\n    gc_collect_cycles();\n\n    if (!$keepRunning) break;\n}\n\n// Cleanup\n$myApp->shutdown();\n```\n\nThen, start your app and use the `FRANKENPHP_CONFIG` environment variable to configure your worker:\n\n```console\ndocker run \\\n    -e FRANKENPHP_CONFIG=\"worker ./public/index.php\" \\\n    -v $PWD:/app \\\n    -p 80:80 -p 443:443 -p 443:443/udp \\\n    dunglas/frankenphp\n```\n\nBy default, 2 workers per CPU are started.\nYou can also configure the number of workers to start:\n\n```console\ndocker run \\\n    -e FRANKENPHP_CONFIG=\"worker ./public/index.php 42\" \\\n    -v $PWD:/app \\\n    -p 80:80 -p 443:443 -p 443:443/udp \\\n    dunglas/frankenphp\n```\n\n### Restart the Worker After a Certain Number of Requests\n\nAs PHP was not originally designed for long-running processes, there are still many libraries and legacy codes that leak memory.\nA workaround to using this type of code in worker mode is to restart the worker script after processing a certain number of requests:\n\nThe previous worker snippet allows configuring a maximum number of request to handle by setting an environment variable named `MAX_REQUESTS`.\n\n### Restart Workers Manually\n\nWhile it's possible to restart workers [on file changes](config.md#watching-for-file-changes), it's also possible to restart all workers\ngracefully via the [Caddy admin API](https://caddyserver.com/docs/api). If the admin is enabled in your\n[Caddyfile](config.md#caddyfile-config), you can ping the restart endpoint with a simple POST request like this:\n\n```console\ncurl -X POST http://localhost:2019/frankenphp/workers/restart\n```\n\n### Worker Failures\n\nIf a worker script crashes with a non-zero exit code, FrankenPHP will restart it with an exponential backoff strategy.\nIf the worker script stays up longer than the last backoff \\* 2,\nit will not penalize the worker script and restart it again.\nHowever, if the worker script continues to fail with a non-zero exit code in a short period of time\n(for example, having a typo in a script), FrankenPHP will crash with the error: `too many consecutive failures`.\n\nThe number of consecutive failures can be configured in your [Caddyfile](config.md#caddyfile-config) with the `max_consecutive_failures` option:\n\n```caddyfile\nfrankenphp {\n    worker {\n        # ...\n        max_consecutive_failures 10\n    }\n}\n```\n\n## Superglobals Behavior\n\n[PHP superglobals](https://www.php.net/manual/en/language.variables.superglobals.php) (`$_SERVER`, `$_ENV`, `$_GET`...)\nbehave as follows:\n\n- before the first call to `frankenphp_handle_request()`, superglobals contain values bound to the worker script itself\n- during and after the call to `frankenphp_handle_request()`, superglobals contain values generated from the processed HTTP request, each call to `frankenphp_handle_request()` changes the superglobals values\n\nTo access the superglobals of the worker script inside the callback, you must copy them and import the copy in the scope of the callback:\n\n```php\n<?php\n// Copy worker's $_SERVER superglobal before the first call to frankenphp_handle_request()\n$workerServer = $_SERVER;\n\n$handler = static function () use ($workerServer) {\n    var_dump($_SERVER); // Request-bound $_SERVER\n    var_dump($workerServer); // $_SERVER of the worker script\n};\n\n// ...\n```\n"
  },
  {
    "path": "docs/x-sendfile.md",
    "content": "# Efficiently Serving Large Static Files (`X-Sendfile`/`X-Accel-Redirect`)\n\nUsually, static files can be served directly by the web server,\nbut sometimes it's necessary to execute some PHP code before sending them:\naccess control, statistics, custom HTTP headers...\n\nUnfortunately, using PHP to serve large static files is inefficient compared to\ndirect use of the web server (memory overload, reduced performance...).\n\nFrankenPHP lets you delegate the sending of static files to the web server\n**after** executing customized PHP code.\n\nTo do this, your PHP application simply needs to define a custom HTTP header\ncontaining the path of the file to be served. FrankenPHP takes care of the rest.\n\nThis feature is known as **`X-Sendfile`** for Apache, and **`X-Accel-Redirect`** for NGINX.\n\nIn the following examples, we assume that the document root of the project is the `public/` directory.\nand that we want to use PHP to serve files stored outside the `public/` directory,\nfrom a directory named `private-files/`.\n\n## Configuration\n\nFirst, add the following configuration to your `Caddyfile` to enable this feature:\n\n```patch\n\troot public/\n\t# ...\n\n+\t# Needed for Symfony, Laravel and other projects using the Symfony HttpFoundation component\n+\trequest_header X-Sendfile-Type x-accel-redirect\n+\trequest_header X-Accel-Mapping ../private-files=/private-files\n+\n+\tintercept {\n+\t\t@accel header X-Accel-Redirect *\n+\t\thandle_response @accel {\n+\t\t\troot private-files/\n+\t\t\trewrite * {resp.header.X-Accel-Redirect}\n+\t\t\tmethod * GET\n+\n+\t\t\t# Remove the X-Accel-Redirect header set by PHP for increased security\n+\t\t\theader -X-Accel-Redirect\n+\n+\t\t\tfile_server\n+\t\t}\n+\t}\n\n\tphp_server\n```\n\n## Plain PHP\n\nSet the relative file path (from `private-files/`) as the value of the `X-Accel-Redirect` header:\n\n```php\nheader('X-Accel-Redirect: file.txt');\n```\n\n## Projects using the Symfony HttpFoundation component (Symfony, Laravel, Drupal...)\n\nSymfony HttpFoundation [natively supports this feature](https://symfony.com/doc/current/components/http_foundation.html#serving-files).\nIt will automatically determine the correct value for the `X-Accel-Redirect` header and add it to the response.\n\n```php\nuse Symfony\\Component\\HttpFoundation\\BinaryFileResponse;\n\nBinaryFileResponse::trustXSendfileTypeHeader();\n$response = new BinaryFileResponse(__DIR__.'/../private-files/file.txt');\n\n// ...\n```\n"
  },
  {
    "path": "embed.go",
    "content": "package frankenphp\n\nimport (\n\t\"archive/tar\"\n\t\"bytes\"\n\t_ \"embed\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/fs\"\n\t\"log\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\t\"time\"\n)\n\n// EmbeddedAppPath contains the path of the embedded PHP application (empty if none).\n// It can be set at build time using -ldflags to override the default extraction path:\n//\n//\tgo build -ldflags \"-X github.com/dunglas/frankenphp.EmbeddedAppPath=/app\" ...\n//\n// When set, the embedded app is extracted to this fixed path instead of a temp\n// directory with a checksum suffix. This is useful when the app contains\n// pre-compiled artifacts (e.g. OPcache file cache) that reference absolute paths\n// and need a predictable extraction location.\nvar EmbeddedAppPath string\n\n//go:embed app.tar\nvar embeddedApp []byte\n\n//go:embed app_checksum.txt\nvar embeddedAppChecksum []byte\n\nfunc init() {\n\tif len(embeddedApp) == 0 {\n\t\t// No embedded app\n\t\treturn\n\t}\n\n\tif EmbeddedAppPath == \"\" {\n\t\tEmbeddedAppPath = filepath.Join(os.TempDir(), \"frankenphp_\"+string(embeddedAppChecksum))\n\t}\n\n\tif err := untar(EmbeddedAppPath); err != nil {\n\t\t_ = os.RemoveAll(EmbeddedAppPath)\n\t\tpanic(err)\n\t}\n}\n\n// untar reads the tar file from r and writes it into dir.\n//\n// Adapted from https://github.com/golang/build/blob/master/cmd/buildlet/buildlet.go\nfunc untar(dir string) (err error) {\n\tt0 := time.Now()\n\tnFiles := 0\n\tmadeDir := map[string]bool{}\n\n\ttr := tar.NewReader(bytes.NewReader(embeddedApp))\n\tloggedChtimesError := false\n\tfor {\n\t\tf, err := tr.Next()\n\t\tif err == io.EOF {\n\t\t\tbreak\n\t\t}\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"tar error: %w\", err)\n\t\t}\n\t\tif f.Typeflag == tar.TypeXGlobalHeader {\n\t\t\t// golang.org/issue/22748: git archive exports\n\t\t\t// a global header ('g') which after Go 1.9\n\t\t\t// (for a bit?) contained an empty filename.\n\t\t\t// Ignore it.\n\t\t\tcontinue\n\t\t}\n\t\trel, err := nativeRelPath(f.Name)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"tar file contained invalid name %q: %v\", f.Name, err)\n\t\t}\n\t\tabs := filepath.Join(dir, rel)\n\n\t\tfi := f.FileInfo()\n\t\tmode := fi.Mode()\n\t\tswitch {\n\t\tcase mode.IsRegular():\n\t\t\t// Make the directory. This is redundant because it should\n\t\t\t// already be made by a directory entry in the tar\n\t\t\t// beforehand. Thus, don't check for errors; the next\n\t\t\t// write will fail with the same error.\n\t\t\tdir := filepath.Dir(abs)\n\t\t\tif !madeDir[dir] {\n\t\t\t\tif err := os.MkdirAll(filepath.Dir(abs), mode.Perm()); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tmadeDir[dir] = true\n\t\t\t}\n\t\t\tif runtime.GOOS == \"darwin\" && mode&0111 != 0 {\n\t\t\t\t// See comment in writeFile.\n\t\t\t\terr := os.Remove(abs)\n\t\t\t\tif err != nil && !errors.Is(err, fs.ErrNotExist) {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\t\tif _, err := os.Stat(abs); os.IsNotExist(err) {\n\t\t\t\twf, err := os.OpenFile(abs, os.O_RDWR|os.O_CREATE|os.O_TRUNC, mode.Perm())\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tn, err := io.Copy(wf, tr)\n\t\t\t\tif closeErr := wf.Close(); closeErr != nil && err == nil {\n\t\t\t\t\terr = closeErr\n\t\t\t\t}\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"error writing to %s: %v\", abs, err)\n\t\t\t\t}\n\t\t\t\tif n != f.Size {\n\t\t\t\t\treturn fmt.Errorf(\"only wrote %d bytes to %s; expected %d\", n, abs, f.Size)\n\t\t\t\t}\n\t\t\t\tmodTime := f.ModTime\n\t\t\t\tif modTime.After(t0) {\n\t\t\t\t\t// Clamp modtimes at system time. See\n\t\t\t\t\t// golang.org/issue/19062 when clock on\n\t\t\t\t\t// buildlet was behind the gitmirror server\n\t\t\t\t\t// doing the git-archive.\n\t\t\t\t\tmodTime = t0\n\t\t\t\t}\n\t\t\t\tif !modTime.IsZero() {\n\t\t\t\t\tif err := os.Chtimes(abs, modTime, modTime); err != nil && !loggedChtimesError {\n\t\t\t\t\t\t// benign error. Gerrit doesn't even set the\n\t\t\t\t\t\t// modtime in these, and we don't end up relying\n\t\t\t\t\t\t// on it anywhere (the gomote push command relies\n\t\t\t\t\t\t// on digests only), so this is a little pointless\n\t\t\t\t\t\t// for now.\n\t\t\t\t\t\tlog.Printf(\"error changing modtime: %v (further Chtimes errors suppressed)\", err)\n\t\t\t\t\t\tloggedChtimesError = true // once is enough\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tnFiles++\n\t\tcase mode.IsDir():\n\t\t\tif err := os.MkdirAll(abs, mode.Perm()); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tmadeDir[abs] = true\n\t\tcase mode&os.ModeSymlink != 0:\n\t\t\t// TODO: ignore these for now. They were breaking x/build tests.\n\t\t\t// Implement these if/when we ever have a test that needs them.\n\t\t\t// But maybe we'd have to skip creating them on Windows for some builders\n\t\t\t// without permissions.\n\t\tdefault:\n\t\t\treturn fmt.Errorf(\"tar file entry %s contained unsupported file type %v\", f.Name, mode)\n\t\t}\n\t}\n\treturn nil\n}\n\n// nativeRelPath verifies that p is a non-empty relative path\n// using either slashes or the buildlet's native path separator,\n// and returns it canonicalized to the native path separator.\nfunc nativeRelPath(p string) (string, error) {\n\tif p == \"\" {\n\t\treturn \"\", errors.New(\"path not provided\")\n\t}\n\n\tif filepath.Separator != '/' && strings.Contains(p, string(filepath.Separator)) {\n\t\tclean := filepath.Clean(p)\n\t\tif filepath.IsAbs(clean) {\n\t\t\treturn \"\", fmt.Errorf(\"path %q is not relative\", p)\n\t\t}\n\t\tif clean == \"..\" || strings.HasPrefix(clean, \"..\"+string(filepath.Separator)) {\n\t\t\treturn \"\", fmt.Errorf(\"path %q refers to a parent directory\", p)\n\t\t}\n\t\tif strings.HasPrefix(p, string(filepath.Separator)) || filepath.VolumeName(clean) != \"\" {\n\t\t\t// On Windows, this catches semi-relative paths like \"C:\" (meaning “the\n\t\t\t// current working directory on volume C:”) and \"\\windows\" (meaning “the\n\t\t\t// windows subdirectory of the current drive letter”).\n\t\t\treturn \"\", fmt.Errorf(\"path %q is relative to volume\", p)\n\t\t}\n\t\treturn p, nil\n\t}\n\n\tclean := path.Clean(p)\n\tif path.IsAbs(clean) {\n\t\treturn \"\", fmt.Errorf(\"path %q is not relative\", p)\n\t}\n\tif clean == \"..\" || strings.HasPrefix(clean, \"../\") {\n\t\treturn \"\", fmt.Errorf(\"path %q refers to a parent directory\", p)\n\t}\n\tcanon := filepath.FromSlash(p)\n\tif filepath.VolumeName(canon) != \"\" {\n\t\treturn \"\", fmt.Errorf(\"path %q begins with a native volume name\", p)\n\t}\n\treturn canon, nil\n}\n"
  },
  {
    "path": "env.go",
    "content": "package frankenphp\n\n// #include \"frankenphp.h\"\n// #include \"types.h\"\nimport \"C\"\nimport (\n\t\"os\"\n\t\"strings\"\n)\n\nvar lengthOfEnv = 0\n\n//export go_init_os_env\nfunc go_init_os_env(mainThreadEnv *C.zend_array) {\n\tfullEnv := os.Environ()\n\tlengthOfEnv = len(fullEnv)\n\n\tfor _, envVar := range fullEnv {\n\t\tkey, val, _ := strings.Cut(envVar, \"=\")\n\t\tzkey := newPersistentZendString(key)\n\t\tzStr := newPersistentZendString(val)\n\t\tC.__hash_update_string__(mainThreadEnv, zkey, zStr)\n\t}\n}\n\n//export go_putenv\nfunc go_putenv(name *C.char, nameLen C.int, val *C.char, valLen C.int) C.bool {\n\tgoName := C.GoStringN(name, nameLen)\n\n\tif val == nil {\n\t\t// If no \"=\" is present, unset the environment variable\n\t\treturn C.bool(os.Unsetenv(goName) == nil)\n\t}\n\n\tgoVal := C.GoStringN(val, valLen)\n\treturn C.bool(os.Setenv(goName, goVal) == nil)\n}\n"
  },
  {
    "path": "ext.go",
    "content": "package frankenphp\n\n// #include \"frankenphp.h\"\nimport \"C\"\nimport (\n\t\"sync\"\n\t\"unsafe\"\n)\n\nvar (\n\textensions   []*C.zend_module_entry\n\tregisterOnce sync.Once\n)\n\n// RegisterExtension registers a new PHP extension.\nfunc RegisterExtension(me unsafe.Pointer) {\n\textensions = append(extensions, (*C.zend_module_entry)(me))\n}\n\nfunc registerExtensions() {\n\tif len(extensions) == 0 {\n\t\treturn\n\t}\n\n\tregisterOnce.Do(func() {\n\t\tC.register_extensions((**C.zend_module_entry)(unsafe.Pointer(&extensions[0])), C.int(len(extensions)))\n\t\textensions = nil\n\t})\n}\n"
  },
  {
    "path": "frankenphp.c",
    "content": "#include \"frankenphp.h\"\n#include <SAPI.h>\n#include <Zend/zend_alloc.h>\n#include <Zend/zend_exceptions.h>\n#include <Zend/zend_interfaces.h>\n#include <errno.h>\n#include <ext/spl/spl_exceptions.h>\n#include <ext/standard/head.h>\n#ifdef HAVE_PHP_SESSION\n#include <ext/session/php_session.h>\n#endif\n#include <inttypes.h>\n#include <php.h>\n#ifdef PHP_WIN32\n#include <config.w32.h>\n#else\n#include <php_config.h>\n#endif\n#include <php_ini.h>\n#include <php_main.h>\n#include <php_output.h>\n#include <php_variables.h>\n#include <pthread.h>\n#include <sapi/embed/php_embed.h>\n#include <signal.h>\n#include <stdint.h>\n#include <stdio.h>\n#include <stdlib.h>\n#ifndef ZEND_WIN32\n#include <unistd.h>\n#endif\n#if defined(__linux__)\n#include <sys/prctl.h>\n#elif defined(__FreeBSD__) || defined(__OpenBSD__)\n#include <pthread_np.h>\n#endif\n\n#include \"_cgo_export.h\"\n#include \"frankenphp_arginfo.h\"\n\n#if defined(PHP_WIN32) && defined(ZTS)\nZEND_TSRMLS_CACHE_DEFINE()\n#endif\n\n/**\n * The list of modules to reload on each request. If an external module\n * requires to be reloaded between requests, it is possible to hook on\n * `sapi_module.activate` and `sapi_module.deactivate`.\n *\n * @see https://github.com/DataDog/dd-trace-php/pull/3169 for an example\n */\nstatic const char *MODULES_TO_RELOAD[] = {\"filter\", NULL};\n\nfrankenphp_version frankenphp_get_version() {\n  return (frankenphp_version){\n      PHP_MAJOR_VERSION, PHP_MINOR_VERSION, PHP_RELEASE_VERSION,\n      PHP_EXTRA_VERSION, PHP_VERSION,       PHP_VERSION_ID,\n  };\n}\n\nfrankenphp_config frankenphp_get_config() {\n  return (frankenphp_config){\n#ifdef ZTS\n      true,\n#else\n      false,\n#endif\n#ifdef ZEND_SIGNALS\n      true,\n#else\n      false,\n#endif\n#ifdef ZEND_MAX_EXECUTION_TIMERS\n      true,\n#else\n      false,\n#endif\n  };\n}\n\nbool should_filter_var = 0;\nbool original_user_abort_setting = 0;\nfrankenphp_interned_strings_t frankenphp_strings = {0};\nHashTable *main_thread_env = NULL;\n\n__thread uintptr_t thread_index;\n__thread bool is_worker_thread = false;\n__thread HashTable *sandboxed_env = NULL;\n\nvoid frankenphp_update_local_thread_context(bool is_worker) {\n  is_worker_thread = is_worker;\n\n  /* workers should keep running if the user aborts the connection */\n  PG(ignore_user_abort) = is_worker ? 1 : original_user_abort_setting;\n}\n\nstatic void frankenphp_update_request_context() {\n  /* the server context is stored on the go side, still SG(server_context) needs\n   * to not be NULL */\n  SG(server_context) = (void *)1;\n  /* status It is not reset by zend engine, set it to 200. */\n  SG(sapi_headers).http_response_code = 200;\n\n  char *authorization_header =\n      go_update_request_info(thread_index, &SG(request_info));\n\n  /* let PHP handle basic auth */\n  php_handle_auth_data(authorization_header);\n}\n\nstatic void frankenphp_free_request_context() {\n  if (SG(request_info).cookie_data != NULL) {\n    free(SG(request_info).cookie_data);\n    SG(request_info).cookie_data = NULL;\n  }\n\n  /* freed via thread.Unpin() */\n  SG(request_info).request_method = NULL;\n  SG(request_info).query_string = NULL;\n  SG(request_info).content_type = NULL;\n  SG(request_info).path_translated = NULL;\n  SG(request_info).request_uri = NULL;\n}\n\n/* reset all 'auto globals' in worker mode except of $_ENV\n * see: php_hash_environment() */\nstatic void frankenphp_reset_super_globals() {\n  zend_try {\n    /* only $_FILES needs to be flushed explicitly\n     * $_GET, $_POST, $_COOKIE and $_SERVER are flushed on reimport\n     * $_ENV is not flushed\n     * for more info see: php_startup_auto_globals()\n     */\n    zval *files = &PG(http_globals)[TRACK_VARS_FILES];\n    zval_ptr_dtor_nogc(files);\n    memset(files, 0, sizeof(*files));\n\n    /* $_SESSION must be explicitly deleted from the symbol table.\n     * Unlike other superglobals, $_SESSION is stored in EG(symbol_table)\n     * with a reference to PS(http_session_vars). The session RSHUTDOWN\n     * only decrements the refcount but doesn't remove it from the symbol\n     * table, causing data to leak between requests. */\n    zend_hash_str_del(&EG(symbol_table), \"_SESSION\", sizeof(\"_SESSION\") - 1);\n  }\n  zend_end_try();\n\n  zend_auto_global *auto_global;\n  zend_string *_env = ZSTR_KNOWN(ZEND_STR_AUTOGLOBAL_ENV);\n  zend_string *_server = ZSTR_KNOWN(ZEND_STR_AUTOGLOBAL_SERVER);\n  ZEND_HASH_MAP_FOREACH_PTR(CG(auto_globals), auto_global) {\n    if (auto_global->name == _env) {\n      /* skip $_ENV */\n    } else if (auto_global->name == _server) {\n      /* always reimport $_SERVER */\n      auto_global->armed = auto_global->auto_global_callback(auto_global->name);\n    } else if (auto_global->jit) {\n      /* JIT globals ($_REQUEST, $GLOBALS) need special handling:\n       * - $GLOBALS will always be handled by the application, we skip it\n       * For $_REQUEST:\n       * - If in symbol_table: re-initialize with current request data\n       * - If not: do nothing, it may be armed by jit later */\n      if (auto_global->name == ZSTR_KNOWN(ZEND_STR_AUTOGLOBAL_REQUEST) &&\n          zend_hash_exists(&EG(symbol_table), auto_global->name)) {\n        auto_global->armed =\n            auto_global->auto_global_callback(auto_global->name);\n      }\n    } else if (auto_global->auto_global_callback) {\n      /* $_GET, $_POST, $_COOKIE, $_FILES are reimported here */\n      auto_global->armed = auto_global->auto_global_callback(auto_global->name);\n    } else {\n      /* $_SESSION will land here (not an http_global) */\n      auto_global->armed = 0;\n    }\n  }\n  ZEND_HASH_FOREACH_END();\n}\n\n/*\n * free php_stream resources that are temporary (php_stream_temp_ops)\n * streams are globally registered in EG(regular_list), see zend_list.c\n * this fixes a leak when reading the body of a request\n */\nstatic void frankenphp_release_temporary_streams() {\n  zend_resource *val;\n  int stream_type = php_file_le_stream();\n  ZEND_HASH_FOREACH_PTR(&EG(regular_list), val) {\n    /* verify the resource is a stream */\n    if (val->type == stream_type) {\n      php_stream *stream = (php_stream *)val->ptr;\n      if (stream != NULL && stream->ops == &php_stream_temp_ops &&\n          stream->__exposed == 0 && GC_REFCOUNT(val) == 1) {\n        ZEND_ASSERT(!stream->is_persistent);\n        zend_list_delete(val);\n      }\n    }\n  }\n  ZEND_HASH_FOREACH_END();\n}\n\n#ifdef HAVE_PHP_SESSION\n/* Reset session state between worker requests, preserving user handlers.\n * Based on php_rshutdown_session_globals() + php_rinit_session_globals(). */\nstatic void frankenphp_reset_session_state(void) {\n  if (PS(session_status) == php_session_active) {\n    php_session_flush(1);\n  }\n\n  if (!Z_ISUNDEF(PS(http_session_vars))) {\n    zval_ptr_dtor(&PS(http_session_vars));\n    ZVAL_UNDEF(&PS(http_session_vars));\n  }\n\n  if (PS(mod_data) || PS(mod_user_implemented)) {\n    zend_try { PS(mod)->s_close(&PS(mod_data)); }\n    zend_end_try();\n  }\n\n  if (PS(id)) {\n    zend_string_release_ex(PS(id), 0);\n    PS(id) = NULL;\n  }\n\n  if (PS(session_vars)) {\n    zend_string_release_ex(PS(session_vars), 0);\n    PS(session_vars) = NULL;\n  }\n\n  /* PS(mod_user_class_name) and PS(mod_user_names) are preserved */\n\n#if PHP_VERSION_ID >= 80300\n  if (PS(session_started_filename)) {\n    zend_string_release(PS(session_started_filename));\n    PS(session_started_filename) = NULL;\n    PS(session_started_lineno) = 0;\n  }\n#endif\n\n  PS(session_status) = php_session_none;\n  PS(in_save_handler) = 0;\n  PS(set_handler) = 0;\n  PS(mod_data) = NULL;\n  PS(mod_user_is_open) = 0;\n  PS(define_sid) = 1;\n}\n#endif\n\n/* Adapted from php_request_shutdown */\nstatic void frankenphp_worker_request_shutdown() {\n  /* Flush all output buffers */\n  zend_try { php_output_end_all(); }\n  zend_end_try();\n\n  const char **module_name;\n  zend_module_entry *module;\n  for (module_name = MODULES_TO_RELOAD; *module_name; module_name++) {\n    if ((module = zend_hash_str_find_ptr(&module_registry, *module_name,\n                                         strlen(*module_name)))) {\n      module->request_shutdown_func(module->type, module->module_number);\n    }\n  }\n\n#ifdef HAVE_PHP_SESSION\n  frankenphp_reset_session_state();\n#endif\n\n  /* Shutdown output layer (send the set HTTP headers, cleanup output handlers,\n   * etc.) */\n  zend_try { php_output_deactivate(); }\n  zend_end_try();\n\n  /* SAPI related shutdown (free stuff) */\n  zend_try { sapi_deactivate(); }\n  zend_end_try();\n  frankenphp_free_request_context();\n\n  zend_set_memory_limit(PG(memory_limit));\n}\n\n// shutdown the dummy request that starts the worker script\nbool frankenphp_shutdown_dummy_request(void) {\n  if (SG(server_context) == NULL) {\n    return false;\n  }\n\n  frankenphp_worker_request_shutdown();\n\n  return true;\n}\n\nvoid get_full_env(zval *track_vars_array) {\n  zend_hash_extend(Z_ARR_P(track_vars_array),\n                   zend_hash_num_elements(main_thread_env), 0);\n  zend_hash_copy(Z_ARR_P(track_vars_array), main_thread_env, NULL);\n}\n\n/* Adapted from php_request_startup() */\nstatic int frankenphp_worker_request_startup() {\n  int retval = SUCCESS;\n\n  frankenphp_update_request_context();\n\n  zend_try {\n    frankenphp_release_temporary_streams();\n    php_output_activate();\n\n    /* initialize global variables */\n    PG(header_is_being_sent) = 0;\n    PG(connection_status) = PHP_CONNECTION_NORMAL;\n\n    /* Keep the current execution context */\n    sapi_activate();\n\n#ifdef ZEND_MAX_EXECUTION_TIMERS\n    if (PG(max_input_time) == -1) {\n      zend_set_timeout(EG(timeout_seconds), 1);\n    } else {\n      zend_set_timeout(PG(max_input_time), 1);\n    }\n#endif\n\n    if (PG(expose_php)) {\n      sapi_add_header(SAPI_PHP_VERSION_HEADER,\n                      sizeof(SAPI_PHP_VERSION_HEADER) - 1, 1);\n    }\n\n    if (PG(output_handler) && PG(output_handler)[0]) {\n      zval oh;\n\n      ZVAL_STRING(&oh, PG(output_handler));\n      php_output_start_user(&oh, 0, PHP_OUTPUT_HANDLER_STDFLAGS);\n      zval_ptr_dtor(&oh);\n    } else if (PG(output_buffering)) {\n      php_output_start_user(NULL,\n                            PG(output_buffering) > 1 ? PG(output_buffering) : 0,\n                            PHP_OUTPUT_HANDLER_STDFLAGS);\n    } else if (PG(implicit_flush)) {\n      php_output_set_implicit_flush(1);\n    }\n\n    frankenphp_reset_super_globals();\n\n    const char **module_name;\n    zend_module_entry *module;\n    for (module_name = MODULES_TO_RELOAD; *module_name; module_name++) {\n      if ((module = zend_hash_str_find_ptr(&module_registry, *module_name,\n                                           strlen(*module_name))) &&\n          module->request_startup_func) {\n        module->request_startup_func(module->type, module->module_number);\n      }\n    }\n  }\n  zend_catch { retval = FAILURE; }\n  zend_end_try();\n\n  SG(sapi_started) = 1;\n\n  return retval;\n}\n\nPHP_FUNCTION(frankenphp_finish_request) { /* {{{ */\n  ZEND_PARSE_PARAMETERS_NONE();\n\n  if (go_is_context_done(thread_index)) {\n    RETURN_FALSE;\n  }\n\n  php_output_end_all();\n  php_header();\n\n  go_frankenphp_finish_php_request(thread_index);\n\n  RETURN_TRUE;\n} /* }}} */\n\n/* {{{ Call go's putenv to prevent race conditions */\nPHP_FUNCTION(frankenphp_putenv) {\n  char *setting;\n  size_t setting_len;\n\n  ZEND_PARSE_PARAMETERS_START(1, 1)\n  Z_PARAM_STRING(setting, setting_len)\n  ZEND_PARSE_PARAMETERS_END();\n\n  // Cast str_len to int (ensure it fits in an int)\n  if (setting_len > INT_MAX) {\n    php_error(E_WARNING, \"String length exceeds maximum integer value\");\n    RETURN_FALSE;\n  }\n\n  if (setting_len == 0 || setting[0] == '=') {\n    zend_argument_value_error(1, \"must have a valid syntax\");\n    RETURN_THROWS();\n  }\n\n  if (sandboxed_env == NULL) {\n    sandboxed_env = zend_array_dup(main_thread_env);\n  }\n\n  /* cut at null byte to stay consistent with regular putenv */\n  char *null_pos = memchr(setting, '\\0', setting_len);\n  if (null_pos != NULL) {\n    setting_len = null_pos - setting;\n  }\n\n  /* cut the string at the first '=' */\n  char *eq_pos = memchr(setting, '=', setting_len);\n  bool success = true;\n\n  /* no '=' found, delete the variable */\n  if (eq_pos == NULL) {\n    success = go_putenv(setting, (int)setting_len, NULL, 0);\n    if (success) {\n      zend_hash_str_del(sandboxed_env, setting, setting_len);\n    }\n\n    RETURN_BOOL(success);\n  }\n\n  size_t name_len = eq_pos - setting;\n  size_t value_len =\n      (setting_len > name_len + 1) ? (setting_len - name_len - 1) : 0;\n  success = go_putenv(setting, (int)name_len, eq_pos + 1, (int)value_len);\n  if (success) {\n    zval val = {0};\n    ZVAL_STRINGL(&val, eq_pos + 1, value_len);\n    zend_hash_str_update(sandboxed_env, setting, name_len, &val);\n  }\n\n  RETURN_BOOL(success);\n} /* }}} */\n\n/* {{{ Get the env from the sandboxed environment */\nPHP_FUNCTION(frankenphp_getenv) {\n  zend_string *name = NULL;\n  bool local_only = 0;\n\n  ZEND_PARSE_PARAMETERS_START(0, 2)\n  Z_PARAM_OPTIONAL\n  Z_PARAM_STR_OR_NULL(name)\n  Z_PARAM_BOOL(local_only)\n  ZEND_PARSE_PARAMETERS_END();\n\n  HashTable *ht = sandboxed_env ? sandboxed_env : main_thread_env;\n\n  if (!name) {\n    RETURN_ARR(zend_array_dup(ht));\n    return;\n  }\n\n  zval *env_val = zend_hash_find(ht, name);\n  if (env_val && Z_TYPE_P(env_val) == IS_STRING) {\n    zend_string *str = Z_STR_P(env_val);\n    zend_string_addref(str);\n    RETVAL_STR(str);\n  } else {\n    RETVAL_FALSE;\n  }\n} /* }}} */\n\n/* {{{ Fetch all HTTP request headers */\nPHP_FUNCTION(frankenphp_request_headers) {\n  ZEND_PARSE_PARAMETERS_NONE();\n\n  struct go_apache_request_headers_return headers =\n      go_apache_request_headers(thread_index);\n\n  array_init_size(return_value, headers.r1);\n\n  for (size_t i = 0; i < headers.r1; i++) {\n    go_string key = headers.r0[i * 2];\n    go_string val = headers.r0[i * 2 + 1];\n\n    add_assoc_stringl_ex(return_value, key.data, key.len, val.data, val.len);\n  }\n}\n/* }}} */\n\n/* add_response_header and apache_response_headers are copied from\n * https://github.com/php/php-src/blob/master/sapi/cli/php_cli_server.c\n * Copyright (c) The PHP Group\n * Licensed under The PHP License\n * Original authors: Moriyoshi Koizumi <moriyoshi@php.net> and Xinchen Hui\n * <laruence@php.net>\n */\nstatic void add_response_header(sapi_header_struct *h,\n                                zval *return_value) /* {{{ */\n{\n  if (h->header_len > 0) {\n    char *s;\n    size_t len = 0;\n    ALLOCA_FLAG(use_heap)\n\n    char *p = strchr(h->header, ':');\n    if (NULL != p) {\n      len = p - h->header;\n    }\n    if (len > 0) {\n      while (len != 0 &&\n             (h->header[len - 1] == ' ' || h->header[len - 1] == '\\t')) {\n        len--;\n      }\n      if (len) {\n        s = do_alloca(len + 1, use_heap);\n        memcpy(s, h->header, len);\n        s[len] = 0;\n        do {\n          p++;\n        } while (*p == ' ' || *p == '\\t');\n        add_assoc_stringl_ex(return_value, s, len, p,\n                             h->header_len - (p - h->header));\n        free_alloca(s, use_heap);\n      }\n    }\n  }\n}\n/* }}} */\n\nPHP_FUNCTION(frankenphp_response_headers) /* {{{ */\n{\n  ZEND_PARSE_PARAMETERS_NONE();\n\n  array_init(return_value);\n  zend_llist_apply_with_argument(\n      &SG(sapi_headers).headers,\n      (llist_apply_with_arg_func_t)add_response_header, return_value);\n}\n/* }}} */\n\nPHP_FUNCTION(frankenphp_handle_request) {\n  zend_fcall_info fci;\n  zend_fcall_info_cache fcc;\n\n  ZEND_PARSE_PARAMETERS_START(1, 1)\n  Z_PARAM_FUNC(fci, fcc)\n  ZEND_PARSE_PARAMETERS_END();\n\n  if (!is_worker_thread) {\n    /* not a worker, throw an error */\n    zend_throw_exception(\n        spl_ce_RuntimeException,\n        \"frankenphp_handle_request() called while not in worker mode\", 0);\n    RETURN_THROWS();\n  }\n\n#ifdef ZEND_MAX_EXECUTION_TIMERS\n  /* Disable timeouts while waiting for a request to handle */\n  zend_unset_timeout();\n#endif\n\n  struct go_frankenphp_worker_handle_request_start_return result =\n      go_frankenphp_worker_handle_request_start(thread_index);\n  if (frankenphp_worker_request_startup() == FAILURE\n      /* Shutting down */\n      || !result.r0) {\n    RETURN_FALSE;\n  }\n\n#ifdef ZEND_MAX_EXECUTION_TIMERS\n  /*\n   * Reset default timeout\n   */\n  if (PG(max_input_time) != -1) {\n    zend_set_timeout(INI_INT(\"max_execution_time\"), 0);\n  }\n#endif\n\n  /* Call the PHP func passed to frankenphp_handle_request() */\n  zval retval = {0};\n  zval *callback_ret = NULL;\n\n  fci.size = sizeof fci;\n  fci.retval = &retval;\n  fci.params = result.r1;\n  fci.param_count = result.r1 == NULL ? 0 : 1;\n\n  if (zend_call_function(&fci, &fcc) == SUCCESS && Z_TYPE(retval) != IS_UNDEF) {\n    callback_ret = &retval;\n\n    /* pass NULL instead of the NULL zval as return value */\n    if (Z_TYPE(retval) == IS_NULL) {\n      callback_ret = NULL;\n    }\n  }\n\n  /*\n   * If an exception occurred, print the message to the client before\n   * closing the connection.\n   */\n  if (EG(exception)) {\n    if (!zend_is_unwind_exit(EG(exception)) &&\n        !zend_is_graceful_exit(EG(exception))) {\n      zend_exception_error(EG(exception), E_ERROR);\n    } else {\n      /* exit() will jump directly to after php_execute_script */\n      zend_bailout();\n    }\n  }\n\n  frankenphp_worker_request_shutdown();\n  go_frankenphp_finish_worker_request(thread_index, callback_ret);\n  if (result.r1 != NULL) {\n    zval_ptr_dtor(result.r1);\n  }\n  if (callback_ret != NULL) {\n    zval_ptr_dtor(&retval);\n  }\n\n  RETURN_TRUE;\n}\n\nPHP_FUNCTION(headers_send) {\n  zend_long response_code = 200;\n\n  ZEND_PARSE_PARAMETERS_START(0, 1)\n  Z_PARAM_OPTIONAL\n  Z_PARAM_LONG(response_code)\n  ZEND_PARSE_PARAMETERS_END();\n\n  int previous_status_code = SG(sapi_headers).http_response_code;\n  SG(sapi_headers).http_response_code = response_code;\n\n  if (response_code >= 100 && response_code < 200) {\n    int ret = sapi_module.send_headers(&SG(sapi_headers));\n    SG(sapi_headers).http_response_code = previous_status_code;\n\n    RETURN_LONG(ret);\n  }\n\n  RETURN_LONG(sapi_send_headers());\n}\n\nPHP_FUNCTION(mercure_publish) {\n  zval *topics;\n  zend_string *data = NULL, *id = NULL, *type = NULL;\n  zend_bool private = 0;\n  zend_long retry = 0;\n  bool retry_is_null = 1;\n\n  ZEND_PARSE_PARAMETERS_START(1, 6)\n  Z_PARAM_ZVAL(topics)\n  Z_PARAM_OPTIONAL\n  Z_PARAM_STR_OR_NULL(data)\n  Z_PARAM_BOOL(private)\n  Z_PARAM_STR_OR_NULL(id)\n  Z_PARAM_STR_OR_NULL(type)\n  Z_PARAM_LONG_OR_NULL(retry, retry_is_null)\n  ZEND_PARSE_PARAMETERS_END();\n\n  if (Z_TYPE_P(topics) != IS_ARRAY && Z_TYPE_P(topics) != IS_STRING) {\n    zend_argument_type_error(1, \"must be of type array|string\");\n    RETURN_THROWS();\n  }\n\n  struct go_mercure_publish_return result =\n      go_mercure_publish(thread_index, topics, data, private, id, type, retry);\n\n  switch (result.r1) {\n  case 0:\n    RETURN_STR(result.r0);\n  case 1:\n    zend_throw_exception(spl_ce_RuntimeException, \"No Mercure hub configured\",\n                         0);\n    RETURN_THROWS();\n  case 2:\n    zend_throw_exception(spl_ce_RuntimeException, \"Publish failed\", 0);\n    RETURN_THROWS();\n  }\n\n  zend_throw_exception(spl_ce_RuntimeException,\n                       \"FrankenPHP not built with Mercure support\", 0);\n  RETURN_THROWS();\n}\n\nPHP_FUNCTION(frankenphp_log) {\n  zend_string *message = NULL;\n  zend_long level = 0;\n  zval *context = NULL;\n\n  ZEND_PARSE_PARAMETERS_START(1, 3)\n  Z_PARAM_STR(message)\n  Z_PARAM_OPTIONAL\n  Z_PARAM_LONG(level)\n  Z_PARAM_ARRAY(context)\n  ZEND_PARSE_PARAMETERS_END();\n\n  char *ret = NULL;\n  ret = go_log_attrs(thread_index, message, level, context);\n  if (ret != NULL) {\n    zend_throw_exception(spl_ce_RuntimeException, ret, 0);\n    free(ret);\n    RETURN_THROWS();\n  }\n}\n\nPHP_MINIT_FUNCTION(frankenphp) {\n  register_frankenphp_symbols(module_number);\n\n  zend_function *func;\n\n  // Override putenv\n  func = zend_hash_str_find_ptr(CG(function_table), \"putenv\",\n                                sizeof(\"putenv\") - 1);\n  if (func != NULL && func->type == ZEND_INTERNAL_FUNCTION) {\n    ((zend_internal_function *)func)->handler = ZEND_FN(frankenphp_putenv);\n  } else {\n    php_error(E_WARNING, \"Failed to find built-in putenv function\");\n  }\n\n  // Override getenv\n  func = zend_hash_str_find_ptr(CG(function_table), \"getenv\",\n                                sizeof(\"getenv\") - 1);\n  if (func != NULL && func->type == ZEND_INTERNAL_FUNCTION) {\n    ((zend_internal_function *)func)->handler = ZEND_FN(frankenphp_getenv);\n  } else {\n    php_error(E_WARNING, \"Failed to find built-in getenv function\");\n  }\n\n  return SUCCESS;\n}\n\nstatic zend_module_entry frankenphp_module = {\n    STANDARD_MODULE_HEADER,\n    \"frankenphp\",\n    ext_functions,         /* function table */\n    PHP_MINIT(frankenphp), /* initialization */\n    NULL,                  /* shutdown */\n    NULL,                  /* request initialization */\n    NULL,                  /* request shutdown */\n    NULL,                  /* information */\n    TOSTRING(FRANKENPHP_VERSION),\n    STANDARD_MODULE_PROPERTIES};\n\nstatic int frankenphp_startup(sapi_module_struct *sapi_module) {\n  php_import_environment_variables = get_full_env;\n\n  return php_module_startup(sapi_module, &frankenphp_module);\n}\n\nstatic int frankenphp_deactivate(void) { return SUCCESS; }\n\nstatic size_t frankenphp_ub_write(const char *str, size_t str_length) {\n  struct go_ub_write_return result =\n      go_ub_write(thread_index, (char *)str, str_length);\n\n  if (result.r1) {\n    php_handle_aborted_connection();\n  }\n\n  return result.r0;\n}\n\nstatic int frankenphp_send_headers(sapi_headers_struct *sapi_headers) {\n  if (SG(request_info).no_headers == 1) {\n    return SAPI_HEADER_SENT_SUCCESSFULLY;\n  }\n\n  int status;\n\n  if (SG(sapi_headers).http_status_line) {\n    status = atoi((SG(sapi_headers).http_status_line) + 9);\n  } else {\n    status = SG(sapi_headers).http_response_code;\n\n    if (!status) {\n      status = 200;\n    }\n  }\n\n  bool success = go_write_headers(thread_index, status, &sapi_headers->headers);\n  if (success) {\n    return SAPI_HEADER_SENT_SUCCESSFULLY;\n  }\n\n  return SAPI_HEADER_SEND_FAILED;\n}\n\nstatic void frankenphp_sapi_flush(void *server_context) {\n  sapi_send_headers();\n  if (go_sapi_flush(thread_index)) {\n    php_handle_aborted_connection();\n  }\n}\n\nstatic size_t frankenphp_read_post(char *buffer, size_t count_bytes) {\n  return go_read_post(thread_index, buffer, count_bytes);\n}\n\nstatic char *frankenphp_read_cookies(void) {\n  return go_read_cookies(thread_index);\n}\n\n/* all variables with well defined keys can safely be registered like this */\nstatic inline void frankenphp_register_trusted_var(zend_string *z_key,\n                                                   char *value, size_t val_len,\n                                                   HashTable *ht) {\n  if (value == NULL) {\n    zval empty;\n    ZVAL_EMPTY_STRING(&empty);\n    zend_hash_update_ind(ht, z_key, &empty);\n    return;\n  }\n  size_t new_val_len = val_len;\n\n  if (!should_filter_var ||\n      sapi_module.input_filter(PARSE_SERVER, ZSTR_VAL(z_key), &value,\n                               new_val_len, &new_val_len)) {\n    zval z_value;\n    ZVAL_STRINGL_FAST(&z_value, value, new_val_len);\n    zend_hash_update_ind(ht, z_key, &z_value);\n  }\n}\n\n/* Register known $_SERVER variables in bulk to avoid cgo overhead */\nvoid frankenphp_register_server_vars(zval *track_vars_array,\n                                     frankenphp_server_vars vars) {\n  HashTable *ht = Z_ARRVAL_P(track_vars_array);\n  zend_hash_extend(ht, vars.total_num_vars, 0);\n  zend_hash_copy(ht, main_thread_env, NULL);\n\n  // update values with variable strings\n#define FRANKENPHP_REGISTER_VAR(name)                                          \\\n  frankenphp_register_trusted_var(frankenphp_strings.name, vars.name,          \\\n                                  vars.name##_len, ht)\n\n  FRANKENPHP_REGISTER_VAR(remote_addr);\n  FRANKENPHP_REGISTER_VAR(remote_host);\n  FRANKENPHP_REGISTER_VAR(remote_port);\n  FRANKENPHP_REGISTER_VAR(document_root);\n  FRANKENPHP_REGISTER_VAR(path_info);\n  FRANKENPHP_REGISTER_VAR(php_self);\n  FRANKENPHP_REGISTER_VAR(document_uri);\n  FRANKENPHP_REGISTER_VAR(script_filename);\n  FRANKENPHP_REGISTER_VAR(script_name);\n  FRANKENPHP_REGISTER_VAR(ssl_cipher);\n  FRANKENPHP_REGISTER_VAR(server_name);\n  FRANKENPHP_REGISTER_VAR(server_port);\n  FRANKENPHP_REGISTER_VAR(content_length);\n  FRANKENPHP_REGISTER_VAR(server_protocol);\n  FRANKENPHP_REGISTER_VAR(http_host);\n  FRANKENPHP_REGISTER_VAR(request_uri);\n\n#undef FRANKENPHP_REGISTER_VAR\n\n  /* update values with hard-coded zend_strings */\n  zval zv;\n  ZVAL_STR(&zv, frankenphp_strings.cgi11);\n  zend_hash_update_ind(ht, frankenphp_strings.gateway_interface, &zv);\n  ZVAL_STR(&zv, frankenphp_strings.frankenphp);\n  zend_hash_update_ind(ht, frankenphp_strings.server_software, &zv);\n  ZVAL_STR(&zv, vars.request_scheme);\n  zend_hash_update_ind(ht, frankenphp_strings.request_scheme, &zv);\n  ZVAL_STR(&zv, vars.ssl_protocol);\n  zend_hash_update_ind(ht, frankenphp_strings.ssl_protocol, &zv);\n  ZVAL_STR(&zv, vars.https);\n  zend_hash_update_ind(ht, frankenphp_strings.https, &zv);\n\n  /* update values with always empty strings */\n  ZVAL_EMPTY_STRING(&zv);\n  zend_hash_update_ind(ht, frankenphp_strings.auth_type, &zv);\n  zend_hash_update_ind(ht, frankenphp_strings.remote_ident, &zv);\n}\n\n/** Create an immutable zend_string that lasts for the whole process **/\nzend_string *frankenphp_init_persistent_string(const char *string, size_t len) {\n  /* persistent strings will be ignored by the GC at the end of a request */\n  zend_string *z_string = zend_string_init(string, len, 1);\n  zend_string_hash_val(z_string);\n\n  /* interned strings will not be ref counted by the GC */\n  GC_ADD_FLAGS(z_string, IS_STR_INTERNED);\n\n  return z_string;\n}\n\n/* initialize all hard-coded zend_strings once per process */\nstatic void frankenphp_init_interned_strings(void) {\n  if (frankenphp_strings.remote_addr != NULL) {\n    return; /* already initialized */\n  }\n\n#define F_INITIALIZE_FIELD(name, str)                                          \\\n  frankenphp_strings.name =                                                    \\\n      frankenphp_init_persistent_string(str, sizeof(str) - 1);\n\n  FRANKENPHP_INTERNED_STRINGS_LIST(F_INITIALIZE_FIELD)\n#undef F_INITIALIZE_FIELD\n}\n\n/* Register variables from SG(request_info) into $_SERVER */\nstatic inline void\nfrankenphp_register_variable_from_request_info(zend_string *zKey, char *value,\n                                               bool must_be_present,\n                                               zval *track_vars_array) {\n  if (value != NULL) {\n    frankenphp_register_trusted_var(zKey, value, strlen(value),\n                                    Z_ARRVAL_P(track_vars_array));\n  } else if (must_be_present) {\n    frankenphp_register_trusted_var(zKey, NULL, 0,\n                                    Z_ARRVAL_P(track_vars_array));\n  }\n}\n\nstatic void\nfrankenphp_register_variables_from_request_info(zval *track_vars_array) {\n  frankenphp_register_variable_from_request_info(\n      frankenphp_strings.content_type, (char *)SG(request_info).content_type,\n      true, track_vars_array);\n  frankenphp_register_variable_from_request_info(\n      frankenphp_strings.path_translated,\n      (char *)SG(request_info).path_translated, false, track_vars_array);\n  frankenphp_register_variable_from_request_info(\n      frankenphp_strings.query_string, SG(request_info).query_string, true,\n      track_vars_array);\n  frankenphp_register_variable_from_request_info(\n      frankenphp_strings.remote_user, (char *)SG(request_info).auth_user, false,\n      track_vars_array);\n  frankenphp_register_variable_from_request_info(\n      frankenphp_strings.request_method,\n      (char *)SG(request_info).request_method, false, track_vars_array);\n}\n\n/* Only hard-coded keys may be registered this way */\nvoid frankenphp_register_known_variable(zend_string *z_key, char *value,\n                                        size_t val_len,\n                                        zval *track_vars_array) {\n  frankenphp_register_trusted_var(z_key, value, val_len,\n                                  Z_ARRVAL_P(track_vars_array));\n}\n\n/* variables with user-defined keys must be registered safely\n * see: php_variables.c -> php_register_variable_ex (#1106) */\nvoid frankenphp_register_variable_safe(char *key, char *val, size_t val_len,\n                                       zval *track_vars_array) {\n  if (key == NULL) {\n    return;\n  }\n  if (val == NULL) {\n    val = \"\";\n  }\n  size_t new_val_len = val_len;\n  if (!should_filter_var ||\n      sapi_module.input_filter(PARSE_SERVER, key, &val, new_val_len,\n                               &new_val_len)) {\n    php_register_variable_safe(key, val, new_val_len, track_vars_array);\n  }\n}\n\nstatic inline void register_server_variable_filtered(const char *key,\n                                                     char **val,\n                                                     size_t *val_len,\n                                                     zval *track_vars_array) {\n  if (sapi_module.input_filter(PARSE_SERVER, key, val, *val_len, val_len)) {\n    php_register_variable_safe(key, *val, *val_len, track_vars_array);\n  }\n}\n\nstatic void frankenphp_register_variables(zval *track_vars_array) {\n  /* https://www.php.net/manual/en/reserved.variables.server.php */\n\n  /* In CGI mode, the environment is part of the $_SERVER variables.\n   * $_SERVER and $_ENV should only contain values from the original\n   * environment, not values added though putenv\n   */\n  /* import environment and CGI variables from the request context in go */\n  go_register_server_variables(thread_index, track_vars_array);\n\n  /* Some variables are already present in SG(request_info) */\n  frankenphp_register_variables_from_request_info(track_vars_array);\n}\n\nstatic void frankenphp_log_message(const char *message, int syslog_type_int) {\n  go_log(thread_index, (char *)message, syslog_type_int);\n}\n\nstatic char *frankenphp_getenv(const char *name, size_t name_len) {\n  HashTable *ht = sandboxed_env ? sandboxed_env : main_thread_env;\n\n  zval *env_val = zend_hash_str_find(ht, name, name_len);\n  if (env_val && Z_TYPE_P(env_val) == IS_STRING) {\n    zend_string *str = Z_STR_P(env_val);\n    return ZSTR_VAL(str);\n  }\n\n  return NULL;\n}\n\nsapi_module_struct frankenphp_sapi_module = {\n    \"frankenphp\", /* name */\n    \"FrankenPHP\", /* pretty name */\n\n    frankenphp_startup,          /* startup */\n    php_module_shutdown_wrapper, /* shutdown */\n\n    NULL,                  /* activate */\n    frankenphp_deactivate, /* deactivate */\n\n    frankenphp_ub_write,   /* unbuffered write */\n    frankenphp_sapi_flush, /* flush */\n    NULL,                  /* get uid */\n    frankenphp_getenv,     /* getenv */\n\n    php_error, /* error handler */\n\n    NULL,                    /* header handler */\n    frankenphp_send_headers, /* send headers handler */\n    NULL,                    /* send header handler */\n\n    frankenphp_read_post,    /* read POST data */\n    frankenphp_read_cookies, /* read Cookies */\n\n    frankenphp_register_variables, /* register server variables */\n    frankenphp_log_message,        /* Log message */\n    NULL,                          /* Get request time */\n    NULL,                          /* Child terminate */\n\n    STANDARD_SAPI_MODULE_PROPERTIES};\n\n/* Sets thread name for profiling and debugging.\n *\n * Adapted from https://github.com/Pithikos/C-Thread-Pool\n * Copyright: Johan Hanssen Seferidis\n * License: MIT\n */\nstatic void set_thread_name(char *thread_name) {\n#if defined(__linux__)\n  /* Use prctl instead to prevent using _GNU_SOURCE flag and implicit\n   * declaration */\n  prctl(PR_SET_NAME, thread_name);\n#elif defined(__APPLE__) && defined(__MACH__)\n  pthread_setname_np(thread_name);\n#elif defined(__FreeBSD__) || defined(__OpenBSD__)\n  pthread_set_name_np(pthread_self(), thread_name);\n#endif\n}\n\nstatic void *php_thread(void *arg) {\n  thread_index = (uintptr_t)arg;\n  char thread_name[16] = {0};\n  snprintf(thread_name, 16, \"php-%\" PRIxPTR, thread_index);\n  set_thread_name(thread_name);\n\n#ifdef ZTS\n  /* initial resource fetch */\n  (void)ts_resource(0);\n#ifdef PHP_WIN32\n  ZEND_TSRMLS_CACHE_UPDATE();\n#endif\n#endif\n\n  // loop until Go signals to stop\n  char *scriptName = NULL;\n  while ((scriptName = go_frankenphp_before_script_execution(thread_index))) {\n    go_frankenphp_after_script_execution(thread_index,\n                                         frankenphp_execute_script(scriptName));\n  }\n\n#ifdef ZTS\n  ts_free_thread();\n#endif\n\n  go_frankenphp_on_thread_shutdown(thread_index);\n\n  return NULL;\n}\n\nstatic void *php_main(void *arg) {\n#ifndef ZEND_WIN32\n  /*\n   * SIGPIPE must be masked in non-Go threads:\n   * https://pkg.go.dev/os/signal#hdr-Go_programs_that_use_cgo_or_SWIG\n   */\n  sigset_t set;\n  sigemptyset(&set);\n  sigaddset(&set, SIGPIPE);\n\n  if (pthread_sigmask(SIG_BLOCK, &set, NULL) != 0) {\n    perror(\"failed to block SIGPIPE\");\n    exit(EXIT_FAILURE);\n  }\n#endif\n\n  set_thread_name(\"php-main\");\n\n#ifdef ZTS\n#if (PHP_VERSION_ID >= 80300)\n  php_tsrm_startup_ex((intptr_t)arg);\n#else\n  php_tsrm_startup();\n#endif\n/*tsrm_error_set(TSRM_ERROR_LEVEL_INFO, NULL);*/\n#ifdef PHP_WIN32\n  ZEND_TSRMLS_CACHE_UPDATE();\n#endif\n#endif\n\n  sapi_startup(&frankenphp_sapi_module);\n\n  /* TODO: adapted from https://github.com/php/php-src/pull/16958, remove when\n   * merged. */\n#ifdef PHP_WIN32\n  {\n    const DWORD flags = GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |\n                        GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT;\n    HMODULE module;\n    /* Use a larger buffer to support long module paths on Windows. */\n    wchar_t filename[32768];\n    if (GetModuleHandleExW(flags, (LPCWSTR)&frankenphp_sapi_module, &module)) {\n      const DWORD filename_capacity = (DWORD)_countof(filename);\n      DWORD len = GetModuleFileNameW(module, filename, filename_capacity);\n      if (len > 0 && len < filename_capacity) {\n        wchar_t *slash = wcsrchr(filename, L'\\\\');\n        if (slash) {\n          *slash = L'\\0';\n          if (!SetDllDirectoryW(filename)) {\n            fprintf(stderr, \"Warning: SetDllDirectoryW failed (error %lu)\\n\",\n                    GetLastError());\n          }\n        }\n      }\n    }\n  }\n#endif\n\n#ifdef ZEND_MAX_EXECUTION_TIMERS\n  /* overwrite php.ini with custom user settings */\n  char *php_ini_overrides = go_get_custom_php_ini(false);\n#else\n  /* overwrite php.ini with custom user settings and disable\n   * max_execution_timers */\n  char *php_ini_overrides = go_get_custom_php_ini(true);\n#endif\n\n  if (php_ini_overrides != NULL) {\n    frankenphp_sapi_module.ini_entries = php_ini_overrides;\n  }\n\n  frankenphp_init_interned_strings();\n\n  /* take a snapshot of the environment for sandboxing */\n  if (main_thread_env == NULL) {\n    main_thread_env = pemalloc(sizeof(HashTable), 1);\n    zend_hash_init(main_thread_env, 8, NULL, NULL, 1);\n    go_init_os_env(main_thread_env);\n  }\n\n  frankenphp_sapi_module.startup(&frankenphp_sapi_module);\n\n  /* check if a default filter is set in php.ini and only filter if\n   * it is, this is deprecated and will be removed in PHP 9 */\n  char *default_filter;\n  cfg_get_string(\"filter.default\", &default_filter);\n  should_filter_var = default_filter != NULL;\n  original_user_abort_setting = PG(ignore_user_abort);\n\n  go_frankenphp_main_thread_is_ready();\n\n  /* channel closed, shutdown gracefully */\n  frankenphp_sapi_module.shutdown(&frankenphp_sapi_module);\n\n  sapi_shutdown();\n#ifdef ZTS\n  tsrm_shutdown();\n#endif\n\n  if (frankenphp_sapi_module.ini_entries) {\n    free((char *)frankenphp_sapi_module.ini_entries);\n    frankenphp_sapi_module.ini_entries = NULL;\n  }\n\n  go_frankenphp_shutdown_main_thread();\n\n  return NULL;\n}\n\nint frankenphp_new_main_thread(int num_threads) {\n  pthread_t thread;\n\n  if (pthread_create(&thread, NULL, &php_main, (void *)(intptr_t)num_threads) !=\n      0) {\n    return -1;\n  }\n\n  return pthread_detach(thread);\n}\n\nbool frankenphp_new_php_thread(uintptr_t thread_index) {\n  pthread_t thread;\n  if (pthread_create(&thread, NULL, &php_thread, (void *)thread_index) != 0) {\n    return false;\n  }\n  pthread_detach(thread);\n  return true;\n}\n\nstatic int frankenphp_request_startup() {\n  frankenphp_update_request_context();\n  if (php_request_startup() == SUCCESS) {\n    return SUCCESS;\n  }\n\n  php_request_shutdown((void *)0);\n  frankenphp_free_request_context();\n\n  return FAILURE;\n}\n\nint frankenphp_execute_script(char *file_name) {\n  if (frankenphp_request_startup() == FAILURE) {\n\n    return FAILURE;\n  }\n\n  int status = SUCCESS;\n\n  zend_file_handle file_handle;\n  zend_stream_init_filename(&file_handle, file_name);\n\n  file_handle.primary_script = 1;\n\n  zend_first_try {\n    EG(exit_status) = 0;\n    php_execute_script(&file_handle);\n    status = EG(exit_status);\n  }\n  zend_catch { status = EG(exit_status); }\n  zend_end_try();\n\n  zend_destroy_file_handle(&file_handle);\n\n  /* Reset the sandboxed environment if it is in use */\n  if (sandboxed_env != NULL) {\n    zend_hash_release(sandboxed_env);\n    sandboxed_env = NULL;\n  }\n\n  php_request_shutdown((void *)0);\n  frankenphp_free_request_context();\n\n  return status;\n}\n\n/* Use global variables to store CLI arguments to prevent useless allocations */\nstatic char *cli_script;\nstatic int cli_argc;\nstatic char **cli_argv;\n\n/*\n * CLI code is adapted from\n * https://github.com/php/php-src/blob/master/sapi/cli/php_cli.c Copyright (c)\n * The PHP Group Licensed under The PHP License Original uthors: Edin Kadribasic\n * <edink@php.net>, Marcus Boerger <helly@php.net> and Johannes Schlueter\n * <johannes@php.net> Parts based on CGI SAPI Module by Rasmus Lerdorf, Stig\n * Bakken and Zeev Suraski\n */\nstatic void cli_register_file_handles(void) {\n  php_stream *s_in, *s_out, *s_err;\n  php_stream_context *sc_in = NULL, *sc_out = NULL, *sc_err = NULL;\n  zend_constant ic, oc, ec;\n\n  s_in = php_stream_open_wrapper_ex(\"php://stdin\", \"rb\", 0, NULL, sc_in);\n  s_out = php_stream_open_wrapper_ex(\"php://stdout\", \"wb\", 0, NULL, sc_out);\n  s_err = php_stream_open_wrapper_ex(\"php://stderr\", \"wb\", 0, NULL, sc_err);\n\n  /* Release stream resources, but don't free the underlying handles. Othewrise,\n   * extensions which write to stderr or company during mshutdown/gshutdown\n   * won't have the expected functionality.\n   */\n  if (s_in)\n    s_in->flags |= PHP_STREAM_FLAG_NO_RSCR_DTOR_CLOSE;\n  if (s_out)\n    s_out->flags |= PHP_STREAM_FLAG_NO_RSCR_DTOR_CLOSE;\n  if (s_err)\n    s_err->flags |= PHP_STREAM_FLAG_NO_RSCR_DTOR_CLOSE;\n\n  if (s_in == NULL || s_out == NULL || s_err == NULL) {\n    if (s_in)\n      php_stream_close(s_in);\n    if (s_out)\n      php_stream_close(s_out);\n    if (s_err)\n      php_stream_close(s_err);\n    return;\n  }\n\n  /*s_in_process = s_in;*/\n\n  php_stream_to_zval(s_in, &ic.value);\n  php_stream_to_zval(s_out, &oc.value);\n  php_stream_to_zval(s_err, &ec.value);\n\n  ZEND_CONSTANT_SET_FLAGS(&ic, CONST_CS, 0);\n  ic.name = zend_string_init_interned(\"STDIN\", sizeof(\"STDIN\") - 1, 0);\n  zend_register_constant(&ic);\n\n  ZEND_CONSTANT_SET_FLAGS(&oc, CONST_CS, 0);\n  oc.name = zend_string_init_interned(\"STDOUT\", sizeof(\"STDOUT\") - 1, 0);\n  zend_register_constant(&oc);\n\n  ZEND_CONSTANT_SET_FLAGS(&ec, CONST_CS, 0);\n  ec.name = zend_string_init_interned(\"STDERR\", sizeof(\"STDERR\") - 1, 0);\n  zend_register_constant(&ec);\n}\n\nstatic void sapi_cli_register_variables(zval *track_vars_array) /* {{{ */\n{\n  size_t len = strlen(cli_script);\n  char *docroot = \"\";\n\n  /*\n   * In CGI mode, we consider the environment to be a part of the server\n   * variables\n   */\n  php_import_environment_variables(track_vars_array);\n\n  /* Build the special-case PHP_SELF variable for the CLI version */\n  register_server_variable_filtered(\"PHP_SELF\", &cli_script, &len,\n                                    track_vars_array);\n  register_server_variable_filtered(\"SCRIPT_NAME\", &cli_script, &len,\n                                    track_vars_array);\n\n  /* filenames are empty for stdin */\n  register_server_variable_filtered(\"SCRIPT_FILENAME\", &cli_script, &len,\n                                    track_vars_array);\n  register_server_variable_filtered(\"PATH_TRANSLATED\", &cli_script, &len,\n                                    track_vars_array);\n\n  /* just make it available */\n  len = 0U;\n  register_server_variable_filtered(\"DOCUMENT_ROOT\", &docroot, &len,\n                                    track_vars_array);\n}\n/* }}} */\n\nstatic void *execute_script_cli(void *arg) {\n  void *exit_status;\n  bool eval = (bool)arg;\n\n  /*\n   * The SAPI name \"cli\" is hardcoded into too many programs... let's usurp it.\n   */\n  php_embed_module.name = \"cli\";\n  php_embed_module.pretty_name = \"PHP CLI embedded in FrankenPHP\";\n  php_embed_module.register_server_variables = sapi_cli_register_variables;\n\n  php_embed_init(cli_argc, cli_argv);\n\n  cli_register_file_handles();\n  zend_first_try {\n    if (eval) {\n      /* evaluate the cli_script as literal PHP code (php-cli -r \"...\") */\n      zend_eval_string_ex(cli_script, NULL, \"Command line code\", 1);\n    } else {\n      zend_file_handle file_handle;\n      zend_stream_init_filename(&file_handle, cli_script);\n\n      CG(skip_shebang) = 1;\n      php_execute_script(&file_handle);\n    }\n  }\n  zend_end_try();\n\n  exit_status = (void *)(intptr_t)EG(exit_status);\n\n  php_embed_shutdown();\n\n  return exit_status;\n}\n\nint frankenphp_execute_script_cli(char *script, int argc, char **argv,\n                                  bool eval) {\n  pthread_t thread;\n  int err;\n  void *exit_status;\n\n  cli_script = script;\n  cli_argc = argc;\n  cli_argv = argv;\n\n  /*\n   * Start the script in a dedicated thread to prevent conflicts between Go and\n   * PHP signal handlers\n   */\n  err = pthread_create(&thread, NULL, execute_script_cli, (void *)eval);\n  if (err != 0) {\n    return err;\n  }\n\n  err = pthread_join(thread, &exit_status);\n  if (err != 0) {\n    return err;\n  }\n\n  return (intptr_t)exit_status;\n}\n\nint frankenphp_reset_opcache(void) {\n  zend_function *opcache_reset =\n      zend_hash_str_find_ptr(CG(function_table), ZEND_STRL(\"opcache_reset\"));\n  if (opcache_reset) {\n    zend_call_known_function(opcache_reset, NULL, NULL, NULL, 0, NULL, NULL);\n  }\n\n  return 0;\n}\n\nint frankenphp_get_current_memory_limit() { return PG(memory_limit); }\n\nstatic zend_module_entry **modules = NULL;\nstatic int modules_len = 0;\nstatic int (*original_php_register_internal_extensions_func)(void) = NULL;\n\nint register_internal_extensions(void) {\n  if (original_php_register_internal_extensions_func != NULL &&\n      original_php_register_internal_extensions_func() != SUCCESS) {\n    return FAILURE;\n  }\n\n  for (int i = 0; i < modules_len; i++) {\n    if (zend_register_internal_module(modules[i]) == NULL) {\n      return FAILURE;\n    }\n  }\n\n  modules = NULL;\n  modules_len = 0;\n\n  return SUCCESS;\n}\n\nvoid register_extensions(zend_module_entry **m, int len) {\n  modules = m;\n  modules_len = len;\n\n  original_php_register_internal_extensions_func =\n      php_register_internal_extensions_func;\n  php_register_internal_extensions_func = register_internal_extensions;\n}\n"
  },
  {
    "path": "frankenphp.go",
    "content": "// Package frankenphp embeds PHP in Go projects and provides a SAPI for net/http.\n//\n// This is the core of the [FrankenPHP app server], and can be used in any Go program.\n//\n// [FrankenPHP app server]: https://frankenphp.dev\npackage frankenphp\n\n// Use PHP includes corresponding to your PHP installation by running:\n//\n//   export CGO_CFLAGS=$(php-config --includes)\n//   export CGO_LDFLAGS=\"$(php-config --ldflags) $(php-config --libs)\"\n//\n// We also set these flags for hardening: https://github.com/docker-library/php/blob/master/8.2/bookworm/zts/Dockerfile#L57-L59\n\n// #include <stdlib.h>\n// #include <stdint.h>\n// #include \"frankenphp.h\"\n// #include <php_variables.h>\n// #include <zend_llist.h>\n// #include <SAPI.h>\nimport \"C\"\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/signal\"\n\t\"runtime\"\n\t\"strings\"\n\t\"sync\"\n\t\"syscall\"\n\t\"time\"\n\t\"unsafe\"\n\t// debug on Linux\n\t//_ \"github.com/ianlancetaylor/cgosymbolizer\"\n)\n\ntype contextKeyStruct struct{}\n\nvar (\n\tErrInvalidRequest     = errors.New(\"not a FrankenPHP request\")\n\tErrAlreadyStarted     = errors.New(\"FrankenPHP is already started\")\n\tErrInvalidPHPVersion  = errors.New(\"FrankenPHP is only compatible with PHP 8.2+\")\n\tErrMainThreadCreation = errors.New(\"error creating the main thread\")\n\tErrScriptExecution    = errors.New(\"error during PHP script execution\")\n\tErrNotRunning         = errors.New(\"FrankenPHP is not running. For proper configuration visit: https://frankenphp.dev/docs/config/#caddyfile-config\")\n\n\tErrInvalidRequestPath         = ErrRejected{\"invalid request path\", http.StatusBadRequest}\n\tErrInvalidContentLengthHeader = ErrRejected{\"invalid Content-Length header\", http.StatusBadRequest}\n\tErrMaxWaitTimeExceeded        = ErrRejected{\"maximum request handling time exceeded\", http.StatusServiceUnavailable}\n\n\tcontextKey   = contextKeyStruct{}\n\tserverHeader = []string{\"FrankenPHP\"}\n\n\tisRunning        bool\n\tonServerShutdown []func()\n\n\t// Set default values to make Shutdown() idempotent\n\tglobalMu     sync.Mutex\n\tglobalCtx    = context.Background()\n\tglobalLogger = slog.Default()\n\n\tmetrics Metrics = nullMetrics{}\n\n\tmaxWaitTime time.Duration\n)\n\ntype ErrRejected struct {\n\tmessage string\n\tstatus  int\n}\n\nfunc (e ErrRejected) Error() string {\n\treturn e.message\n}\n\ntype syslogLevel int\n\nconst (\n\tsyslogLevelEmerg  syslogLevel = iota // system is unusable\n\tsyslogLevelAlert                     // action must be taken immediately\n\tsyslogLevelCrit                      // critical conditions\n\tsyslogLevelErr                       // error conditions\n\tsyslogLevelWarn                      // warning conditions\n\tsyslogLevelNotice                    // normal but significant condition\n\tsyslogLevelInfo                      // informational\n\tsyslogLevelDebug                     // debug-level messages\n)\n\nfunc (l syslogLevel) String() string {\n\tswitch l {\n\tcase syslogLevelEmerg:\n\t\treturn \"emerg\"\n\tcase syslogLevelAlert:\n\t\treturn \"alert\"\n\tcase syslogLevelCrit:\n\t\treturn \"crit\"\n\tcase syslogLevelErr:\n\t\treturn \"err\"\n\tcase syslogLevelWarn:\n\t\treturn \"warning\"\n\tcase syslogLevelNotice:\n\t\treturn \"notice\"\n\tcase syslogLevelDebug:\n\t\treturn \"debug\"\n\tdefault:\n\t\treturn \"info\"\n\t}\n}\n\ntype PHPVersion struct {\n\tMajorVersion   int\n\tMinorVersion   int\n\tReleaseVersion int\n\tExtraVersion   string\n\tVersion        string\n\tVersionID      int\n}\n\ntype PHPConfig struct {\n\tVersion                PHPVersion\n\tZTS                    bool\n\tZendSignals            bool\n\tZendMaxExecutionTimers bool\n}\n\n// Version returns infos about the PHP version.\nfunc Version() PHPVersion {\n\tcVersion := C.frankenphp_get_version()\n\n\treturn PHPVersion{\n\t\tint(cVersion.major_version),\n\t\tint(cVersion.minor_version),\n\t\tint(cVersion.release_version),\n\t\tC.GoString(cVersion.extra_version),\n\t\tC.GoString(cVersion.version),\n\t\tint(cVersion.version_id),\n\t}\n}\n\nfunc Config() PHPConfig {\n\tcConfig := C.frankenphp_get_config()\n\n\treturn PHPConfig{\n\t\tVersion:                Version(),\n\t\tZTS:                    bool(cConfig.zts),\n\t\tZendSignals:            bool(cConfig.zend_signals),\n\t\tZendMaxExecutionTimers: bool(cConfig.zend_max_execution_timers),\n\t}\n}\n\nfunc calculateMaxThreads(opt *opt) (numWorkers int, _ error) {\n\tmaxProcs := runtime.GOMAXPROCS(0) * 2\n\tmaxThreadsFromWorkers := 0\n\n\tfor i, w := range opt.workers {\n\t\tif w.num <= 0 {\n\t\t\t// https://github.com/php/frankenphp/issues/126\n\t\t\topt.workers[i].num = maxProcs\n\t\t}\n\t\tmetrics.TotalWorkers(w.name, w.num)\n\n\t\tnumWorkers += opt.workers[i].num\n\n\t\tif w.maxThreads > 0 {\n\t\t\tif w.maxThreads < w.num {\n\t\t\t\treturn 0, fmt.Errorf(\"worker max_threads (%d) must be greater or equal to worker num (%d) (%q)\", w.maxThreads, w.num, w.fileName)\n\t\t\t}\n\n\t\t\tif w.maxThreads > opt.maxThreads && opt.maxThreads > 0 {\n\t\t\t\treturn 0, fmt.Errorf(\"worker max_threads (%d) cannot be greater than total max_threads (%d) (%q)\", w.maxThreads, opt.maxThreads, w.fileName)\n\t\t\t}\n\n\t\t\tmaxThreadsFromWorkers += w.maxThreads - w.num\n\t\t}\n\t}\n\n\tnumThreadsIsSet := opt.numThreads > 0\n\tmaxThreadsIsSet := opt.maxThreads != 0\n\tmaxThreadsIsAuto := opt.maxThreads < 0 // maxthreads < 0 signifies auto mode (see phpmaintread.go)\n\n\t// if max_threads is only defined in workers, scale up to the sum of all worker max_threads\n\tif !maxThreadsIsSet && maxThreadsFromWorkers > 0 {\n\t\tmaxThreadsIsSet = true\n\t\tif numThreadsIsSet {\n\t\t\topt.maxThreads = opt.numThreads + maxThreadsFromWorkers\n\t\t} else {\n\t\t\topt.maxThreads = numWorkers + 1 + maxThreadsFromWorkers\n\t\t}\n\t}\n\n\tif numThreadsIsSet && !maxThreadsIsSet {\n\t\topt.maxThreads = opt.numThreads\n\t\tif opt.numThreads <= numWorkers {\n\t\t\treturn 0, fmt.Errorf(\"num_threads (%d) must be greater than the number of worker threads (%d)\", opt.numThreads, numWorkers)\n\t\t}\n\n\t\treturn numWorkers, nil\n\t}\n\n\tif maxThreadsIsSet && !numThreadsIsSet {\n\t\topt.numThreads = numWorkers + 1\n\t\tif !maxThreadsIsAuto && opt.numThreads > opt.maxThreads {\n\t\t\treturn 0, fmt.Errorf(\"max_threads (%d) must be greater than the number of worker threads (%d)\", opt.maxThreads, numWorkers)\n\t\t}\n\n\t\treturn numWorkers, nil\n\t}\n\n\tif !numThreadsIsSet {\n\t\tif numWorkers >= maxProcs {\n\t\t\t// Start at least as many threads as workers, and keep a free thread to handle requests in non-worker mode\n\t\t\topt.numThreads = numWorkers + 1\n\t\t} else {\n\t\t\topt.numThreads = maxProcs\n\t\t}\n\t\topt.maxThreads = opt.numThreads\n\n\t\treturn numWorkers, nil\n\t}\n\n\t// both num_threads and max_threads are set\n\tif opt.numThreads <= numWorkers {\n\t\treturn 0, fmt.Errorf(\"num_threads (%d) must be greater than the number of worker threads (%d)\", opt.numThreads, numWorkers)\n\t}\n\n\tif !maxThreadsIsAuto && opt.maxThreads < opt.numThreads {\n\t\treturn 0, fmt.Errorf(\"max_threads (%d) must be greater than or equal to num_threads (%d)\", opt.maxThreads, opt.numThreads)\n\t}\n\n\treturn numWorkers, nil\n}\n\n// Init starts the PHP runtime and the configured workers.\nfunc Init(options ...Option) error {\n\tif isRunning {\n\t\treturn ErrAlreadyStarted\n\t}\n\tisRunning = true\n\n\t// Ignore all SIGPIPE signals to prevent weird issues with systemd: https://github.com/php/frankenphp/issues/1020\n\t// Docker/Moby has a similar hack: https://github.com/moby/moby/blob/d828b032a87606ae34267e349bf7f7ccb1f6495a/cmd/dockerd/docker.go#L87-L90\n\tsignal.Ignore(syscall.SIGPIPE)\n\n\tregisterExtensions()\n\n\topt := &opt{}\n\tfor _, o := range options {\n\t\tif err := o(opt); err != nil {\n\t\t\tShutdown()\n\t\t\treturn err\n\t\t}\n\t}\n\n\tglobalMu.Lock()\n\n\tif opt.ctx != nil {\n\t\tglobalCtx = opt.ctx\n\t\topt.ctx = nil\n\t}\n\n\tif opt.logger != nil {\n\t\tglobalLogger = opt.logger\n\t\topt.logger = nil\n\t}\n\n\tglobalMu.Unlock()\n\n\tif opt.metrics != nil {\n\t\tmetrics = opt.metrics\n\t}\n\n\tmaxWaitTime = opt.maxWaitTime\n\n\tif opt.maxIdleTime > 0 {\n\t\tmaxIdleTime = opt.maxIdleTime\n\t}\n\n\tworkerThreadCount, err := calculateMaxThreads(opt)\n\tif err != nil {\n\t\tShutdown()\n\t\treturn err\n\t}\n\n\tmetrics.TotalThreads(opt.numThreads)\n\n\tconfig := Config()\n\n\tif config.Version.MajorVersion < 8 || (config.Version.MajorVersion == 8 && config.Version.MinorVersion < 2) {\n\t\tShutdown()\n\t\treturn ErrInvalidPHPVersion\n\t}\n\n\tif config.ZTS {\n\t\tif !config.ZendMaxExecutionTimers && runtime.GOOS == \"linux\" {\n\t\t\tif globalLogger.Enabled(globalCtx, slog.LevelWarn) {\n\t\t\t\tglobalLogger.LogAttrs(globalCtx, slog.LevelWarn, `Zend Max Execution Timers are not enabled, timeouts (e.g. \"max_execution_time\") are disabled, recompile PHP with the \"--enable-zend-max-execution-timers\" configuration option to fix this issue`)\n\t\t\t}\n\t\t}\n\t} else {\n\t\topt.numThreads = 1\n\n\t\tif globalLogger.Enabled(globalCtx, slog.LevelWarn) {\n\t\t\tglobalLogger.LogAttrs(globalCtx, slog.LevelWarn, `ZTS is not enabled, only 1 thread will be available, recompile PHP using the \"--enable-zts\" configuration option or performance will be degraded`)\n\t\t}\n\t}\n\n\tmainThread, err := initPHPThreads(opt.numThreads, opt.maxThreads, opt.phpIni)\n\tif err != nil {\n\t\tShutdown()\n\t\treturn err\n\t}\n\n\tregularRequestChan = make(chan contextHolder)\n\tregularThreads = make([]*phpThread, 0, opt.numThreads-workerThreadCount)\n\tfor i := 0; i < opt.numThreads-workerThreadCount; i++ {\n\t\tconvertToRegularThread(getInactivePHPThread())\n\t}\n\n\tif err := initWorkers(opt.workers); err != nil {\n\t\tShutdown()\n\n\t\treturn err\n\t}\n\n\tif err := initWatchers(opt); err != nil {\n\t\tShutdown()\n\t\treturn err\n\t}\n\n\tinitAutoScaling(mainThread)\n\n\tif globalLogger.Enabled(globalCtx, slog.LevelInfo) {\n\t\tglobalLogger.LogAttrs(globalCtx, slog.LevelInfo, \"FrankenPHP started 🐘\", slog.String(\"php_version\", Version().Version), slog.Int(\"num_threads\", mainThread.numThreads), slog.Int(\"max_threads\", mainThread.maxThreads))\n\n\t\tif EmbeddedAppPath != \"\" {\n\t\t\tglobalLogger.LogAttrs(globalCtx, slog.LevelInfo, \"embedded PHP app 📦\", slog.String(\"path\", EmbeddedAppPath))\n\t\t}\n\t}\n\n\t// register the startup/shutdown hooks (mainly useful for extensions)\n\tonServerShutdown = nil\n\tfor _, w := range opt.workers {\n\t\tif w.onServerStartup != nil {\n\t\t\tw.onServerStartup()\n\t\t}\n\t\tif w.onServerShutdown != nil {\n\t\t\tonServerShutdown = append(onServerShutdown, w.onServerShutdown)\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// Shutdown stops the workers and the PHP runtime.\nfunc Shutdown() {\n\tif !isRunning {\n\t\treturn\n\t}\n\n\t// call the shutdown hooks (mainly useful for extensions)\n\tfor _, fn := range onServerShutdown {\n\t\tfn()\n\t}\n\n\tdrainWatchers()\n\tdrainAutoScaling()\n\tdrainPHPThreads()\n\n\tmetrics.Shutdown()\n\n\t// Remove the installed app\n\tif EmbeddedAppPath != \"\" {\n\t\t_ = os.RemoveAll(EmbeddedAppPath)\n\t}\n\n\tisRunning = false\n\tif globalLogger.Enabled(globalCtx, slog.LevelDebug) {\n\t\tglobalLogger.LogAttrs(globalCtx, slog.LevelDebug, \"FrankenPHP shut down\")\n\t}\n\n\tresetGlobals()\n}\n\n// ServeHTTP executes a PHP script according to the given context.\nfunc ServeHTTP(responseWriter http.ResponseWriter, request *http.Request) error {\n\th := responseWriter.Header()\n\tif h[\"Server\"] == nil {\n\t\th[\"Server\"] = serverHeader\n\t}\n\n\tif !isRunning {\n\t\treturn ErrNotRunning\n\t}\n\n\tctx := request.Context()\n\tfc, ok := fromContext(ctx)\n\n\tch := contextHolder{ctx, fc}\n\n\tif !ok {\n\t\treturn ErrInvalidRequest\n\t}\n\n\tfc.responseWriter = responseWriter\n\n\tif err := fc.validate(); err != nil {\n\t\treturn err\n\t}\n\n\t// Detect if a worker is available to handle this request\n\tif fc.worker != nil {\n\t\treturn fc.worker.handleRequest(ch)\n\t}\n\n\t// If no worker was available, send the request to non-worker threads\n\treturn handleRequestWithRegularPHPThreads(ch)\n}\n\n//export go_ub_write\nfunc go_ub_write(threadIndex C.uintptr_t, cBuf *C.char, length C.size_t) (C.size_t, C.bool) {\n\tthread := phpThreads[threadIndex]\n\tfc := thread.frankenPHPContext()\n\n\tif fc.isDone {\n\t\treturn 0, C.bool(true)\n\t}\n\n\tvar writer io.Writer\n\tif fc.responseWriter == nil {\n\t\tvar b bytes.Buffer\n\t\t// log the output of the worker\n\t\twriter = &b\n\t} else {\n\t\twriter = fc.responseWriter\n\t}\n\n\tvar ctx context.Context\n\n\ti, e := writer.Write(unsafe.Slice((*byte)(unsafe.Pointer(cBuf)), length))\n\tif e != nil {\n\t\tctx = thread.context()\n\n\t\tif fc.logger.Enabled(ctx, slog.LevelWarn) {\n\t\t\tfc.logger.LogAttrs(ctx, slog.LevelWarn, \"write error\", slog.Any(\"error\", e))\n\t\t}\n\t}\n\n\tif fc.responseWriter == nil {\n\t\t// probably starting a worker script, log the output\n\n\t\tif ctx == nil {\n\t\t\tctx = thread.context()\n\t\t}\n\n\t\tif fc.logger.Enabled(ctx, slog.LevelInfo) {\n\t\t\tfc.logger.LogAttrs(ctx, slog.LevelInfo, writer.(*bytes.Buffer).String())\n\t\t}\n\t}\n\n\treturn C.size_t(i), C.bool(fc.clientHasClosed())\n}\n\n//export go_apache_request_headers\nfunc go_apache_request_headers(threadIndex C.uintptr_t) (*C.go_string, C.size_t) {\n\tthread := phpThreads[threadIndex]\n\tctx := thread.context()\n\tfc := thread.frankenPHPContext()\n\n\tif fc.responseWriter == nil {\n\t\t// worker mode, not handling a request\n\n\t\tif globalLogger.Enabled(ctx, slog.LevelDebug) {\n\t\t\tglobalLogger.LogAttrs(ctx, slog.LevelDebug, \"apache_request_headers() called in non-HTTP context\", slog.String(\"worker\", fc.worker.name))\n\t\t}\n\n\t\treturn nil, 0\n\t}\n\n\theaders := make([]C.go_string, 0, len(fc.request.Header)*2)\n\n\tfor field, val := range fc.request.Header {\n\t\tfd := unsafe.StringData(field)\n\t\tthread.Pin(fd)\n\n\t\tcv := strings.Join(val, \", \")\n\t\tvd := unsafe.StringData(cv)\n\t\tthread.Pin(vd)\n\n\t\theaders = append(\n\t\t\theaders,\n\t\t\tC.go_string{C.size_t(len(field)), (*C.char)(unsafe.Pointer(fd))},\n\t\t\tC.go_string{C.size_t(len(cv)), (*C.char)(unsafe.Pointer(vd))},\n\t\t)\n\t}\n\n\tsd := unsafe.SliceData(headers)\n\tthread.Pin(sd)\n\n\treturn sd, C.size_t(len(fc.request.Header))\n}\n\nfunc addHeader(ctx context.Context, fc *frankenPHPContext, h *C.sapi_header_struct) {\n\tkey, val := splitRawHeader(h.header, int(h.header_len))\n\tif key == \"\" {\n\t\tif fc.logger.Enabled(ctx, slog.LevelDebug) {\n\t\t\tfc.logger.LogAttrs(ctx, slog.LevelDebug, \"invalid header\", slog.String(\"header\", C.GoStringN(h.header, C.int(h.header_len))))\n\t\t}\n\n\t\treturn\n\t}\n\tfc.responseWriter.Header().Add(key, val)\n}\n\n// split the raw header coming from C with minimal allocations\nfunc splitRawHeader(rawHeader *C.char, length int) (string, string) {\n\tbuf := unsafe.Slice((*byte)(unsafe.Pointer(rawHeader)), length)\n\n\t// Search for the colon in 'Header-Key: value'\n\tvar i int\n\tfor i = 0; i < length; i++ {\n\t\tif buf[i] == ':' {\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif i == length {\n\t\treturn \"\", \"\" // No colon found, invalid header\n\t}\n\n\theaderKey := C.GoStringN(rawHeader, C.int(i))\n\n\t// skip whitespaces after the colon\n\tj := i + 1\n\tfor j < length && buf[j] == ' ' {\n\t\tj++\n\t}\n\n\t// anything left is the header value\n\tvaluePtr := (*C.char)(unsafe.Pointer(uintptr(unsafe.Pointer(rawHeader)) + uintptr(j)))\n\theaderValue := C.GoStringN(valuePtr, C.int(length-j))\n\n\treturn headerKey, headerValue\n}\n\n//export go_write_headers\nfunc go_write_headers(threadIndex C.uintptr_t, status C.int, headers *C.zend_llist) C.bool {\n\tthread := phpThreads[threadIndex]\n\tfc := thread.frankenPHPContext()\n\tif fc == nil {\n\t\treturn C.bool(false)\n\t}\n\n\tif fc.isDone {\n\t\treturn C.bool(false)\n\t}\n\n\tif fc.responseWriter == nil {\n\t\t// probably starting a worker script, pretend we wrote headers so PHP still calls ub_write\n\t\treturn C.bool(true)\n\t}\n\n\tcurrent := headers.head\n\tfor current != nil {\n\t\th := (*C.sapi_header_struct)(unsafe.Pointer(&(current.data)))\n\n\t\taddHeader(thread.context(), fc, h)\n\t\tcurrent = current.next\n\t}\n\n\tgoStatus := int(status)\n\n\t// go panics on invalid status code\n\t// https://github.com/golang/go/blob/9b8742f2e79438b9442afa4c0a0139d3937ea33f/src/net/http/server.go#L1162\n\tif goStatus < 100 || goStatus > 999 {\n\t\tctx := thread.context()\n\n\t\tif globalLogger.Enabled(ctx, slog.LevelWarn) {\n\t\t\tglobalLogger.LogAttrs(ctx, slog.LevelWarn, \"Invalid response status code\", slog.Int(\"status_code\", goStatus))\n\t\t}\n\n\t\tgoStatus = 500\n\t}\n\n\tfc.responseWriter.WriteHeader(goStatus)\n\n\tif goStatus < 200 {\n\t\t// Clear headers, it's not automatically done by ResponseWriter.WriteHeader() for 1xx responses\n\t\th := fc.responseWriter.Header()\n\t\tfor k := range h {\n\t\t\tdelete(h, k)\n\t\t}\n\t}\n\n\treturn C.bool(true)\n}\n\n//export go_sapi_flush\nfunc go_sapi_flush(threadIndex C.uintptr_t) bool {\n\tthread := phpThreads[threadIndex]\n\tfc := thread.frankenPHPContext()\n\tif fc == nil {\n\t\treturn false\n\t}\n\n\tif fc.responseWriter == nil {\n\t\treturn false\n\t}\n\n\tif fc.clientHasClosed() && !fc.isDone {\n\t\treturn true\n\t}\n\n\tif fc.responseController == nil {\n\t\tfc.responseController = http.NewResponseController(fc.responseWriter)\n\t}\n\tif err := fc.responseController.Flush(); err != nil {\n\t\tctx := thread.context()\n\n\t\tif globalLogger.Enabled(ctx, slog.LevelWarn) {\n\t\t\tglobalLogger.LogAttrs(ctx, slog.LevelWarn, \"the current responseWriter is not a flusher, if you are not using a custom build, please report this issue\", slog.Any(\"error\", err))\n\t\t}\n\t}\n\n\treturn false\n}\n\n//export go_read_post\nfunc go_read_post(threadIndex C.uintptr_t, cBuf *C.char, countBytes C.size_t) (readBytes C.size_t) {\n\tfc := phpThreads[threadIndex].frankenPHPContext()\n\n\tif fc.responseWriter == nil {\n\t\treturn 0\n\t}\n\n\tp := unsafe.Slice((*byte)(unsafe.Pointer(cBuf)), countBytes)\n\tvar err error\n\tfor readBytes < countBytes && err == nil {\n\t\tvar n int\n\t\tn, err = fc.request.Body.Read(p[readBytes:])\n\t\treadBytes += C.size_t(n)\n\t}\n\n\treturn\n}\n\n//export go_read_cookies\nfunc go_read_cookies(threadIndex C.uintptr_t) *C.char {\n\trequest := phpThreads[threadIndex].frankenPHPContext().request\n\tif request == nil {\n\t\treturn nil\n\t}\n\n\tcookie := strings.Join(request.Header.Values(\"Cookie\"), \"; \")\n\tif cookie == \"\" {\n\t\treturn nil\n\t}\n\n\t// remove potential null bytes\n\tcookie = strings.ReplaceAll(cookie, \"\\x00\", \"\")\n\n\t// freed in frankenphp_free_request_context()\n\treturn C.CString(cookie)\n}\n\nfunc getLogger(threadIndex C.uintptr_t) (*slog.Logger, context.Context) {\n\tctxHolder := phpThreads[threadIndex]\n\tif ctxHolder == nil {\n\t\treturn globalLogger, globalCtx\n\t}\n\n\tctx := ctxHolder.context()\n\tif ctxHolder.handler == nil {\n\t\treturn globalLogger, ctx\n\t}\n\n\tfCtx := ctxHolder.frankenPHPContext()\n\tif fCtx == nil || fCtx.logger == nil {\n\t\treturn globalLogger, ctx\n\t}\n\n\treturn fCtx.logger, ctx\n}\n\n//export go_log\nfunc go_log(threadIndex C.uintptr_t, message *C.char, level C.int) {\n\tlogger, ctx := getLogger(threadIndex)\n\n\tle := syslogLevelInfo\n\tif level >= C.int(syslogLevelEmerg) && level <= C.int(syslogLevelDebug) {\n\t\tle = syslogLevel(level)\n\t}\n\n\tvar slogLevel slog.Level\n\tswitch le {\n\tcase syslogLevelEmerg, syslogLevelAlert, syslogLevelCrit, syslogLevelErr:\n\t\tslogLevel = slog.LevelError\n\tcase syslogLevelWarn:\n\t\tslogLevel = slog.LevelWarn\n\tcase syslogLevelDebug:\n\t\tslogLevel = slog.LevelDebug\n\tdefault:\n\t\tslogLevel = slog.LevelInfo\n\t}\n\n\tif !logger.Enabled(ctx, slogLevel) {\n\t\treturn\n\t}\n\n\tlogger.LogAttrs(ctx, slogLevel, C.GoString(message), slog.String(\"syslog_level\", le.String()))\n}\n\n//export go_log_attrs\nfunc go_log_attrs(threadIndex C.uintptr_t, message *C.zend_string, cLevel C.zend_long, cAttrs *C.zval) *C.char {\n\tlogger, ctx := getLogger(threadIndex)\n\n\tlevel := slog.Level(cLevel)\n\n\tif !logger.Enabled(ctx, level) {\n\t\treturn nil\n\t}\n\n\tvar attrs map[string]any\n\n\tif cAttrs != nil {\n\t\tvar err error\n\t\tif attrs, err = GoMap[any](unsafe.Pointer(*(**C.zend_array)(unsafe.Pointer(&cAttrs.value[0])))); err != nil {\n\t\t\t// PHP exception message.\n\t\t\treturn C.CString(\"Failed to log message: converting attrs: \" + err.Error())\n\t\t}\n\t}\n\n\tlogger.LogAttrs(ctx, level, GoString(unsafe.Pointer(message)), mapToAttr(attrs)...)\n\n\treturn nil\n}\n\nfunc mapToAttr(input map[string]any) []slog.Attr {\n\tout := make([]slog.Attr, 0, len(input))\n\n\tfor key, val := range input {\n\t\tout = append(out, slog.Any(key, val))\n\t}\n\n\treturn out\n}\n\n//export go_is_context_done\nfunc go_is_context_done(threadIndex C.uintptr_t) C.bool {\n\treturn C.bool(phpThreads[threadIndex].frankenPHPContext().isDone)\n}\n\nfunc convertArgs(args []string) (C.int, []*C.char) {\n\targc := C.int(len(args))\n\targv := make([]*C.char, argc)\n\tfor i, arg := range args {\n\t\targv[i] = C.CString(arg)\n\t}\n\treturn argc, argv\n}\n\nfunc freeArgs(argv []*C.char) {\n\tfor _, arg := range argv {\n\t\tC.free(unsafe.Pointer(arg))\n\t}\n}\n\nfunc timeoutChan(timeout time.Duration) <-chan time.Time {\n\tif timeout == 0 {\n\t\treturn nil\n\t}\n\n\treturn time.After(timeout)\n}\n\nfunc resetGlobals() {\n\tglobalMu.Lock()\n\tglobalCtx = context.Background()\n\tglobalLogger = slog.Default()\n\tworkers = nil\n\tworkersByName = nil\n\tworkersByPath = nil\n\twatcherIsEnabled = false\n\tmaxIdleTime = defaultMaxIdleTime\n\tglobalMu.Unlock()\n}\n"
  },
  {
    "path": "frankenphp.h",
    "content": "#ifndef _FRANKENPHP_H\n#define _FRANKENPHP_H\n\n#ifdef _WIN32\n// Define this to prevent windows.h from including legacy winsock.h\n#ifndef WIN32_LEAN_AND_MEAN\n#define WIN32_LEAN_AND_MEAN\n#endif\n\n// Explicitly include Winsock2 BEFORE windows.h\n#include <windows.h>\n#include <winerror.h>\n#include <winsock2.h>\n#include <ws2tcpip.h>\n\n// Fix for missing IntSafe functions (LongLongAdd) when building with Clang\n#ifdef __clang__\n#ifndef INTSAFE_E_ARITHMETIC_OVERFLOW\n#define INTSAFE_E_ARITHMETIC_OVERFLOW ((HRESULT)0x80070216L)\n#endif\n\n#ifndef LongLongAdd\nstatic inline HRESULT LongLongAdd(LONGLONG llAugend, LONGLONG llAddend,\n                                  LONGLONG *pllResult) {\n  if (__builtin_add_overflow(llAugend, llAddend, pllResult)) {\n    return INTSAFE_E_ARITHMETIC_OVERFLOW;\n  }\n  return S_OK;\n}\n#endif\n\n#ifndef LongLongSub\nstatic inline HRESULT LongLongSub(LONGLONG llMinuend, LONGLONG llSubtrahend,\n                                  LONGLONG *pllResult) {\n  if (__builtin_sub_overflow(llMinuend, llSubtrahend, pllResult)) {\n    return INTSAFE_E_ARITHMETIC_OVERFLOW;\n  }\n  return S_OK;\n}\n#endif\n#endif\n#endif\n\n#include <Zend/zend_modules.h>\n#include <Zend/zend_types.h>\n#include <stdbool.h>\n#include <stdint.h>\n\n#ifndef FRANKENPHP_VERSION\n#define FRANKENPHP_VERSION dev\n#endif\n#define STRINGIFY(x) #x\n#define TOSTRING(x) STRINGIFY(x)\n\ntypedef struct go_string {\n  size_t len;\n  char *data;\n} go_string;\n\ntypedef struct frankenphp_server_vars {\n  size_t total_num_vars;\n  char *remote_addr;\n  size_t remote_addr_len;\n  char *remote_host;\n  size_t remote_host_len;\n  char *remote_port;\n  size_t remote_port_len;\n  char *document_root;\n  size_t document_root_len;\n  char *path_info;\n  size_t path_info_len;\n  char *php_self;\n  size_t php_self_len;\n  char *document_uri;\n  size_t document_uri_len;\n  char *script_filename;\n  size_t script_filename_len;\n  char *script_name;\n  size_t script_name_len;\n  char *server_name;\n  size_t server_name_len;\n  char *server_port;\n  size_t server_port_len;\n  char *content_length;\n  size_t content_length_len;\n  char *server_protocol;\n  size_t server_protocol_len;\n  char *http_host;\n  size_t http_host_len;\n  char *request_uri;\n  size_t request_uri_len;\n  char *ssl_cipher;\n  size_t ssl_cipher_len;\n  zend_string *request_scheme;\n  zend_string *ssl_protocol;\n  zend_string *https;\n} frankenphp_server_vars;\n\n/**\n * Cached interned strings for memory and performance benefits\n * Add more hard-coded strings here if needed\n */\n#define FRANKENPHP_INTERNED_STRINGS_LIST(X)                                    \\\n  X(remote_addr, \"REMOTE_ADDR\")                                                \\\n  X(remote_host, \"REMOTE_HOST\")                                                \\\n  X(remote_port, \"REMOTE_PORT\")                                                \\\n  X(document_root, \"DOCUMENT_ROOT\")                                            \\\n  X(path_info, \"PATH_INFO\")                                                    \\\n  X(php_self, \"PHP_SELF\")                                                      \\\n  X(document_uri, \"DOCUMENT_URI\")                                              \\\n  X(script_filename, \"SCRIPT_FILENAME\")                                        \\\n  X(script_name, \"SCRIPT_NAME\")                                                \\\n  X(https, \"HTTPS\")                                                            \\\n  X(httpsLowercase, \"https\")                                                   \\\n  X(httpLowercase, \"http\")                                                     \\\n  X(ssl_protocol, \"SSL_PROTOCOL\")                                              \\\n  X(request_scheme, \"REQUEST_SCHEME\")                                          \\\n  X(server_name, \"SERVER_NAME\")                                                \\\n  X(server_port, \"SERVER_PORT\")                                                \\\n  X(content_length, \"CONTENT_LENGTH\")                                          \\\n  X(server_protocol, \"SERVER_PROTOCOL\")                                        \\\n  X(http_host, \"HTTP_HOST\")                                                    \\\n  X(request_uri, \"REQUEST_URI\")                                                \\\n  X(ssl_cipher, \"SSL_CIPHER\")                                                  \\\n  X(server_software, \"SERVER_SOFTWARE\")                                        \\\n  X(frankenphp, \"FrankenPHP\")                                                  \\\n  X(gateway_interface, \"GATEWAY_INTERFACE\")                                    \\\n  X(cgi11, \"CGI/1.1\")                                                          \\\n  X(auth_type, \"AUTH_TYPE\")                                                    \\\n  X(remote_ident, \"REMOTE_IDENT\")                                              \\\n  X(content_type, \"CONTENT_TYPE\")                                              \\\n  X(path_translated, \"PATH_TRANSLATED\")                                        \\\n  X(query_string, \"QUERY_STRING\")                                              \\\n  X(remote_user, \"REMOTE_USER\")                                                \\\n  X(request_method, \"REQUEST_METHOD\")                                          \\\n  X(tls1, \"TLSv1\")                                                             \\\n  X(tls11, \"TLSv1.1\")                                                          \\\n  X(tls12, \"TLSv1.2\")                                                          \\\n  X(tls13, \"TLSv1.3\")                                                          \\\n  X(on, \"on\")                                                                  \\\n  X(empty, \"\")\n\ntypedef struct frankenphp_interned_strings_t {\n#define F_DEFINE_STRUCT_FIELD(name, str) zend_string *name;\n  FRANKENPHP_INTERNED_STRINGS_LIST(F_DEFINE_STRUCT_FIELD)\n#undef F_DEFINE_STRUCT_FIELD\n} frankenphp_interned_strings_t;\n\nextern frankenphp_interned_strings_t frankenphp_strings;\n\ntypedef struct frankenphp_version {\n  unsigned char major_version;\n  unsigned char minor_version;\n  unsigned char release_version;\n  const char *extra_version;\n  const char *version;\n  unsigned long version_id;\n} frankenphp_version;\nfrankenphp_version frankenphp_get_version();\n\ntypedef struct frankenphp_config {\n  bool zts;\n  bool zend_signals;\n  bool zend_max_execution_timers;\n} frankenphp_config;\nfrankenphp_config frankenphp_get_config();\n\nint frankenphp_new_main_thread(int num_threads);\nbool frankenphp_new_php_thread(uintptr_t thread_index);\n\nbool frankenphp_shutdown_dummy_request(void);\nint frankenphp_execute_script(char *file_name);\nvoid frankenphp_update_local_thread_context(bool is_worker);\n\nint frankenphp_execute_script_cli(char *script, int argc, char **argv,\n                                  bool eval);\n\nvoid frankenphp_register_known_variable(zend_string *z_key, char *value,\n                                        size_t val_len, zval *track_vars_array);\nvoid frankenphp_register_variable_safe(char *key, char *var, size_t val_len,\n                                       zval *track_vars_array);\nvoid frankenphp_register_server_vars(zval *track_vars_array,\n                                     frankenphp_server_vars vars);\n\nzend_string *frankenphp_init_persistent_string(const char *string, size_t len);\nint frankenphp_reset_opcache(void);\nint frankenphp_get_current_memory_limit();\n\nvoid register_extensions(zend_module_entry **m, int len);\n\n#endif\n"
  },
  {
    "path": "frankenphp.stub.php",
    "content": "<?php\n\n/** @generate-class-entries */\n\n/** @var int */\nconst FRANKENPHP_LOG_LEVEL_DEBUG = -4;\n\n/** @var int */\nconst FRANKENPHP_LOG_LEVEL_INFO = 0;\n\n/** @var int */\nconst FRANKENPHP_LOG_LEVEL_WARN = 4;\n\n/** @var int */\nconst FRANKENPHP_LOG_LEVEL_ERROR = 8;\n\nfunction frankenphp_handle_request(callable $callback): bool {}\n\nfunction headers_send(int $status = 200): int {}\n\nfunction frankenphp_finish_request(): bool {}\n\n/**\n * @alias frankenphp_finish_request\n */\nfunction fastcgi_finish_request(): bool {}\n\nfunction frankenphp_request_headers(): array {}\n\n/**\n * @alias frankenphp_request_headers\n */\nfunction apache_request_headers(): array {}\n\n/**\n * @alias frankenphp_request_headers\n*/\nfunction getallheaders(): array {}\n\nfunction frankenphp_response_headers(): array|bool {}\n\n/**\n * @alias frankenphp_response_headers\n */\nfunction apache_response_headers(): array|bool {}\n\n/**\n * @param string|string[] $topics\n */\nfunction mercure_publish(string|array $topics, string $data = '', bool $private = false, ?string $id = null, ?string $type = null, ?int $retry = null): string {}\n\n/**\n * @param int $level The importance or severity of a log event. The higher the level, the more important or severe the event. For more details, see: https://pkg.go.dev/log/slog#Level\n * array<string, any> $context Values of the array will be converted to the corresponding Go type (if supported by FrankenPHP) and added to the context of the structured logs using https://pkg.go.dev/log/slog#Attr\n */\nfunction frankenphp_log(string $message, int $level = 0, array $context = []): void {}\n"
  },
  {
    "path": "frankenphp_arginfo.h",
    "content": "/* This is a generated file, edit the .stub.php file instead.\n * Stub hash: 60f0d27c04f94d7b24c052e91ef294595a2bc421 */\n\nZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_frankenphp_handle_request, 0, 1, _IS_BOOL, 0)\n\tZEND_ARG_TYPE_INFO(0, callback, IS_CALLABLE, 0)\nZEND_END_ARG_INFO()\n\nZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_headers_send, 0, 0, IS_LONG, 0)\n\tZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, status, IS_LONG, 0, \"200\")\nZEND_END_ARG_INFO()\n\nZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_frankenphp_finish_request, 0, 0, _IS_BOOL, 0)\nZEND_END_ARG_INFO()\n\n#define arginfo_fastcgi_finish_request arginfo_frankenphp_finish_request\n\nZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_frankenphp_request_headers, 0, 0, IS_ARRAY, 0)\nZEND_END_ARG_INFO()\n\n#define arginfo_apache_request_headers arginfo_frankenphp_request_headers\n\n#define arginfo_getallheaders arginfo_frankenphp_request_headers\n\nZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX(arginfo_frankenphp_response_headers, 0, 0, MAY_BE_ARRAY|MAY_BE_BOOL)\nZEND_END_ARG_INFO()\n\n#define arginfo_apache_response_headers arginfo_frankenphp_response_headers\n\nZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_mercure_publish, 0, 1, IS_STRING, 0)\n\tZEND_ARG_TYPE_MASK(0, topics, MAY_BE_STRING|MAY_BE_ARRAY, NULL)\n\tZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, data, IS_STRING, 0, \"\\'\\'\")\n\tZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, private, _IS_BOOL, 0, \"false\")\n\tZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, id, IS_STRING, 1, \"null\")\n\tZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, type, IS_STRING, 1, \"null\")\n\tZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, retry, IS_LONG, 1, \"null\")\nZEND_END_ARG_INFO()\n\nZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_frankenphp_log, 0, 1, IS_VOID, 0)\n\tZEND_ARG_TYPE_INFO(0, message, IS_STRING, 0)\n\tZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, level, IS_LONG, 0, \"0\")\n\tZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, context, IS_ARRAY, 0, \"[]\")\nZEND_END_ARG_INFO()\n\n\nZEND_FUNCTION(frankenphp_handle_request);\nZEND_FUNCTION(headers_send);\nZEND_FUNCTION(frankenphp_finish_request);\nZEND_FUNCTION(frankenphp_request_headers);\nZEND_FUNCTION(frankenphp_response_headers);\nZEND_FUNCTION(mercure_publish);\nZEND_FUNCTION(frankenphp_log);\n\n\nstatic const zend_function_entry ext_functions[] = {\n\tZEND_FE(frankenphp_handle_request, arginfo_frankenphp_handle_request)\n\tZEND_FE(headers_send, arginfo_headers_send)\n\tZEND_FE(frankenphp_finish_request, arginfo_frankenphp_finish_request)\n\tZEND_FALIAS(fastcgi_finish_request, frankenphp_finish_request, arginfo_fastcgi_finish_request)\n\tZEND_FE(frankenphp_request_headers, arginfo_frankenphp_request_headers)\n\tZEND_FALIAS(apache_request_headers, frankenphp_request_headers, arginfo_apache_request_headers)\n\tZEND_FALIAS(getallheaders, frankenphp_request_headers, arginfo_getallheaders)\n\tZEND_FE(frankenphp_response_headers, arginfo_frankenphp_response_headers)\n\tZEND_FALIAS(apache_response_headers, frankenphp_response_headers, arginfo_apache_response_headers)\n\tZEND_FE(mercure_publish, arginfo_mercure_publish)\n\tZEND_FE(frankenphp_log, arginfo_frankenphp_log)\n\tZEND_FE_END\n};\n\nstatic void register_frankenphp_symbols(int module_number)\n{\n\tREGISTER_LONG_CONSTANT(\"FRANKENPHP_LOG_LEVEL_DEBUG\", -4, CONST_PERSISTENT);\n\tREGISTER_LONG_CONSTANT(\"FRANKENPHP_LOG_LEVEL_INFO\", 0, CONST_PERSISTENT);\n\tREGISTER_LONG_CONSTANT(\"FRANKENPHP_LOG_LEVEL_WARN\", 4, CONST_PERSISTENT);\n\tREGISTER_LONG_CONSTANT(\"FRANKENPHP_LOG_LEVEL_ERROR\", 8, CONST_PERSISTENT);\n}\n"
  },
  {
    "path": "frankenphp_test.go",
    "content": "// In all tests, headers added to requests are copied on the heap using strings.Clone.\n// This was originally a workaround for https://github.com/golang/go/issues/65286#issuecomment-1920087884 (fixed in Go 1.22),\n// but this allows to catch panics occurring in real life but not when the string is in the internal binary memory.\n\npackage frankenphp_test\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"flag\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"log/slog\"\n\t\"mime/multipart\"\n\t\"net/http\"\n\t\"net/http/cookiejar\"\n\t\"net/http/httptest\"\n\t\"net/http/httptrace\"\n\t\"net/textproto\"\n\t\"net/url\"\n\t\"os\"\n\t\"os/exec\"\n\t\"os/user\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/dunglas/frankenphp\"\n\t\"github.com/dunglas/frankenphp/internal/fastabs\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\ntype testOptions struct {\n\tworkerScript       string\n\twatch              []string\n\tnbWorkers          int\n\tenv                map[string]string\n\tnbParallelRequests int\n\trealServer         bool\n\tlogger             *slog.Logger\n\tinitOpts           []frankenphp.Option\n\trequestOpts        []frankenphp.RequestOption\n\tphpIni             map[string]string\n}\n\nfunc runTest(t *testing.T, test func(func(http.ResponseWriter, *http.Request), *httptest.Server, int), opts *testOptions) {\n\tif opts == nil {\n\t\topts = &testOptions{}\n\t}\n\tif opts.nbParallelRequests == 0 {\n\t\topts.nbParallelRequests = 100\n\t}\n\n\tcwd, _ := os.Getwd()\n\ttestDataDir := cwd + \"/testdata/\"\n\n\tinitOpts := []frankenphp.Option{frankenphp.WithLogger(opts.logger)}\n\tif opts.workerScript != \"\" {\n\t\tworkerOpts := []frankenphp.WorkerOption{\n\t\t\tfrankenphp.WithWorkerEnv(opts.env),\n\t\t\tfrankenphp.WithWorkerWatchMode(opts.watch),\n\t\t}\n\t\tinitOpts = append(initOpts, frankenphp.WithWorkers(\"workerName\", testDataDir+opts.workerScript, opts.nbWorkers, workerOpts...))\n\t}\n\tinitOpts = append(initOpts, opts.initOpts...)\n\tif opts.phpIni != nil {\n\t\tinitOpts = append(initOpts, frankenphp.WithPhpIni(opts.phpIni))\n\t}\n\n\terr := frankenphp.Init(initOpts...)\n\trequire.NoError(t, err)\n\tdefer frankenphp.Shutdown()\n\n\topts.requestOpts = append(opts.requestOpts, frankenphp.WithRequestDocumentRoot(testDataDir, false))\n\n\thandler := func(w http.ResponseWriter, r *http.Request) {\n\t\treq, err := frankenphp.NewRequestWithContext(r, opts.requestOpts...)\n\t\tassert.NoError(t, err)\n\n\t\terr = frankenphp.ServeHTTP(w, req)\n\t\tif err != nil && !errors.As(err, &frankenphp.ErrRejected{}) {\n\t\t\tassert.Fail(t, fmt.Sprintf(\"Received unexpected error:\\n%+v\", err))\n\t\t}\n\t}\n\n\tvar ts *httptest.Server\n\tif opts.realServer {\n\t\tts = httptest.NewServer(http.HandlerFunc(handler))\n\t\tdefer ts.Close()\n\t}\n\n\tvar wg sync.WaitGroup\n\twg.Add(opts.nbParallelRequests)\n\tfor i := 0; i < opts.nbParallelRequests; i++ {\n\t\tgo func(i int) {\n\t\t\ttest(handler, ts, i)\n\t\t\twg.Done()\n\t\t}(i)\n\t}\n\n\twg.Wait()\n}\n\nfunc testRequest(req *http.Request, handler func(http.ResponseWriter, *http.Request), t *testing.T) (string, *http.Response) {\n\tt.Helper()\n\n\tw := httptest.NewRecorder()\n\thandler(w, req)\n\tresp := w.Result()\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\n\treturn string(body), resp\n}\n\nfunc testGet(url string, handler func(http.ResponseWriter, *http.Request), t *testing.T) (string, *http.Response) {\n\tt.Helper()\n\treq := httptest.NewRequest(http.MethodGet, url, nil)\n\n\treturn testRequest(req, handler, t)\n}\n\nfunc testPost(url string, body string, handler func(http.ResponseWriter, *http.Request), t *testing.T) (string, *http.Response) {\n\tt.Helper()\n\treq := httptest.NewRequest(http.MethodPost, url, nil)\n\treq.Body = io.NopCloser(strings.NewReader(body))\n\n\treturn testRequest(req, handler, t)\n}\n\nfunc TestMain(m *testing.M) {\n\tflag.Parse()\n\n\tif !testing.Verbose() {\n\t\tslog.SetDefault(slog.New(slog.DiscardHandler))\n\t}\n\n\t// setup custom environment var for TestWorkerHasOSEnvironmentVariableInSERVER and TestPhpIni\n\tif os.Setenv(\"CUSTOM_OS_ENV_VARIABLE\", \"custom_env_variable_value\") != nil || os.Setenv(\"LITERAL_ZERO\", \"0\") != nil {\n\t\tfmt.Println(\"Failed to set environment variable for tests\")\n\t\tos.Exit(1)\n\t}\n\n\tos.Exit(m.Run())\n}\n\nfunc TestHelloWorld_module(t *testing.T) { testHelloWorld(t, nil) }\nfunc TestHelloWorld_worker(t *testing.T) {\n\ttestHelloWorld(t, &testOptions{workerScript: \"index.php\"})\n}\nfunc testHelloWorld(t *testing.T, opts *testOptions) {\n\trunTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {\n\t\tbody, _ := testGet(fmt.Sprintf(\"http://example.com/index.php?i=%d\", i), handler, t)\n\t\tassert.Equal(t, fmt.Sprintf(\"I am by birth a Genevese (%d)\", i), body)\n\t}, opts)\n}\n\nfunc TestEnvVarsInPhpIni(t *testing.T) {\n\trunTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, _ int) {\n\t\tbody, _ := testGet(\"http://example.com/ini.php?key=opcache.enable\", handler, t)\n\t\tassert.Equal(t, \"opcache.enable:0\", body)\n\t}, &testOptions{\n\t\tphpIni: map[string]string{\n\t\t\t\"opcache.enable\": \"${LITERAL_ZERO}\",\n\t\t},\n\t})\n}\n\nfunc TestFinishRequest_module(t *testing.T) { testFinishRequest(t, nil) }\nfunc TestFinishRequest_worker(t *testing.T) {\n\ttestFinishRequest(t, &testOptions{workerScript: \"finish-request.php\"})\n}\nfunc testFinishRequest(t *testing.T, opts *testOptions) {\n\trunTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {\n\t\tbody, _ := testGet(fmt.Sprintf(\"http://example.com/finish-request.php?i=%d\", i), handler, t)\n\t\tassert.Equal(t, fmt.Sprintf(\"This is output %d\\n\", i), body)\n\t}, opts)\n}\n\nfunc TestServerVariable_module(t *testing.T) {\n\ttestServerVariable(t, nil)\n}\nfunc TestServerVariable_worker(t *testing.T) {\n\ttestServerVariable(t, &testOptions{workerScript: \"server-variable.php\"})\n}\nfunc testServerVariable(t *testing.T, opts *testOptions) {\n\trunTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {\n\t\treq := httptest.NewRequest(\"POST\", fmt.Sprintf(\"http://example.com/server-variable.php/baz/bat?foo=a&bar=b&i=%d#hash\", i), strings.NewReader(\"foo\"))\n\t\treq.SetBasicAuth(strings.Clone(\"kevin\"), strings.Clone(\"password\"))\n\t\treq.Header.Add(strings.Clone(\"Content-Type\"), strings.Clone(\"text/plain\"))\n\t\tbody, _ := testRequest(req, handler, t)\n\n\t\tassert.Contains(t, body, \"[REMOTE_HOST]\")\n\t\tassert.Contains(t, body, \"[REMOTE_USER] => kevin\")\n\t\tassert.Contains(t, body, \"[PHP_AUTH_USER] => kevin\")\n\t\tassert.Contains(t, body, \"[PHP_AUTH_PW] => password\")\n\t\tassert.Contains(t, body, \"[HTTP_AUTHORIZATION] => Basic a2V2aW46cGFzc3dvcmQ=\")\n\t\tassert.Contains(t, body, \"[DOCUMENT_ROOT]\")\n\t\tassert.Contains(t, body, \"[PHP_SELF] => /server-variable.php/baz/bat\")\n\t\tassert.Contains(t, body, \"[CONTENT_TYPE] => text/plain\")\n\t\tassert.Contains(t, body, fmt.Sprintf(\"[QUERY_STRING] => foo=a&bar=b&i=%d#hash\", i))\n\t\tassert.Contains(t, body, fmt.Sprintf(\"[REQUEST_URI] => /server-variable.php/baz/bat?foo=a&bar=b&i=%d#hash\", i))\n\t\tassert.Contains(t, body, \"[CONTENT_LENGTH]\")\n\t\tassert.Contains(t, body, \"[REMOTE_ADDR]\")\n\t\tassert.Contains(t, body, \"[REMOTE_PORT]\")\n\t\tassert.Contains(t, body, \"[REQUEST_SCHEME] => http\")\n\t\tassert.Contains(t, body, \"[DOCUMENT_URI]\")\n\t\tassert.Contains(t, body, \"[AUTH_TYPE]\")\n\t\tassert.Contains(t, body, \"[REMOTE_IDENT]\")\n\t\tassert.Contains(t, body, \"[REQUEST_METHOD] => POST\")\n\t\tassert.Contains(t, body, \"[SERVER_NAME] => example.com\")\n\t\tassert.Contains(t, body, \"[SERVER_PROTOCOL] => HTTP/1.1\")\n\t\tassert.Contains(t, body, \"[SCRIPT_FILENAME]\")\n\t\tassert.Contains(t, body, \"[SERVER_SOFTWARE] => FrankenPHP\")\n\t\tassert.Contains(t, body, \"[REQUEST_TIME_FLOAT]\")\n\t\tassert.Contains(t, body, \"[REQUEST_TIME]\")\n\t\tassert.Contains(t, body, \"[SERVER_PORT] => 80\")\n\t}, opts)\n}\n\nfunc TestPathInfo_module(t *testing.T) { testPathInfo(t, nil) }\nfunc TestPathInfo_worker(t *testing.T) {\n\ttestPathInfo(t, &testOptions{workerScript: \"server-variable.php\"})\n}\nfunc testPathInfo(t *testing.T, opts *testOptions) {\n\tcwd, _ := os.Getwd()\n\ttestDataDir := cwd + strings.Clone(\"/testdata/\")\n\tpath := strings.Clone(\"/server-variable.php/pathinfo\")\n\n\trunTest(t, func(_ func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {\n\t\thandler := func(w http.ResponseWriter, r *http.Request) {\n\t\t\trequestURI := r.URL.RequestURI()\n\t\t\tr.URL.Path = path\n\n\t\t\trewriteRequest, err := frankenphp.NewRequestWithContext(r,\n\t\t\t\tfrankenphp.WithRequestDocumentRoot(testDataDir, false),\n\t\t\t\tfrankenphp.WithRequestEnv(map[string]string{\"REQUEST_URI\": requestURI}),\n\t\t\t)\n\t\t\tassert.NoError(t, err)\n\n\t\t\terr = frankenphp.ServeHTTP(w, rewriteRequest)\n\t\t\tassert.NoError(t, err)\n\t\t}\n\n\t\tbody, _ := testGet(fmt.Sprintf(\"http://example.com/pathinfo/%d\", i), handler, t)\n\n\t\tassert.Contains(t, body, \"[PATH_INFO] => /pathinfo\")\n\t\tassert.Contains(t, body, fmt.Sprintf(\"[REQUEST_URI] => /pathinfo/%d\", i))\n\t\tassert.Contains(t, body, \"[PATH_TRANSLATED] =>\")\n\t\tassert.Contains(t, body, \"[SCRIPT_NAME] => /server-variable.php\")\n\n\t}, opts)\n}\n\nfunc TestHeaders_module(t *testing.T) { testHeaders(t, nil) }\nfunc TestHeaders_worker(t *testing.T) { testHeaders(t, &testOptions{workerScript: \"headers.php\"}) }\nfunc testHeaders(t *testing.T, opts *testOptions) {\n\trunTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {\n\t\tbody, resp := testGet(fmt.Sprintf(\"http://example.com/headers.php?i=%d\", i), handler, t)\n\n\t\tassert.Equal(t, \"Hello\", body)\n\t\tassert.Equal(t, 201, resp.StatusCode)\n\t\tassert.Equal(t, \"bar\", resp.Header.Get(\"Foo\"))\n\t\tassert.Equal(t, \"bar2\", resp.Header.Get(\"Foo2\"))\n\t\tassert.Equal(t, \"bar3\", resp.Header.Get(\"Foo3\"), \"header without whitespace after colon\")\n\t\tassert.Empty(t, resp.Header.Get(\"Invalid\"))\n\t\tassert.Equal(t, fmt.Sprintf(\"%d\", i), resp.Header.Get(\"I\"))\n\t}, opts)\n}\n\nfunc TestResponseHeaders_module(t *testing.T) { testResponseHeaders(t, nil) }\nfunc TestResponseHeaders_worker(t *testing.T) {\n\ttestResponseHeaders(t, &testOptions{workerScript: \"response-headers.php\"})\n}\nfunc testResponseHeaders(t *testing.T, opts *testOptions) {\n\trunTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {\n\t\tbody, resp := testGet(fmt.Sprintf(\"http://example.com/response-headers.php?i=%d\", i), handler, t)\n\n\t\tif i%3 != 0 {\n\t\t\tassert.Equal(t, i+100, resp.StatusCode)\n\t\t} else {\n\t\t\tassert.Equal(t, 200, resp.StatusCode)\n\t\t}\n\n\t\tassert.Contains(t, body, \"'X-Powered-By' => 'PH\")\n\t\tassert.Contains(t, body, \"'Foo' => 'bar',\")\n\t\tassert.Contains(t, body, \"'Foo2' => 'bar2',\")\n\t\tassert.Contains(t, body, fmt.Sprintf(\"'I' => '%d',\", i))\n\t\tassert.NotContains(t, body, \"Invalid\")\n\t}, opts)\n}\n\nfunc TestInput_module(t *testing.T) { testInput(t, nil) }\nfunc TestInput_worker(t *testing.T) { testInput(t, &testOptions{workerScript: \"input.php\"}) }\nfunc testInput(t *testing.T, opts *testOptions) {\n\trunTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {\n\t\tbody, resp := testPost(\"http://example.com/input.php\", fmt.Sprintf(\"post data %d\", i), handler, t)\n\n\t\tassert.Equal(t, fmt.Sprintf(\"post data %d\", i), body)\n\t\tassert.Equal(t, \"bar\", resp.Header.Get(\"Foo\"))\n\t}, opts)\n}\n\nfunc TestPostSuperGlobals_module(t *testing.T) { testPostSuperGlobals(t, nil) }\nfunc TestPostSuperGlobals_worker(t *testing.T) {\n\ttestPostSuperGlobals(t, &testOptions{workerScript: \"super-globals.php\"})\n}\nfunc testPostSuperGlobals(t *testing.T, opts *testOptions) {\n\trunTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {\n\t\tformData := url.Values{\"baz\": {\"bat\"}, \"i\": {fmt.Sprintf(\"%d\", i)}}\n\t\treq := httptest.NewRequest(\"POST\", fmt.Sprintf(\"http://example.com/super-globals.php?foo=bar&iG=%d\", i), strings.NewReader(formData.Encode()))\n\t\treq.Header.Set(\"Content-Type\", strings.Clone(\"application/x-www-form-urlencoded\"))\n\t\tbody, _ := testRequest(req, handler, t)\n\n\t\tassert.Contains(t, body, \"'foo' => 'bar'\")\n\t\tassert.Contains(t, body, fmt.Sprintf(\"'i' => '%d'\", i))\n\t\tassert.Contains(t, body, \"'baz' => 'bat'\")\n\t\tassert.Contains(t, body, fmt.Sprintf(\"'iG' => '%d'\", i))\n\t}, opts)\n}\n\nfunc TestRequestSuperGlobal_module(t *testing.T) { testRequestSuperGlobal(t, nil) }\nfunc TestRequestSuperGlobal_worker(t *testing.T) {\n\tphpIni := make(map[string]string)\n\tphpIni[\"auto_globals_jit\"] = \"1\"\n\ttestRequestSuperGlobal(t, &testOptions{workerScript: \"request-superglobal.php\", phpIni: phpIni})\n}\nfunc testRequestSuperGlobal(t *testing.T, opts *testOptions) {\n\trunTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {\n\t\t// Test with both GET and POST parameters\n\t\t// $_REQUEST should contain merged data from both\n\t\tformData := url.Values{\"post_key\": {fmt.Sprintf(\"post_value_%d\", i)}}\n\t\treq := httptest.NewRequest(\"POST\", fmt.Sprintf(\"http://example.com/request-superglobal.php?get_key=get_value_%d\", i), strings.NewReader(formData.Encode()))\n\t\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\t\tbody, _ := testRequest(req, handler, t)\n\n\t\t// Verify $_REQUEST contains both GET and POST data for the current request\n\t\tassert.Contains(t, body, fmt.Sprintf(\"'get_key' => 'get_value_%d'\", i))\n\t\tassert.Contains(t, body, fmt.Sprintf(\"'post_key' => 'post_value_%d'\", i))\n\t}, opts)\n}\n\nfunc TestRequestSuperGlobalConditional_worker(t *testing.T) {\n\t// This test verifies that $_REQUEST works correctly when accessed conditionally\n\t// in worker mode. The first request does NOT access $_REQUEST, but subsequent\n\t// requests do. This tests the \"re-arm\" mechanism for JIT auto globals.\n\t//\n\t// The bug scenario:\n\t// - Request 1 (i=1): includes file, $_REQUEST initialized with val=1\n\t// - Request 3 (i=3): includes file from cache, $_REQUEST should have val=3\n\t// If the bug exists, $_REQUEST would still have val=1 from request 1.\n\tphpIni := make(map[string]string)\n\tphpIni[\"auto_globals_jit\"] = \"1\"\n\trunTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {\n\t\tif i%2 == 0 {\n\t\t\t// Even requests: don't use $_REQUEST\n\t\t\tbody, _ := testGet(fmt.Sprintf(\"http://example.com/request-superglobal-conditional.php?val=%d\", i), handler, t)\n\t\t\tassert.Contains(t, body, \"SKIPPED\")\n\t\t\tassert.Contains(t, body, fmt.Sprintf(\"'val' => '%d'\", i))\n\t\t} else {\n\t\t\t// Odd requests: use $_REQUEST\n\t\t\tbody, _ := testGet(fmt.Sprintf(\"http://example.com/request-superglobal-conditional.php?use_request=1&val=%d\", i), handler, t)\n\t\t\tassert.Contains(t, body, \"REQUEST:\")\n\t\t\tassert.Contains(t, body, \"REQUEST_COUNT:2\", \"$_REQUEST should have ONLY current request's data (2 keys: use_request and val)\")\n\t\t\tassert.Contains(t, body, fmt.Sprintf(\"'val' => '%d'\", i), \"request data is not present\")\n\t\t\tassert.Contains(t, body, \"'use_request' => '1'\")\n\t\t\tassert.Contains(t, body, \"VAL_CHECK:MATCH\", \"BUG: $_REQUEST contains stale data from previous request! Body: \"+body)\n\t\t}\n\t}, &testOptions{workerScript: \"request-superglobal-conditional.php\", phpIni: phpIni})\n}\n\nfunc TestCookies_module(t *testing.T) { testCookies(t, nil) }\nfunc TestCookies_worker(t *testing.T) { testCookies(t, &testOptions{workerScript: \"cookies.php\"}) }\nfunc testCookies(t *testing.T, opts *testOptions) {\n\trunTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {\n\t\treq := httptest.NewRequest(\"GET\", \"http://example.com/cookies.php\", nil)\n\t\treq.AddCookie(&http.Cookie{Name: \"foo\", Value: \"bar\"})\n\t\treq.AddCookie(&http.Cookie{Name: \"i\", Value: fmt.Sprintf(\"%d\", i)})\n\t\tbody, _ := testRequest(req, handler, t)\n\n\t\tassert.Contains(t, body, \"'foo' => 'bar'\")\n\t\tassert.Contains(t, body, fmt.Sprintf(\"'i' => '%d'\", i))\n\t}, opts)\n}\n\nfunc TestMalformedCookie(t *testing.T) {\n\trunTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {\n\t\treq := httptest.NewRequest(\"GET\", \"http://example.com/cookies.php\", nil)\n\t\treq.Header.Add(\"Cookie\", \"foo =bar; ===;;==;  .dot.=val  ;\\x00 ; PHPSESSID=1234\")\n\t\t// Multiple Cookie header should be joined https://www.rfc-editor.org/rfc/rfc7540#section-8.1.2.5\n\t\treq.Header.Add(\"Cookie\", \"secondCookie=test; secondCookie=overwritten\")\n\t\tbody, _ := testRequest(req, handler, t)\n\n\t\tassert.Contains(t, body, \"'foo_' => 'bar'\")\n\t\tassert.Contains(t, body, \"'_dot_' => 'val  '\")\n\n\t\t// PHPSESSID should still be present since we remove the null byte\n\t\tassert.Contains(t, body, \"'PHPSESSID' => '1234'\")\n\n\t\t// The cookie in the second headers should be present,\n\t\t// but it should not be overwritten by following values\n\t\tassert.Contains(t, body, \"'secondCookie' => 'test'\")\n\n\t}, &testOptions{nbParallelRequests: 1})\n}\n\nfunc TestSession_module(t *testing.T) { testSession(t, nil) }\nfunc TestSession_worker(t *testing.T) {\n\ttestSession(t, &testOptions{workerScript: \"session.php\"})\n}\nfunc testSession(t *testing.T, opts *testOptions) {\n\tif opts == nil {\n\t\topts = &testOptions{}\n\t}\n\topts.realServer = true\n\n\trunTest(t, func(_ func(http.ResponseWriter, *http.Request), ts *httptest.Server, i int) {\n\t\tjar, err := cookiejar.New(&cookiejar.Options{})\n\t\tassert.NoError(t, err)\n\n\t\tclient := &http.Client{Jar: jar}\n\n\t\tresp1, err := client.Get(ts.URL + \"/session.php\")\n\t\tassert.NoError(t, err)\n\n\t\tbody1, _ := io.ReadAll(resp1.Body)\n\t\tassert.Equal(t, \"Count: 0\\n\", string(body1))\n\n\t\tresp2, err := client.Get(ts.URL + \"/session.php\")\n\t\tassert.NoError(t, err)\n\n\t\tbody2, _ := io.ReadAll(resp2.Body)\n\t\tassert.Equal(t, \"Count: 1\\n\", string(body2))\n\t}, opts)\n}\n\nfunc TestPhpInfo_module(t *testing.T) { testPhpInfo(t, nil) }\nfunc TestPhpInfo_worker(t *testing.T) { testPhpInfo(t, &testOptions{workerScript: \"phpinfo.php\"}) }\nfunc testPhpInfo(t *testing.T, opts *testOptions) {\n\tvar logOnce sync.Once\n\trunTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {\n\t\tbody, _ := testGet(fmt.Sprintf(\"http://example.com/phpinfo.php?i=%d\", i), handler, t)\n\n\t\tlogOnce.Do(func() {\n\t\t\tt.Log(body)\n\t\t})\n\n\t\tassert.Contains(t, body, \"frankenphp\")\n\t\tassert.Contains(t, body, fmt.Sprintf(\"i=%d\", i))\n\t}, opts)\n}\n\nfunc TestPersistentObject_module(t *testing.T) { testPersistentObject(t, nil) }\nfunc TestPersistentObject_worker(t *testing.T) {\n\ttestPersistentObject(t, &testOptions{workerScript: \"persistent-object.php\"})\n}\nfunc testPersistentObject(t *testing.T, opts *testOptions) {\n\trunTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {\n\t\tbody, _ := testGet(fmt.Sprintf(\"http://example.com/persistent-object.php?i=%d\", i), handler, t)\n\n\t\tassert.Equal(t, fmt.Sprintf(`request: %d\nclass exists: 1\nid: obj1\nobject id: 1`, i), body)\n\t}, opts)\n}\n\nfunc TestAutoloader_module(t *testing.T) { testAutoloader(t, nil) }\nfunc TestAutoloader_worker(t *testing.T) {\n\ttestAutoloader(t, &testOptions{workerScript: \"autoloader.php\"})\n}\nfunc testAutoloader(t *testing.T, opts *testOptions) {\n\trunTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {\n\t\tbody, _ := testGet(fmt.Sprintf(\"http://example.com/autoloader.php?i=%d\", i), handler, t)\n\n\t\tassert.Equal(t, fmt.Sprintf(`request %d\nmy_autoloader`, i), body)\n\t}, opts)\n}\n\nfunc TestLog_error_log_module(t *testing.T) { testLog_error_log(t, &testOptions{}) }\nfunc TestLog_error_log_worker(t *testing.T) {\n\ttestLog_error_log(t, &testOptions{workerScript: \"log-error_log.php\"})\n}\nfunc testLog_error_log(t *testing.T, opts *testOptions) {\n\tvar buf fmt.Stringer\n\topts.logger, buf = newTestLogger(t)\n\n\trunTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {\n\t\treq := httptest.NewRequest(\"GET\", fmt.Sprintf(\"http://example.com/log-error_log.php?i=%d\", i), nil)\n\t\tw := httptest.NewRecorder()\n\t\thandler(w, req)\n\n\t\tassert.Contains(t, buf.String(), fmt.Sprintf(\"request %d\", i))\n\t}, opts)\n}\n\nfunc TestLog_frankenphp_log_module(t *testing.T) { testLog_frankenphp_log(t, &testOptions{}) }\nfunc TestLog_frankenphp_log_worker(t *testing.T) {\n\ttestLog_frankenphp_log(t, &testOptions{workerScript: \"log-frankenphp_log.php\"})\n}\nfunc testLog_frankenphp_log(t *testing.T, opts *testOptions) {\n\tvar buf fmt.Stringer\n\topts.logger, buf = newTestLogger(t)\n\n\trunTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {\n\t\treq := httptest.NewRequest(\"GET\", fmt.Sprintf(\"http://example.com/log-frankenphp_log.php?i=%d\", i), nil)\n\t\tw := httptest.NewRecorder()\n\t\thandler(w, req)\n\n\t\tlogs := buf.String()\n\t\tfor _, message := range []string{\n\t\t\t`level=INFO msg=\"default level message\"`,\n\t\t\tfmt.Sprintf(`level=DEBUG msg=\"some debug message %d\" \"key int\"=1`, i),\n\t\t\tfmt.Sprintf(`level=INFO msg=\"some info message %d\" \"key string\"=string`, i),\n\t\t\tfmt.Sprintf(`level=WARN msg=\"some warn message %d\"`, i),\n\t\t\tfmt.Sprintf(`level=ERROR msg=\"some error message %d\" err=\"[a v]\"`, i),\n\t\t} {\n\t\t\tassert.Contains(t, logs, message)\n\t\t}\n\t}, opts)\n}\n\nfunc TestConnectionAbort_module(t *testing.T) { testConnectionAbort(t, &testOptions{}) }\nfunc TestConnectionAbort_worker(t *testing.T) {\n\ttestConnectionAbort(t, &testOptions{workerScript: \"connection_status.php\"})\n}\nfunc testConnectionAbort(t *testing.T, opts *testOptions) {\n\ttestFinish := func(finish string) {\n\t\tt.Run(fmt.Sprintf(\"finish=%s\", finish), func(t *testing.T) {\n\t\t\tvar buf syncBuffer\n\t\t\topts.logger = slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelInfo}))\n\n\t\t\trunTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {\n\t\t\t\treq := httptest.NewRequest(\"GET\", fmt.Sprintf(\"http://example.com/connection_status.php?i=%d&finish=%s\", i, finish), nil)\n\t\t\t\tw := httptest.NewRecorder()\n\n\t\t\t\tctx, cancel := context.WithCancel(req.Context())\n\t\t\t\treq = req.WithContext(ctx)\n\t\t\t\tcancel()\n\t\t\t\thandler(w, req)\n\n\t\t\t\tfor !strings.Contains(buf.String(), fmt.Sprintf(\"request %d: 1\", i)) {\n\t\t\t\t}\n\t\t\t}, opts)\n\t\t})\n\t}\n\n\ttestFinish(\"0\")\n\ttestFinish(\"1\")\n}\n\nfunc TestException_module(t *testing.T) { testException(t, &testOptions{}) }\nfunc TestException_worker(t *testing.T) {\n\ttestException(t, &testOptions{workerScript: \"exception.php\"})\n}\nfunc testException(t *testing.T, opts *testOptions) {\n\trunTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {\n\t\tbody, _ := testGet(fmt.Sprintf(\"http://example.com/exception.php?i=%d\", i), handler, t)\n\n\t\tassert.Contains(t, body, \"hello\")\n\t\tassert.Contains(t, body, fmt.Sprintf(`Uncaught Exception: request %d`, i))\n\t}, opts)\n}\n\nfunc TestEarlyHints_module(t *testing.T) { testEarlyHints(t, &testOptions{}) }\nfunc TestEarlyHints_worker(t *testing.T) {\n\ttestEarlyHints(t, &testOptions{workerScript: \"early-hints.php\"})\n}\nfunc testEarlyHints(t *testing.T, opts *testOptions) {\n\trunTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {\n\t\tvar earlyHintReceived bool\n\t\ttrace := &httptrace.ClientTrace{\n\t\t\tGot1xxResponse: func(code int, header textproto.MIMEHeader) error {\n\t\t\t\tswitch code {\n\t\t\t\tcase http.StatusEarlyHints:\n\t\t\t\t\tassert.Equal(t, \"</style.css>; rel=preload; as=style\", header.Get(\"Link\"))\n\t\t\t\t\tassert.Equal(t, strconv.Itoa(i), header.Get(\"Request\"))\n\n\t\t\t\t\tearlyHintReceived = true\n\t\t\t\t}\n\n\t\t\t\treturn nil\n\t\t\t},\n\t\t}\n\t\treq := httptest.NewRequest(\"GET\", fmt.Sprintf(\"http://example.com/early-hints.php?i=%d\", i), nil)\n\t\tw := NewRecorder()\n\t\tw.ClientTrace = trace\n\t\thandler(w, req)\n\n\t\tassert.Equal(t, strconv.Itoa(i), w.Header().Get(\"Request\"))\n\t\tassert.Equal(t, \"\", w.Header().Get(\"Link\"))\n\n\t\tassert.True(t, earlyHintReceived)\n\t}, opts)\n}\n\ntype streamResponseRecorder struct {\n\t*httptest.ResponseRecorder\n\twriteCallback func(buf []byte)\n}\n\nfunc (srr *streamResponseRecorder) Write(buf []byte) (int, error) {\n\tsrr.writeCallback(buf)\n\n\treturn srr.ResponseRecorder.Write(buf)\n}\n\nfunc TestFlush_module(t *testing.T) { testFlush(t, &testOptions{}) }\nfunc TestFlush_worker(t *testing.T) {\n\ttestFlush(t, &testOptions{workerScript: \"flush.php\"})\n}\nfunc testFlush(t *testing.T, opts *testOptions) {\n\trunTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {\n\t\tvar j int\n\n\t\treq := httptest.NewRequest(\"GET\", fmt.Sprintf(\"http://example.com/flush.php?i=%d\", i), nil)\n\t\tw := &streamResponseRecorder{httptest.NewRecorder(), func(buf []byte) {\n\t\t\tif j == 0 {\n\t\t\t\tassert.Equal(t, []byte(\"He\"), buf)\n\t\t\t} else {\n\t\t\t\tassert.Equal(t, fmt.Appendf(nil, \"llo %d\", i), buf)\n\t\t\t}\n\n\t\t\tj++\n\t\t}}\n\t\thandler(w, req)\n\n\t\tassert.Equal(t, 2, j)\n\t}, opts)\n}\n\nfunc TestLargeRequest_module(t *testing.T) {\n\ttestLargeRequest(t, &testOptions{})\n}\nfunc TestLargeRequest_worker(t *testing.T) {\n\ttestLargeRequest(t, &testOptions{workerScript: \"large-request.php\"})\n}\nfunc testLargeRequest(t *testing.T, opts *testOptions) {\n\trunTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {\n\t\tbody, _ := testPost(\n\t\t\tfmt.Sprintf(\"http://example.com/large-request.php?i=%d\", i),\n\t\t\tstrings.Repeat(\"f\", 6_048_576),\n\t\t\thandler,\n\t\t\tt,\n\t\t)\n\n\t\tassert.Contains(t, body, fmt.Sprintf(\"Request body size: 6048576 (%d)\", i))\n\t}, opts)\n}\n\nfunc TestVersion(t *testing.T) {\n\tv := frankenphp.Version()\n\n\tassert.GreaterOrEqual(t, v.MajorVersion, 8)\n\tassert.GreaterOrEqual(t, v.MinorVersion, 0)\n\tassert.GreaterOrEqual(t, v.ReleaseVersion, 0)\n\tassert.GreaterOrEqual(t, v.VersionID, 0)\n\tassert.NotEmpty(t, v.Version, 0)\n}\n\nfunc TestFiberNoCgo_module(t *testing.T) { testFiberNoCgo(t, &testOptions{}) }\nfunc TestFiberNonCgo_worker(t *testing.T) {\n\ttestFiberNoCgo(t, &testOptions{workerScript: \"fiber-no-cgo.php\"})\n}\nfunc testFiberNoCgo(t *testing.T, opts *testOptions) {\n\trunTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {\n\t\tbody, _ := testGet(fmt.Sprintf(\"http://example.com/fiber-no-cgo.php?i=%d\", i), handler, t)\n\t\tassert.Equal(t, body, fmt.Sprintf(\"Fiber %d\", i))\n\t}, opts)\n}\n\nfunc TestFiberBasic_module(t *testing.T) { testFiberBasic(t, &testOptions{}) }\nfunc TestFiberBasic_worker(t *testing.T) {\n\ttestFiberBasic(t, &testOptions{workerScript: \"fiber-basic.php\"})\n}\nfunc testFiberBasic(t *testing.T, opts *testOptions) {\n\trunTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {\n\t\tbody, _ := testGet(fmt.Sprintf(\"http://example.com/fiber-basic.php?i=%d\", i), handler, t)\n\t\tassert.Equal(t, body, fmt.Sprintf(\"Fiber %d\", i))\n\t}, opts)\n}\n\nfunc TestRequestHeaders_module(t *testing.T) { testRequestHeaders(t, &testOptions{}) }\nfunc TestRequestHeaders_worker(t *testing.T) {\n\ttestRequestHeaders(t, &testOptions{workerScript: \"request-headers.php\"})\n}\nfunc testRequestHeaders(t *testing.T, opts *testOptions) {\n\trunTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {\n\t\treq := httptest.NewRequest(\"GET\", fmt.Sprintf(\"http://example.com/request-headers.php?i=%d\", i), nil)\n\t\treq.Header.Add(strings.Clone(\"Content-Type\"), strings.Clone(\"text/plain\"))\n\t\treq.Header.Add(strings.Clone(\"Frankenphp-I\"), strings.Clone(strconv.Itoa(i)))\n\t\tbody, _ := testRequest(req, handler, t)\n\n\t\tassert.Contains(t, body, \"[Content-Type] => text/plain\")\n\t\tassert.Contains(t, body, fmt.Sprintf(\"[Frankenphp-I] => %d\", i))\n\t}, opts)\n}\n\nfunc TestFailingWorker(t *testing.T) {\n\tt.Cleanup(frankenphp.Shutdown)\n\n\terr := frankenphp.Init(\n\t\tfrankenphp.WithWorkers(\"failing worker\", \"testdata/failing-worker.php\", 4, frankenphp.WithWorkerMaxFailures(1)),\n\t\tfrankenphp.WithNumThreads(5),\n\t)\n\tassert.Error(t, err, \"should return an immediate error if workers fail on startup\")\n}\n\nfunc TestEnv_module(t *testing.T) {\n\ttestEnv(t, &testOptions{nbParallelRequests: 1, phpIni: map[string]string{\"variables_order\": \"EGPCS\"}})\n}\nfunc TestEnv_worker(t *testing.T) {\n\ttestEnv(t, &testOptions{nbParallelRequests: 1, workerScript: \"env/test-env.php\", phpIni: map[string]string{\"variables_order\": \"EGPCS\"}})\n}\n\n// testEnv cannot be run in parallel due to https://github.com/golang/go/issues/63567\nfunc testEnv(t *testing.T, opts *testOptions) {\n\tassert.NoError(t, os.Setenv(\"EMPTY\", \"\"))\n\n\trunTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {\n\t\tbody, _ := testGet(fmt.Sprintf(\"http://example.com/env/test-env.php?var=%d\", i), handler, t)\n\n\t\t// execute the script as regular php script\n\t\tcmd := exec.Command(\"php\", \"testdata/env/test-env.php\", strconv.Itoa(i))\n\t\tstdoutStderr, err := cmd.CombinedOutput()\n\t\tif err != nil {\n\t\t\t// php is not installed or other issue, use the hardcoded output below:\n\t\t\tstdoutStderr = []byte(\"Set MY_VAR successfully.\\nMY_VAR = HelloWorld\\nMY_VAR not found in $_ENV.\\nMY_VAR not found in $_SERVER.\\nUnset MY_VAR successfully.\\nMY_VAR is unset.\\nMY_VAR set to empty successfully.\\nMY_VAR = \\nUnset NON_EXISTING_VAR successfully.\\nInvalid value was not inserted.\\n\")\n\t\t}\n\n\t\tassert.Equal(t, string(stdoutStderr), body)\n\t}, opts)\n}\n\nfunc TestEnvIsResetInNonWorkerMode(t *testing.T) {\n\tassert.NoError(t, os.Setenv(\"test\", \"\"))\n\trunTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {\n\t\tputResult, _ := testGet(fmt.Sprintf(\"http://example.com/env/putenv.php?key=test&put=%d\", i), handler, t)\n\n\t\tassert.Equal(t, fmt.Sprintf(\"test=%d\", i), putResult, \"putenv and then echo getenv\")\n\n\t\tgetResult, _ := testGet(\"http://example.com/env/putenv.php?key=test\", handler, t)\n\n\t\tassert.Equal(t, \"test=\", getResult, \"putenv should be reset across requests\")\n\t}, &testOptions{})\n}\n\n// TODO: should it actually get reset in worker mode?\nfunc TestEnvIsNotResetInWorkerMode(t *testing.T) {\n\tassert.NoError(t, os.Setenv(\"index\", \"\"))\n\trunTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {\n\t\tputResult, _ := testGet(fmt.Sprintf(\"http://example.com/env/remember-env.php?index=%d\", i), handler, t)\n\n\t\tassert.Equal(t, \"success\", putResult, \"putenv and then echo getenv\")\n\n\t\tgetResult, _ := testGet(\"http://example.com/env/remember-env.php\", handler, t)\n\n\t\tassert.Equal(t, \"success\", getResult, \"putenv should not be reset across worker requests\")\n\t}, &testOptions{workerScript: \"env/remember-env.php\"})\n}\n\n// reproduction of https://github.com/php/frankenphp/issues/1061\nfunc TestModificationsToEnvPersistAcrossRequests(t *testing.T) {\n\trunTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {\n\t\tfor range 3 {\n\t\t\tresult, _ := testGet(\"http://example.com/env/overwrite-env.php\", handler, t)\n\t\t\tassert.Equal(t, \"custom_value\", result, \"a var directly added to $_ENV should persist\")\n\t\t}\n\t}, &testOptions{\n\t\tworkerScript: \"env/overwrite-env.php\",\n\t\tphpIni:       map[string]string{\"variables_order\": \"EGPCS\"},\n\t})\n}\n\nfunc TestFileUpload_module(t *testing.T) { testFileUpload(t, &testOptions{}) }\nfunc TestFileUpload_worker(t *testing.T) {\n\ttestFileUpload(t, &testOptions{workerScript: \"file-upload.php\"})\n}\nfunc testFileUpload(t *testing.T, opts *testOptions) {\n\trunTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {\n\t\trequestBody := &bytes.Buffer{}\n\t\twriter := multipart.NewWriter(requestBody)\n\t\tpart, _ := writer.CreateFormFile(\"file\", \"foo.txt\")\n\t\t_, err := part.Write([]byte(\"bar\"))\n\t\trequire.NoError(t, err)\n\n\t\trequire.NoError(t, writer.Close())\n\n\t\treq := httptest.NewRequest(\"POST\", \"http://example.com/file-upload.php\", requestBody)\n\t\treq.Header.Add(\"Content-Type\", writer.FormDataContentType())\n\n\t\tbody, _ := testRequest(req, handler, t)\n\n\t\tassert.Contains(t, string(body), \"Upload OK\")\n\t}, opts)\n}\n\nfunc ExampleServeHTTP() {\n\tif err := frankenphp.Init(); err != nil {\n\t\tpanic(err)\n\t}\n\tdefer frankenphp.Shutdown()\n\n\thttp.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) {\n\t\treq, err := frankenphp.NewRequestWithContext(r, frankenphp.WithRequestDocumentRoot(\"/path/to/document/root\", false))\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\n\t\tif err := frankenphp.ServeHTTP(w, req); err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t})\n\tlog.Fatal(http.ListenAndServe(\":8080\", nil))\n}\n\nfunc BenchmarkHelloWorld(b *testing.B) {\n\trequire.NoError(b, frankenphp.Init())\n\tb.Cleanup(frankenphp.Shutdown)\n\n\tcwd, _ := os.Getwd()\n\ttestDataDir := cwd + \"/testdata/\"\n\n\topt := frankenphp.WithRequestDocumentRoot(testDataDir, false)\n\thandler := func(w http.ResponseWriter, r *http.Request) {\n\t\treq, err := frankenphp.NewRequestWithContext(r, opt)\n\t\trequire.NoError(b, err)\n\n\t\trequire.NoError(b, frankenphp.ServeHTTP(w, req))\n\t}\n\n\treq := httptest.NewRequest(\"GET\", \"http://example.com/index.php\", nil)\n\tw := httptest.NewRecorder()\n\n\tfor b.Loop() {\n\t\thandler(w, req)\n\t}\n}\n\nfunc BenchmarkEcho(b *testing.B) {\n\trequire.NoError(b, frankenphp.Init())\n\tb.Cleanup(frankenphp.Shutdown)\n\n\tcwd, _ := os.Getwd()\n\ttestDataDir := cwd + \"/testdata/\"\n\n\topt := frankenphp.WithRequestDocumentRoot(testDataDir, false)\n\thandler := func(w http.ResponseWriter, r *http.Request) {\n\t\treq, err := frankenphp.NewRequestWithContext(r, opt)\n\t\trequire.NoError(b, err)\n\n\t\trequire.NoError(b, frankenphp.ServeHTTP(w, req))\n\t}\n\n\tconst body = `{\n\t\t\"squadName\": \"Super hero squad\",\n\t\t\"homeTown\": \"Metro City\",\n\t\t\"formed\": 2016,\n\t\t\"secretBase\": \"Super tower\",\n\t\t\"active\": true,\n\t\t\"members\": [\n\t\t  {\n\t\t\t\"name\": \"Molecule Man\",\n\t\t\t\"age\": 29,\n\t\t\t\"secretIdentity\": \"Dan Jukes\",\n\t\t\t\"powers\": [\"Radiation resistance\", \"Turning tiny\", \"Radiation blast\"]\n\t\t  },\n\t\t  {\n\t\t\t\"name\": \"Madame Uppercut\",\n\t\t\t\"age\": 39,\n\t\t\t\"secretIdentity\": \"Jane Wilson\",\n\t\t\t\"powers\": [\n\t\t\t  \"Million tonne punch\",\n\t\t\t  \"Damage resistance\",\n\t\t\t  \"Superhuman reflexes\"\n\t\t\t]\n\t\t  },\n\t\t  {\n\t\t\t\"name\": \"Eternal Flame\",\n\t\t\t\"age\": 1000000,\n\t\t\t\"secretIdentity\": \"Unknown\",\n\t\t\t\"powers\": [\n\t\t\t  \"Immortality\",\n\t\t\t  \"Heat Immunity\",\n\t\t\t  \"Inferno\",\n\t\t\t  \"Teleportation\",\n\t\t\t  \"Interdimensional travel\"\n\t\t\t]\n\t\t  }\n\t\t]\n\t  }`\n\n\tr := strings.NewReader(body)\n\treq := httptest.NewRequest(\"POST\", \"http://example.com/echo.php\", r)\n\tw := httptest.NewRecorder()\n\n\tfor b.Loop() {\n\t\tr.Reset(body)\n\t\thandler(w, req)\n\t}\n}\n\nfunc BenchmarkServerSuperGlobal(b *testing.B) {\n\trequire.NoError(b, frankenphp.Init())\n\tb.Cleanup(frankenphp.Shutdown)\n\n\tcwd, _ := os.Getwd()\n\ttestDataDir := cwd + \"/testdata/\"\n\n\t// Mimics headers of a request sent by Firefox to GitHub\n\theaders := http.Header{}\n\theaders.Add(strings.Clone(\"Accept\"), strings.Clone(\"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8\"))\n\theaders.Add(strings.Clone(\"Accept-Encoding\"), strings.Clone(\"gzip, deflate, br\"))\n\theaders.Add(strings.Clone(\"Accept-Language\"), strings.Clone(\"fr,fr-FR;q=0.8,en-US;q=0.5,en;q=0.3\"))\n\theaders.Add(strings.Clone(\"Cache-Control\"), strings.Clone(\"no-cache\"))\n\theaders.Add(strings.Clone(\"Connection\"), strings.Clone(\"keep-alive\"))\n\theaders.Add(strings.Clone(\"Cookie\"), strings.Clone(\"user_session=myrandomuuid; __Host-user_session_same_site=myotherrandomuuid; dotcom_user=dunglas; logged_in=yes; _foo=barbarbarbarbarbar; _device_id=anotherrandomuuid; color_mode=foobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobar; preferred_color_mode=light; tz=Europe%2FParis; has_recent_activity=1\"))\n\theaders.Add(strings.Clone(\"DNT\"), strings.Clone(\"1\"))\n\theaders.Add(strings.Clone(\"Host\"), strings.Clone(\"example.com\"))\n\theaders.Add(strings.Clone(\"Pragma\"), strings.Clone(\"no-cache\"))\n\theaders.Add(strings.Clone(\"Sec-Fetch-Dest\"), strings.Clone(\"document\"))\n\theaders.Add(strings.Clone(\"Sec-Fetch-Mode\"), strings.Clone(\"navigate\"))\n\theaders.Add(strings.Clone(\"Sec-Fetch-Site\"), strings.Clone(\"cross-site\"))\n\theaders.Add(strings.Clone(\"Sec-GPC\"), strings.Clone(\"1\"))\n\theaders.Add(strings.Clone(\"Upgrade-Insecure-Requests\"), strings.Clone(\"1\"))\n\theaders.Add(strings.Clone(\"User-Agent\"), strings.Clone(\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:122.0) Gecko/20100101 Firefox/122.0\"))\n\n\t// Env vars available in a typical Docker container\n\tenv := map[string]string{\n\t\t\"HOSTNAME\":        \"a88e81aa22e4\",\n\t\t\"PHP_INI_DIR\":     \"/usr/local/etc/php\",\n\t\t\"HOME\":            \"/root\",\n\t\t\"GODEBUG\":         \"cgocheck=0\",\n\t\t\"PHP_LDFLAGS\":     \"-Wl,-O1 -pie\",\n\t\t\"PHP_CFLAGS\":      \"-fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64\",\n\t\t\"PHP_VERSION\":     \"8.3.2\",\n\t\t\"GPG_KEYS\":        \"1198C0117593497A5EC5C199286AF1F9897469DC C28D937575603EB4ABB725861C0779DC5C0A9DE4 AFD8691FDAEDF03BDF6E460563F15A9B715376CA\",\n\t\t\"PHP_CPPFLAGS\":    \"-fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64\",\n\t\t\"PHP_ASC_URL\":     \"https://www.php.net/distributions/php-8.3.2.tar.xz.asc\",\n\t\t\"PHP_URL\":         \"https://www.php.net/distributions/php-8.3.2.tar.xz\",\n\t\t\"PATH\":            \"/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\n\t\t\"XDG_CONFIG_HOME\": \"/config\",\n\t\t\"XDG_DATA_HOME\":   \"/data\",\n\t\t\"PHPIZE_DEPS\":     \"autoconf dpkg-dev file g++ gcc libc-dev make pkg-config re2c\",\n\t\t\"PWD\":             \"/app\",\n\t\t\"PHP_SHA256\":      \"4ffa3e44afc9c590e28dc0d2d31fc61f0139f8b335f11880a121b9f9b9f0634e\",\n\t}\n\n\tpreparedEnv := frankenphp.PrepareEnv(env)\n\n\topts := []frankenphp.RequestOption{frankenphp.WithRequestDocumentRoot(testDataDir, false), frankenphp.WithRequestPreparedEnv(preparedEnv)}\n\thandler := func(w http.ResponseWriter, r *http.Request) {\n\t\treq, err := frankenphp.NewRequestWithContext(r, opts...)\n\t\trequire.NoError(b, err)\n\n\t\tr.Header = headers\n\n\t\trequire.NoError(b, frankenphp.ServeHTTP(w, req))\n\t}\n\n\treq := httptest.NewRequest(\"GET\", \"http://example.com/server-variable.php\", nil)\n\tw := httptest.NewRecorder()\n\n\tfor b.Loop() {\n\t\thandler(w, req)\n\t}\n}\n\nfunc BenchmarkUncommonHeaders(b *testing.B) {\n\trequire.NoError(b, frankenphp.Init())\n\tb.Cleanup(frankenphp.Shutdown)\n\n\tcwd, _ := os.Getwd()\n\ttestDataDir := cwd + \"/testdata/\"\n\n\t// Mimics headers of a request sent by Firefox to GitHub\n\theaders := http.Header{}\n\theaders.Add(strings.Clone(\"Accept\"), strings.Clone(\"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8\"))\n\theaders.Add(strings.Clone(\"Accept-Encoding\"), strings.Clone(\"gzip, deflate, br\"))\n\theaders.Add(strings.Clone(\"Accept-Language\"), strings.Clone(\"fr,fr-FR;q=0.8,en-US;q=0.5,en;q=0.3\"))\n\theaders.Add(strings.Clone(\"Cache-Control\"), strings.Clone(\"no-cache\"))\n\theaders.Add(strings.Clone(\"Connection\"), strings.Clone(\"keep-alive\"))\n\theaders.Add(strings.Clone(\"Cookie\"), strings.Clone(\"user_session=myrandomuuid; __Host-user_session_same_site=myotherrandomuuid; dotcom_user=dunglas; logged_in=yes; _foo=barbarbarbarbarbar; _device_id=anotherrandomuuid; color_mode=foobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobar; preferred_color_mode=light; tz=Europe%2FParis; has_recent_activity=1\"))\n\theaders.Add(strings.Clone(\"DNT\"), strings.Clone(\"1\"))\n\theaders.Add(strings.Clone(\"Host\"), strings.Clone(\"example.com\"))\n\theaders.Add(strings.Clone(\"Pragma\"), strings.Clone(\"no-cache\"))\n\theaders.Add(strings.Clone(\"Sec-Fetch-Dest\"), strings.Clone(\"document\"))\n\theaders.Add(strings.Clone(\"Sec-Fetch-Mode\"), strings.Clone(\"navigate\"))\n\theaders.Add(strings.Clone(\"Sec-Fetch-Site\"), strings.Clone(\"cross-site\"))\n\theaders.Add(strings.Clone(\"Sec-GPC\"), strings.Clone(\"1\"))\n\theaders.Add(strings.Clone(\"Upgrade-Insecure-Requests\"), strings.Clone(\"1\"))\n\theaders.Add(strings.Clone(\"User-Agent\"), strings.Clone(\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:122.0) Gecko/20100101 Firefox/122.0\"))\n\t// Some uncommon headers\n\theaders.Add(strings.Clone(\"X-Super-Custom\"), strings.Clone(\"Foo\"))\n\theaders.Add(strings.Clone(\"Super-Super-Custom\"), strings.Clone(\"Foo\"))\n\theaders.Add(strings.Clone(\"Super-Super-Custom\"), strings.Clone(\"Bar\"))\n\theaders.Add(strings.Clone(\"Very-Custom\"), strings.Clone(\"1\"))\n\n\topt := frankenphp.WithRequestDocumentRoot(testDataDir, false)\n\thandler := func(w http.ResponseWriter, r *http.Request) {\n\t\treq, err := frankenphp.NewRequestWithContext(r, opt)\n\t\trequire.NoError(b, err)\n\n\t\tr.Header = headers\n\n\t\trequire.NoError(b, frankenphp.ServeHTTP(w, req))\n\t}\n\n\treq := httptest.NewRequest(\"GET\", \"http://example.com/server-variable.php\", nil)\n\tw := httptest.NewRecorder()\n\n\tfor b.Loop() {\n\t\thandler(w, req)\n\t}\n}\n\nfunc TestRejectInvalidHeaders_module(t *testing.T) { testRejectInvalidHeaders(t, &testOptions{}) }\nfunc TestRejectInvalidHeaders_worker(t *testing.T) {\n\ttestRejectInvalidHeaders(t, &testOptions{workerScript: \"headers.php\"})\n}\nfunc testRejectInvalidHeaders(t *testing.T, opts *testOptions) {\n\tinvalidHeaders := [][]string{\n\t\t{\"Content-Length\", \"-1\"},\n\t\t{\"Content-Length\", \"something\"},\n\t}\n\tfor _, header := range invalidHeaders {\n\t\trunTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, _ int) {\n\t\t\treq := httptest.NewRequest(\"GET\", \"http://example.com/headers.php\", nil)\n\t\t\treq.Header.Add(header[0], header[1])\n\t\t\tbody, resp := testRequest(req, handler, t)\n\n\t\t\tassert.Equal(t, 400, resp.StatusCode)\n\t\t\tassert.Contains(t, body, \"invalid\")\n\t\t}, opts)\n\t}\n}\n\nfunc TestFlushEmptyResponse_module(t *testing.T) { testFlushEmptyResponse(t, &testOptions{}) }\nfunc TestFlushEmptyResponse_worker(t *testing.T) {\n\ttestFlushEmptyResponse(t, &testOptions{workerScript: \"only-headers.php\"})\n}\n\nfunc testFlushEmptyResponse(t *testing.T, opts *testOptions) {\n\trunTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, _ int) {\n\t\t_, resp := testGet(\"http://example.com/only-headers.php\", handler, t)\n\t\tassert.Equal(t, 204, resp.StatusCode)\n\t}, opts)\n}\n\n// Worker mode will clean up unreferenced streams between requests\n// Make sure referenced streams are not cleaned up\nfunc TestFileStreamInWorkerMode(t *testing.T) {\n\trunTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, _ int) {\n\t\tresp1, _ := testGet(\"http://example.com/file-stream.php\", handler, t)\n\t\tassert.Equal(t, resp1, \"word1\")\n\n\t\tresp2, _ := testGet(\"http://example.com/file-stream.php\", handler, t)\n\t\tassert.Equal(t, resp2, \"word2\")\n\n\t\tresp3, _ := testGet(\"http://example.com/file-stream.php\", handler, t)\n\t\tassert.Equal(t, resp3, \"word3\")\n\t}, &testOptions{workerScript: \"file-stream.php\", nbParallelRequests: 1, nbWorkers: 1})\n}\n\n// To run this fuzzing test use: go test -fuzz FuzzRequest\n// TODO: Cover more potential cases\nfunc FuzzRequest(f *testing.F) {\n\tabsPath, _ := fastabs.FastAbs(\"./testdata/\")\n\n\tf.Add(\"hello world\")\n\tf.Add(\"😀😅🙃🤩🥲🤪😘😇😉🐘🧟\")\n\tf.Add(\"%00%11%%22%%33%%44%%55%%66%%77%%88%%99%%aa%%bb%%cc%%dd%%ee%%ff\")\n\tf.Add(\"\\x00\\x01\\x02\\x03\\x04\\x05\\x06\\x07\\x08\\x09\\x0a\\x0b\\x0c\\x0d\\x0e\\x0f\")\n\tf.Fuzz(func(t *testing.T, fuzzedString string) {\n\t\trunTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, _ int) {\n\t\t\treq := httptest.NewRequest(\"GET\", \"http://example.com/server-variable\", nil)\n\t\t\treq.URL = &url.URL{RawQuery: \"test=\" + fuzzedString, Path: \"/server-variable.php/\" + fuzzedString}\n\t\t\treq.Header.Add(strings.Clone(\"Fuzzed\"), strings.Clone(fuzzedString))\n\t\t\treq.Header.Add(strings.Clone(\"Content-Type\"), fuzzedString)\n\t\t\tbody, resp := testRequest(req, handler, t)\n\n\t\t\t// The response status must be 400 if the request path contains null bytes\n\t\t\tif strings.Contains(req.URL.Path, \"\\x00\") {\n\t\t\t\tassert.Equal(t, 400, resp.StatusCode)\n\t\t\t\tassert.Contains(t, body, \"invalid request path\")\n\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// The fuzzed string must be present in the path\n\t\t\tassert.Contains(t, body, fmt.Sprintf(\"[PATH_INFO] => /%s\", fuzzedString))\n\t\t\tassert.Contains(t, body, fmt.Sprintf(\"[PATH_TRANSLATED] => %s\", filepath.Join(absPath, fuzzedString)))\n\n\t\t\t// Headers should always be present even if empty\n\t\t\tassert.Contains(t, body, fmt.Sprintf(\"[CONTENT_TYPE] => %s\", fuzzedString))\n\t\t\tassert.Contains(t, body, fmt.Sprintf(\"[HTTP_FUZZED] => %s\", fuzzedString))\n\t\t}, &testOptions{workerScript: \"request-headers.php\"})\n\t})\n}\n\nfunc TestSessionHandlerReset_worker(t *testing.T) {\n\trunTest(t, func(_ func(http.ResponseWriter, *http.Request), ts *httptest.Server, i int) {\n\t\t// Request 1: Set a custom session handler and start session\n\t\tresp1, err := http.Get(ts.URL + \"/session-handler.php?action=set_handler_and_start&value=test1\")\n\t\tassert.NoError(t, err)\n\t\tbody1, _ := io.ReadAll(resp1.Body)\n\t\t_ = resp1.Body.Close()\n\n\t\tbody1Str := string(body1)\n\t\tassert.Contains(t, body1Str, \"HANDLER_SET_AND_STARTED\")\n\t\tassert.Contains(t, body1Str, \"session.save_handler=user\")\n\n\t\t// Request 2: Start session without setting a custom handler\n\t\t// The user handler from request 1 is preserved (mod_user_names persist),\n\t\t// so session_start() should work without crashing.\n\t\tresp2, err := http.Get(ts.URL + \"/session-handler.php?action=start_without_handler\")\n\t\tassert.NoError(t, err)\n\t\tbody2, _ := io.ReadAll(resp2.Body)\n\t\t_ = resp2.Body.Close()\n\n\t\tbody2Str := string(body2)\n\n\t\t// session_start() should succeed (handlers are preserved)\n\t\tassert.Contains(t, body2Str, \"SESSION_START_RESULT=true\",\n\t\t\t\"session_start() should succeed.\\nResponse: %s\", body2Str)\n\n\t\t// No errors or exceptions should occur\n\t\tassert.NotContains(t, body2Str, \"ERROR:\",\n\t\t\t\"No errors expected.\\nResponse: %s\", body2Str)\n\t\tassert.NotContains(t, body2Str, \"EXCEPTION:\",\n\t\t\t\"No exceptions expected.\\nResponse: %s\", body2Str)\n\n\t}, &testOptions{\n\t\tworkerScript:       \"session-handler.php\",\n\t\tnbWorkers:          1,\n\t\tnbParallelRequests: 1,\n\t\trealServer:         true,\n\t})\n}\n\nfunc TestSessionHandlerPreLoopPreserved_worker(t *testing.T) {\n\trunTest(t, func(_ func(http.ResponseWriter, *http.Request), ts *httptest.Server, i int) {\n\t\t// Request 1: Check that the pre-loop session handler is preserved\n\t\tresp1, err := http.Get(ts.URL + \"/worker-with-session-handler.php?action=check\")\n\t\tassert.NoError(t, err)\n\t\tbody1, _ := io.ReadAll(resp1.Body)\n\t\t_ = resp1.Body.Close()\n\n\t\tbody1Str := string(body1)\n\t\tt.Logf(\"Request 1 response: %s\", body1Str)\n\t\tassert.Contains(t, body1Str, \"HANDLER_PRESERVED\",\n\t\t\t\"Session handler set before loop should be preserved\")\n\t\tassert.Contains(t, body1Str, \"save_handler=user\",\n\t\t\t\"session.save_handler should remain 'user'\")\n\n\t\t// Request 2: Use the session - should work with pre-loop handler\n\t\tresp2, err := http.Get(ts.URL + \"/worker-with-session-handler.php?action=use_session\")\n\t\tassert.NoError(t, err)\n\t\tbody2, _ := io.ReadAll(resp2.Body)\n\t\t_ = resp2.Body.Close()\n\n\t\tbody2Str := string(body2)\n\t\tt.Logf(\"Request 2 response: %s\", body2Str)\n\t\tassert.Contains(t, body2Str, \"SESSION_OK\",\n\t\t\t\"Session should work with pre-loop handler.\\nResponse: %s\", body2Str)\n\t\tassert.NotContains(t, body2Str, \"ERROR:\",\n\t\t\t\"No errors expected.\\nResponse: %s\", body2Str)\n\t\tassert.NotContains(t, body2Str, \"EXCEPTION:\",\n\t\t\t\"No exceptions expected.\\nResponse: %s\", body2Str)\n\n\t\t// Request 3: Check handler is still preserved after using session\n\t\tresp3, err := http.Get(ts.URL + \"/worker-with-session-handler.php?action=check\")\n\t\tassert.NoError(t, err)\n\t\tbody3, _ := io.ReadAll(resp3.Body)\n\t\t_ = resp3.Body.Close()\n\n\t\tbody3Str := string(body3)\n\t\tt.Logf(\"Request 3 response: %s\", body3Str)\n\t\tassert.Contains(t, body3Str, \"HANDLER_PRESERVED\",\n\t\t\t\"Session handler should still be preserved after use\")\n\n\t}, &testOptions{\n\t\tworkerScript:       \"worker-with-session-handler.php\",\n\t\tnbWorkers:          1,\n\t\tnbParallelRequests: 1,\n\t\trealServer:         true,\n\t})\n}\n\nfunc TestSessionNoLeakBetweenRequests_worker(t *testing.T) {\n\trunTest(t, func(_ func(http.ResponseWriter, *http.Request), ts *httptest.Server, i int) {\n\t\t// Client A: Set a secret value in session\n\t\tclientA := &http.Client{}\n\t\tresp1, err := clientA.Get(ts.URL + \"/session-leak.php?action=set&value=secret_A&client_id=clientA\")\n\t\tassert.NoError(t, err)\n\t\tbody1, _ := io.ReadAll(resp1.Body)\n\t\t_ = resp1.Body.Close()\n\n\t\tbody1Str := string(body1)\n\t\tt.Logf(\"Client A set session: %s\", body1Str)\n\t\tassert.Contains(t, body1Str, \"SESSION_SET\")\n\t\tassert.Contains(t, body1Str, \"secret=secret_A\")\n\n\t\t// Client B: Check that session is empty (no cookie, should not see Client A's data)\n\t\tclientB := &http.Client{}\n\t\tresp2, err := clientB.Get(ts.URL + \"/session-leak.php?action=check_empty\")\n\t\tassert.NoError(t, err)\n\t\tbody2, _ := io.ReadAll(resp2.Body)\n\t\t_ = resp2.Body.Close()\n\n\t\tbody2Str := string(body2)\n\t\tt.Logf(\"Client B check empty: %s\", body2Str)\n\t\tassert.Contains(t, body2Str, \"SESSION_CHECK\")\n\t\tassert.Contains(t, body2Str, \"SESSION_EMPTY=true\",\n\t\t\t\"Client B should have empty session, not see Client A's data.\\nResponse: %s\", body2Str)\n\t\tassert.NotContains(t, body2Str, \"secret_A\",\n\t\t\t\"Client A's secret should not leak to Client B.\\nResponse: %s\", body2Str)\n\n\t\t// Client C: Read session without cookie (should also be empty)\n\t\tclientC := &http.Client{}\n\t\tresp3, err := clientC.Get(ts.URL + \"/session-leak.php?action=get\")\n\t\tassert.NoError(t, err)\n\t\tbody3, _ := io.ReadAll(resp3.Body)\n\t\t_ = resp3.Body.Close()\n\n\t\tbody3Str := string(body3)\n\t\tt.Logf(\"Client C get session: %s\", body3Str)\n\t\tassert.Contains(t, body3Str, \"SESSION_READ\")\n\t\tassert.Contains(t, body3Str, \"secret=NOT_FOUND\",\n\t\t\t\"Client C should not find any secret.\\nResponse: %s\", body3Str)\n\t\tassert.Contains(t, body3Str, \"client_id=NOT_FOUND\",\n\t\t\t\"Client C should not find any client_id.\\nResponse: %s\", body3Str)\n\n\t}, &testOptions{\n\t\tworkerScript:       \"session-leak.php\",\n\t\tnbWorkers:          1,\n\t\tnbParallelRequests: 1,\n\t\trealServer:         true,\n\t})\n}\n\nfunc TestSessionNoLeakAfterExit_worker(t *testing.T) {\n\trunTest(t, func(_ func(http.ResponseWriter, *http.Request), ts *httptest.Server, i int) {\n\t\t// Client A: Set a secret value in session and call exit(1)\n\t\tclientA := &http.Client{}\n\t\tresp1, err := clientA.Get(ts.URL + \"/session-leak.php?action=set_and_exit&value=exit_secret&client_id=exitClient\")\n\t\tassert.NoError(t, err)\n\t\tbody1, _ := io.ReadAll(resp1.Body)\n\t\t_ = resp1.Body.Close()\n\n\t\tbody1Str := string(body1)\n\t\tt.Logf(\"Client A set and exit: %s\", body1Str)\n\t\t// The response may be incomplete due to exit(1)\n\t\tassert.Contains(t, body1Str, \"BEFORE_EXIT\")\n\n\t\t// Client B: Check that session is empty (should not see Client A's data)\n\t\t// Retry until the worker has restarted after exit(1)\n\t\tclientB := &http.Client{}\n\t\tvar body2Str string\n\t\tassert.Eventually(t, func() bool {\n\t\t\tresp2, err := clientB.Get(ts.URL + \"/session-leak.php?action=check_empty\")\n\t\t\tif err != nil {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tbody2, _ := io.ReadAll(resp2.Body)\n\t\t\t_ = resp2.Body.Close()\n\t\t\tbody2Str = string(body2)\n\t\t\treturn strings.Contains(body2Str, \"SESSION_CHECK\")\n\t\t}, 2*time.Second, 10*time.Millisecond, \"Worker did not restart in time after exit(1)\")\n\n\t\tt.Logf(\"Client B check empty after exit: %s\", body2Str)\n\t\tassert.Contains(t, body2Str, \"SESSION_EMPTY=true\",\n\t\t\t\"Client B should have empty session after Client A's exit(1).\\nResponse: %s\", body2Str)\n\t\tassert.NotContains(t, body2Str, \"exit_secret\",\n\t\t\t\"Client A's secret should not leak to Client B after exit(1).\\nResponse: %s\", body2Str)\n\n\t\t// Client C: Try to read session (should also be empty)\n\t\tclientC := &http.Client{}\n\t\tresp3, err := clientC.Get(ts.URL + \"/session-leak.php?action=get\")\n\t\tassert.NoError(t, err)\n\t\tbody3, _ := io.ReadAll(resp3.Body)\n\t\t_ = resp3.Body.Close()\n\n\t\tbody3Str := string(body3)\n\t\tt.Logf(\"Client C get session after exit: %s\", body3Str)\n\t\tassert.Contains(t, body3Str, \"SESSION_READ\")\n\t\tassert.Contains(t, body3Str, \"secret=NOT_FOUND\",\n\t\t\t\"Client C should not find any secret after exit(1).\\nResponse: %s\", body3Str)\n\n\t}, &testOptions{\n\t\tworkerScript:       \"session-leak.php\",\n\t\tnbWorkers:          1,\n\t\tnbParallelRequests: 1,\n\t\trealServer:         true,\n\t})\n}\n\nfunc TestOpcachePreload_module(t *testing.T) {\n\ttestOpcachePreload(t, &testOptions{env: map[string]string{\"TEST\": \"123\"}, realServer: true})\n}\n\nfunc TestOpcachePreload_worker(t *testing.T) {\n\ttestOpcachePreload(t, &testOptions{workerScript: \"preload-check.php\", env: map[string]string{\"TEST\": \"123\"}, realServer: true})\n}\n\nfunc testOpcachePreload(t *testing.T, opts *testOptions) {\n\tif frankenphp.Version().VersionID <= 80300 {\n\t\tt.Skip(\"This test is only supported in PHP 8.3 and above\")\n\t\treturn\n\t}\n\n\tcwd, _ := os.Getwd()\n\tpreloadScript := cwd + \"/testdata/preload.php\"\n\n\tu, err := user.Current()\n\trequire.NoError(t, err)\n\n\t// use opcache.log_verbosity_level:4 for debugging\n\topts.phpIni = map[string]string{\n\t\t\"opcache.enable\":       \"1\",\n\t\t\"opcache.preload\":      preloadScript,\n\t\t\"opcache.preload_user\": u.Username,\n\t}\n\n\trunTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {\n\t\tbody, _ := testGet(\"http://example.com/preload-check.php\", handler, t)\n\t\tassert.Equal(t, \"I am preloaded\", body)\n\t}, opts)\n}\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/dunglas/frankenphp\n\ngo 1.26.0\n\nretract v1.0.0-rc.1 // Human error\n\nrequire (\n\tgithub.com/Masterminds/sprig/v3 v3.3.0\n\tgithub.com/dunglas/mercure v0.21.11\n\tgithub.com/e-dant/watcher v0.0.0-20260223030516-06f84a1314be\n\tgithub.com/maypok86/otter/v2 v2.3.0\n\tgithub.com/prometheus/client_golang v1.23.2\n\tgithub.com/stretchr/testify v1.11.1\n\tgolang.org/x/net v0.51.0\n\tgolang.org/x/text v0.34.0\n)\n\nrequire (\n\tdario.cat/mergo v1.0.2 // indirect\n\tgithub.com/Masterminds/goutils v1.1.1 // indirect\n\tgithub.com/Masterminds/semver/v3 v3.4.0 // indirect\n\tgithub.com/MauriceGit/skiplist v0.0.0-20211105230623-77f5c8d3e145 // indirect\n\tgithub.com/RoaringBitmap/roaring/v2 v2.15.0 // indirect\n\tgithub.com/beorn7/perks v1.0.1 // indirect\n\tgithub.com/bits-and-blooms/bitset v1.24.4 // indirect\n\tgithub.com/cespare/xxhash/v2 v2.3.0 // indirect\n\tgithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect\n\tgithub.com/dunglas/skipfilter v1.0.0 // indirect\n\tgithub.com/felixge/httpsnoop v1.0.4 // indirect\n\tgithub.com/fsnotify/fsnotify v1.9.0 // indirect\n\tgithub.com/go-viper/mapstructure/v2 v2.5.0 // indirect\n\tgithub.com/gofrs/uuid/v5 v5.4.0 // indirect\n\tgithub.com/golang-jwt/jwt/v5 v5.3.1 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/gorilla/handlers v1.5.2 // indirect\n\tgithub.com/gorilla/mux v1.8.1 // indirect\n\tgithub.com/huandu/xstrings v1.5.0 // indirect\n\tgithub.com/klauspost/compress v1.18.4 // indirect\n\tgithub.com/kylelemons/godebug v1.1.0 // indirect\n\tgithub.com/mitchellh/copystructure v1.2.0 // indirect\n\tgithub.com/mitchellh/reflectwalk v1.0.2 // indirect\n\tgithub.com/mschoch/smat v0.2.0 // indirect\n\tgithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect\n\tgithub.com/pelletier/go-toml/v2 v2.2.4 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect\n\tgithub.com/prometheus/client_model v0.6.2 // indirect\n\tgithub.com/prometheus/common v0.67.5 // indirect\n\tgithub.com/prometheus/procfs v0.20.1 // indirect\n\tgithub.com/rogpeppe/go-internal v1.13.1 // indirect\n\tgithub.com/rs/cors v1.11.1 // indirect\n\tgithub.com/sagikazarmark/locafero v0.12.0 // indirect\n\tgithub.com/shopspring/decimal v1.4.0 // indirect\n\tgithub.com/spf13/afero v1.15.0 // indirect\n\tgithub.com/spf13/cast v1.10.0 // indirect\n\tgithub.com/spf13/pflag v1.0.10 // indirect\n\tgithub.com/spf13/viper v1.21.0 // indirect\n\tgithub.com/subosito/gotenv v1.6.0 // indirect\n\tgithub.com/unrolled/secure v1.17.0 // indirect\n\tgithub.com/yosida95/uritemplate/v3 v3.0.2 // indirect\n\tgo.etcd.io/bbolt v1.4.3 // indirect\n\tgo.yaml.in/yaml/v2 v2.4.4 // indirect\n\tgo.yaml.in/yaml/v3 v3.0.4 // indirect\n\tgolang.org/x/crypto v0.48.0 // indirect\n\tgolang.org/x/sys v0.42.0 // indirect\n\tgoogle.golang.org/protobuf v1.36.11 // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n)\n"
  },
  {
    "path": "go.sh",
    "content": "#!/bin/sh\n# Runs the go command with the proper Go and cgo flags.\n\nGOFLAGS=\"$GOFLAGS -tags=nobadger,nomysql,nopgx\" \\\n\tCGO_CFLAGS=\"$CGO_CFLAGS $(php-config --includes)\" \\\n\tCGO_LDFLAGS=\"$CGO_LDFLAGS $(php-config --ldflags) $(php-config --libs)\" \\\n\tgo \"$@\"\n"
  },
  {
    "path": "go.sum",
    "content": "dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=\ndario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=\ngithub.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=\ngithub.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=\ngithub.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=\ngithub.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=\ngithub.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=\ngithub.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=\ngithub.com/MauriceGit/skiplist v0.0.0-20211105230623-77f5c8d3e145 h1:1yw6O62BReQ+uA1oyk9XaQTvLhcoHWmoQAgXmDFXpIY=\ngithub.com/MauriceGit/skiplist v0.0.0-20211105230623-77f5c8d3e145/go.mod h1:877WBceefKn14QwVVn4xRFUsHsZb9clICgdeTj4XsUg=\ngithub.com/RoaringBitmap/roaring/v2 v2.15.0 h1:gCbixa3UiG7g6WUZNVOfEEg2HTc1vR4OVdMkX8t1ZFc=\ngithub.com/RoaringBitmap/roaring/v2 v2.15.0/go.mod h1:eq4wdNXxtJIS/oikeCzdX1rBzek7ANzbth041hrU8Q4=\ngithub.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=\ngithub.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=\ngithub.com/bits-and-blooms/bitset v1.24.4 h1:95H15Og1clikBrKr/DuzMXkQzECs1M6hhoGXLwLQOZE=\ngithub.com/bits-and-blooms/bitset v1.24.4/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=\ngithub.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=\ngithub.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/dunglas/mercure v0.21.11 h1:4Sd/Q77j8uh9SI5D9ZMg5sePlWs336+9CKxDQC1FV34=\ngithub.com/dunglas/mercure v0.21.11/go.mod h1:WPMgfqonUiO1qB+W8Tya63Ngag9ZwplGMXSOy8P/uMg=\ngithub.com/dunglas/skipfilter v1.0.0 h1:JG9SgGg4n6BlFwuTYzb9RIqjH7PfwszvWehanrYWPF4=\ngithub.com/dunglas/skipfilter v1.0.0/go.mod h1:ryhr8j7CAHSjzeN7wI6YEuwoArQ3OQmRqWWVCEAfb9w=\ngithub.com/e-dant/watcher v0.0.0-20260223030516-06f84a1314be h1:vqHrvilasyJcnru/0Z4FoojsQJUIfXGVplte7JtupfY=\ngithub.com/e-dant/watcher v0.0.0-20260223030516-06f84a1314be/go.mod h1:PmV4IVmBJVqT2NcfTGN4+sZ+qGe3PA0qkphAtOHeFG0=\ngithub.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=\ngithub.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=\ngithub.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=\ngithub.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=\ngithub.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=\ngithub.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=\ngithub.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=\ngithub.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=\ngithub.com/gofrs/uuid/v5 v5.4.0 h1:EfbpCTjqMuGyq5ZJwxqzn3Cbr2d0rUZU7v5ycAk/e/0=\ngithub.com/gofrs/uuid/v5 v5.4.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=\ngithub.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=\ngithub.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=\ngithub.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w=\ngithub.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=\ngithub.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=\ngithub.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=\ngithub.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=\ngithub.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=\ngithub.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=\ngithub.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=\ngithub.com/maypok86/otter/v2 v2.3.0 h1:8H8AVVFUSzJwIegKwv1uF5aGitTY+AIrtktg7OcLs8w=\ngithub.com/maypok86/otter/v2 v2.3.0/go.mod h1:XgIdlpmL6jYz882/CAx1E4C1ukfgDKSaw4mWq59+7l8=\ngithub.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=\ngithub.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=\ngithub.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=\ngithub.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=\ngithub.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM=\ngithub.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw=\ngithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=\ngithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=\ngithub.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=\ngithub.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=\ngithub.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=\ngithub.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=\ngithub.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=\ngithub.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=\ngithub.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=\ngithub.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=\ngithub.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=\ngithub.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=\ngithub.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=\ngithub.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=\ngithub.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=\ngithub.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4=\ngithub.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=\ngithub.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=\ngithub.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=\ngithub.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=\ngithub.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=\ngithub.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=\ngithub.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=\ngithub.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=\ngithub.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=\ngithub.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngithub.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=\ngithub.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=\ngithub.com/unrolled/secure v1.17.0 h1:Io7ifFgo99Bnh0J7+Q+qcMzWM6kaDPCA5FroFZEdbWU=\ngithub.com/unrolled/secure v1.17.0/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40=\ngithub.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=\ngithub.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=\ngo.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=\ngo.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=\ngo.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=\ngo.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=\ngo.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=\ngo.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=\ngo.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=\ngo.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=\ngolang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=\ngolang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=\ngolang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=\ngolang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=\ngolang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=\ngolang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=\ngolang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=\ngolang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=\ngolang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=\ngolang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=\ngoogle.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=\ngoogle.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "hotreload.go",
    "content": "//go:build !nomercure && !nowatcher\n\npackage frankenphp\n\nimport (\n\t\"encoding/json\"\n\t\"log/slog\"\n\n\t\"github.com/dunglas/frankenphp/internal/watcher\"\n\t\"github.com/dunglas/mercure\"\n\twatcherGo \"github.com/e-dant/watcher/watcher-go\"\n)\n\n// WithHotReload sets files to watch for file changes to trigger a hot reload update.\nfunc WithHotReload(topic string, hub *mercure.Hub, patterns []string) Option {\n\treturn func(o *opt) error {\n\t\to.hotReload = append(o.hotReload, &watcher.PatternGroup{\n\t\t\tPatterns: patterns,\n\t\t\tCallback: func(events []*watcherGo.Event) {\n\t\t\t\t// Wait for workers to restart before sending the update\n\t\t\t\tgo func() {\n\t\t\t\t\tdata, err := json.Marshal(events)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tif globalLogger.Enabled(globalCtx, slog.LevelError) {\n\t\t\t\t\t\t\tglobalLogger.LogAttrs(globalCtx, slog.LevelError, \"error marshaling watcher events\", slog.Any(\"error\", err))\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\n\t\t\t\t\tif err := hub.Publish(globalCtx, &mercure.Update{\n\t\t\t\t\t\tTopics: []string{topic},\n\t\t\t\t\t\tEvent:  mercure.Event{Data: string(data)},\n\t\t\t\t\t\tDebug:  globalLogger.Enabled(globalCtx, slog.LevelDebug),\n\t\t\t\t\t}); err != nil && globalLogger.Enabled(globalCtx, slog.LevelError) {\n\t\t\t\t\t\tglobalLogger.LogAttrs(globalCtx, slog.LevelError, \"error publishing hot reloading Mercure update\", slog.Any(\"error\", err))\n\t\t\t\t\t}\n\t\t\t\t}()\n\t\t\t},\n\t\t})\n\n\t\treturn nil\n\t}\n}\n"
  },
  {
    "path": "install.ps1",
    "content": "#Requires -Version 5.1\n<#\n.SYNOPSIS\n    Downloads and installs the latest FrankenPHP release for Windows.\n.DESCRIPTION\n    This script downloads the latest FrankenPHP Windows release from GitHub\n    and extracts it to the specified directory (~\\.frankenphp by default).\n\n    Usage as a one-liner:\n        irm https://github.com/php/frankenphp/raw/refs/heads/main/install.ps1 | iex\n    Custom install directory:\n        $env:FRANKENPHP_INSTALL = 'C:\\frankenphp'; irm https://github.com/php/frankenphp/raw/refs/heads/main/install.ps1 | iex\n#>\n\n$ErrorActionPreference = \"Stop\"\n\nif ($env:FRANKENPHP_INSTALL) {\n    $BinDir = $env:FRANKENPHP_INSTALL\n} else {\n    $BinDir = Join-Path $HOME \".frankenphp\"\n}\n\nWrite-Host \"Downloading FrankenPHP for Windows (x64)...\" -ForegroundColor Cyan\n\n$tmpZip = Join-Path $env:TEMP \"frankenphp-windows-$PID.zip\"\n\ntry {\n    Invoke-WebRequest -Uri \"https://github.com/php/frankenphp/releases/latest/download/frankenphp-windows-x86_64.zip\" -OutFile $tmpZip\n} catch {\n    Write-Host \"Download failed: $_\" -ForegroundColor Red\n    exit 1\n}\n\nWrite-Host \"Extracting to $BinDir...\" -ForegroundColor Cyan\n\nif (-not (Test-Path $BinDir)) {\n    New-Item -ItemType Directory -Path $BinDir -Force | Out-Null\n}\n\ntry {\n    Expand-Archive -Force -Path $tmpZip -DestinationPath $BinDir\n} finally {\n    Remove-Item $tmpZip -Force -ErrorAction SilentlyContinue\n}\n\nWrite-Host \"\"\nWrite-Host \"FrankenPHP downloaded successfully to $BinDir\" -ForegroundColor Green\n\n# Check if the directory is in PATH\n$inPath = $env:PATH -split \";\" | Where-Object { $_ -eq $BinDir -or $_ -eq \"$BinDir\\\" }\nif (-not $inPath) {\n    Write-Host \"Add $BinDir to your PATH to use frankenphp.exe globally:\" -ForegroundColor Yellow\n    Write-Host \"  [Environment]::SetEnvironmentVariable('PATH', `\"$BinDir;`\" + [Environment]::GetEnvironmentVariable('PATH', 'User'), 'User')\" -ForegroundColor Gray\n}\n\nWrite-Host \"\"\nWrite-Host \"If you like FrankenPHP, please give it a star on GitHub: https://github.com/php/frankenphp\" -ForegroundColor Cyan\n"
  },
  {
    "path": "install.sh",
    "content": "#!/bin/sh\n\nset -e\n\nSUDO=\"\"\n\nif [ -z \"${BIN_DIR}\" ]; then\n\tBIN_DIR=$(pwd)\nfi\n\nTHE_ARCH_BIN=\"\"\nDEST=${BIN_DIR}/frankenphp\n\nOS=$(uname -s)\nARCH=$(uname -m)\nGNU=\"\"\n\nif ! command -v curl >/dev/null 2>&1; then\n\techo \"❗ Please install curl to download FrankenPHP\"\n\texit 1\nfi\n\nif type \"tput\" >/dev/null 2>&1; then\n\tbold=$(tput bold || true)\n\titalic=$(tput sitm || true)\n\tnormal=$(tput sgr0 || true)\nfi\n\ncase ${OS} in\nLinux*)\n\tif [ \"${ARCH}\" = \"aarch64\" ] || [ \"${ARCH}\" = \"x86_64\" ]; then\n\t\tif command -v dnf >/dev/null 2>&1; then\n\t\t\techo \"📦 Detected dnf. Installing FrankenPHP from RPM repository...\"\n\t\t\tif [ \"$(id -u)\" -ne 0 ]; then\n\t\t\t\tSUDO=\"sudo\"\n\t\t\t\techo \"❗ Enter your password to grant sudo powers for package installation\"\n\t\t\t\t${SUDO} -v || true\n\t\t\tfi\n\t\t\t${SUDO} dnf -y install https://rpm.henderkes.com/static-php-1-1.noarch.rpm\n\t\t\t${SUDO} dnf -y module enable php-zts:static-8.5 || true\n\t\t\t${SUDO} dnf -y install frankenphp\n\t\t\techo\n\t\t\techo \"🥳 FrankenPHP installed to ${italic}/usr/bin/frankenphp${normal} successfully.\"\n\t\t\techo \"❗ The systemd service uses the Caddyfile in ${italic}/etc/frankenphp/Caddyfile${normal}\"\n\t\t\techo \"❗ Your php.ini is found in ${italic}/etc/php-zts/php.ini${normal}\"\n\t\t\techo\n\t\t\techo \"⭐ If you like FrankenPHP, please give it a star on GitHub: ${italic}https://github.com/php/frankenphp${normal}\"\n\t\t\texit 0\n\t\tfi\n\n\t\tif command -v apt-get >/dev/null 2>&1; then\n\t\t\techo \"📦 Detected apt-get. Installing FrankenPHP from DEB repository...\"\n\t\t\tif [ \"$(id -u)\" -ne 0 ]; then\n\t\t\t\tSUDO=\"sudo\"\n\t\t\t\techo \"❗ Enter your password to grant sudo powers for package installation\"\n\t\t\t\t${SUDO} -v || true\n\t\t\tfi\n\t\t\t${SUDO} sh -c 'curl -fsSL https://pkg.henderkes.com/api/packages/85/debian/repository.key -o /etc/apt/keyrings/static-php85.asc'\n\t\t\t${SUDO} sh -c 'echo \"deb [signed-by=/etc/apt/keyrings/static-php85.asc] https://pkg.henderkes.com/api/packages/85/debian php-zts main\" | sudo tee -a /etc/apt/sources.list.d/static-php85.list'\n\t\t\t${SUDO} apt-get update\n\t\t\t${SUDO} apt-get -y install frankenphp\n\t\t\techo\n\t\t\techo \"🥳 FrankenPHP installed to ${italic}/usr/bin/frankenphp${normal} successfully.\"\n\t\t\techo \"❗ The systemd service uses the Caddyfile in ${italic}/etc/frankenphp/Caddyfile${normal}\"\n\t\t\techo \"❗ Your php.ini is found in ${italic}/etc/php-zts/php.ini${normal}\"\n\t\t\techo\n\t\t\techo \"⭐ If you like FrankenPHP, please give it a star on GitHub: ${italic}https://github.com/php/frankenphp${normal}\"\n\t\t\texit 0\n\t\tfi\n\n\t\tif command -v apk >/dev/null 2>&1; then\n\t\t\techo \"📦 Detected apk. Installing FrankenPHP from APK repository...\"\n\t\t\tif [ \"$(id -u)\" -ne 0 ]; then\n\t\t\t\tSUDO=\"sudo\"\n\t\t\t\techo \"❗ Enter your password to grant sudo powers for package installation\"\n\t\t\t\t${SUDO} -v || true\n\t\t\tfi\n\n\t\t\tKEY_URL=\"https://pkg.henderkes.com/api/packages/85/alpine/key\"\n\t\t\t${SUDO} sh -c \"cd /etc/apk/keys && curl -JOsS \\\"$KEY_URL\\\" 2>/dev/null || true\"\n\n\t\t\tREPO_URL=\"https://pkg.henderkes.com/api/packages/85/alpine/main/php-zts\"\n\t\t\tif grep -q \"$REPO_URL\" /etc/apk/repositories 2>/dev/null; then\n\t\t\t\techo \"Repository already exists in /etc/apk/repositories\"\n\t\t\telse\n\t\t\t\t${SUDO} sh -c \"echo \\\"$REPO_URL\\\" >> /etc/apk/repositories\"\n\t\t\t\t${SUDO} apk update\n\t\t\t\techo \"Repository added to /etc/apk/repositories\"\n\t\t\tfi\n\n\t\t\t${SUDO} apk add frankenphp\n\t\t\techo\n\t\t\techo \"🥳 FrankenPHP installed to ${italic}/usr/bin/frankenphp${normal} successfully.\"\n\t\t\techo \"❗ The OpenRC service uses the Caddyfile in ${italic}/etc/frankenphp/Caddyfile${normal}\"\n\t\t\techo \"❗ Your php.ini is found in ${italic}/etc/php-zts/php.ini${normal}\"\n\t\t\techo\n\t\t\techo \"⭐ If you like FrankenPHP, please give it a star on GitHub: ${italic}https://github.com/php/frankenphp${normal}\"\n\t\t\texit 0\n\t\tfi\n\tfi\n\n\tcase ${ARCH} in\n\taarch64)\n\t\tTHE_ARCH_BIN=\"frankenphp-linux-aarch64\"\n\t\t;;\n\tx86_64)\n\t\tTHE_ARCH_BIN=\"frankenphp-linux-x86_64\"\n\t\t;;\n\t*)\n\t\tTHE_ARCH_BIN=\"\"\n\t\t;;\n\tesac\n\n\tif getconf GNU_LIBC_VERSION >/dev/null 2>&1; then\n\t\tTHE_ARCH_BIN=\"${THE_ARCH_BIN}-gnu\"\n\t\tGNU=\" (glibc)\"\n\tfi\n\t;;\nDarwin*)\n\tcase ${ARCH} in\n\tarm64)\n\t\tTHE_ARCH_BIN=\"frankenphp-mac-arm64\"\n\t\t;;\n\t*)\n\t\tTHE_ARCH_BIN=\"frankenphp-mac-x86_64\"\n\t\t;;\n\tesac\n\t;;\nCYGWIN_NT* | MSYS_NT* | MINGW*)\n\tif ! command -v unzip >/dev/null 2>&1 && ! command -v powershell.exe >/dev/null 2>&1; then\n\t\techo \"❗ Please install unzip or ensure PowerShell is available to extract FrankenPHP\"\n\t\texit 1\n\tfi\n\n\techo \"📦 Downloading ${bold}FrankenPHP${normal} for Windows (x64):\"\n\n\tTMPZIP=\"/tmp/frankenphp-windows-$$.zip\"\n\tif ! curl -f -L --progress-bar \"https://github.com/php/frankenphp/releases/latest/download/frankenphp-windows-x86_64.zip\" -o \"${TMPZIP}\"; then\n\t\techo \"❗ Failed to download FrankenPHP for Windows. Please check your internet connection or download it manually from:\"\n\t\techo \"   https://github.com/php/frankenphp/releases/latest\"\n\t\texit 1\n\tfi\n\n\techo \"📂 Extracting to ${italic}${BIN_DIR}${normal}...\"\n\tif command -v unzip >/dev/null 2>&1; then\n\t\tunzip -o -q \"${TMPZIP}\" -d \"${BIN_DIR}\"\n\telse\n\t\tpowershell.exe -Command \"Expand-Archive -Force -Path '$(cygpath -w \"${TMPZIP}\")' -DestinationPath '$(cygpath -w \"${BIN_DIR}\")'\"\n\tfi\n\trm -f \"${TMPZIP}\"\n\n\techo\n\techo \"🥳 FrankenPHP downloaded successfully to ${italic}${BIN_DIR}${normal}\"\n\techo \"🔧 Add ${italic}$(cygpath -w \"${BIN_DIR}\")${normal} to your Windows PATH to use ${italic}frankenphp.exe${normal} globally.\"\n\techo\n\techo \"⭐ If you like FrankenPHP, please give it a star on GitHub: ${italic}https://github.com/php/frankenphp${normal}\"\n\texit 0\n\t;;\n*)\n\tTHE_ARCH_BIN=\"\"\n\t;;\nesac\n\nif [ -z \"${THE_ARCH_BIN}\" ]; then\n\techo \"❗ Precompiled binaries are not available for ${ARCH}-${OS}\"\n\techo \"❗ You can compile from sources by following the documentation at: https://frankenphp.dev/docs/compile/\"\n\texit 1\nfi\n\necho \"📦 Downloading ${bold}FrankenPHP${normal} for ${OS}${GNU} (${ARCH}):\"\n\n# check if $DEST is writable and suppress an error message\ntouch \"${DEST}\" 2>/dev/null\n\n# we need sudo powers to write to DEST\nif [ $? -eq 1 ]; then\n\techo \"❗ You do not have permission to write to ${italic}${DEST}${normal}, enter your password to grant sudo powers\"\n\tSUDO=\"sudo\"\nfi\n\ncurl -L --progress-bar \"https://github.com/php/frankenphp/releases/latest/download/${THE_ARCH_BIN}\" -o \"${DEST}\"\n\n${SUDO} chmod +x \"${DEST}\"\n# Allow binding to ports 80/443 without running as root (if setcap is available)\nif command -v setcap >/dev/null 2>&1; then\n\t${SUDO} setcap 'cap_net_bind_service=+ep' \"${DEST}\" || true\nelse\n\techo \"❗ install setcap (e.g. libcap2-bin) to allow FrankenPHP to bind to ports 80/443 without root:\"\n\techo \"\t ${bold}sudo setcap 'cap_net_bind_service=+ep' \\\"${DEST}\\\"${normal}\"\nfi\n\necho\necho \"🥳 FrankenPHP downloaded successfully to ${italic}${DEST}${normal}\"\necho \"❗ It uses ${italic}/etc/frankenphp/php.ini${normal} if found.\"\ncase \":$PATH:\" in\n*\":$DEST:\"*) ;;\n*)\n\techo \"🔧 Move the binary to ${italic}/usr/local/bin/${normal} or another directory in your ${italic}PATH${normal} to use it globally:\"\n\techo \"\t${bold}sudo mv ${DEST} /usr/local/bin/${normal}\"\n\t;;\nesac\n\necho\necho \"⭐ If you like FrankenPHP, please give it a star on GitHub: ${italic}https://github.com/php/frankenphp${normal}\"\n"
  },
  {
    "path": "internal/cpu/cpu_unix.go",
    "content": "//go:build unix\n\npackage cpu\n\n// #include <time.h>\nimport \"C\"\nimport (\n\t\"runtime\"\n\t\"time\"\n)\n\nvar cpuCount = runtime.GOMAXPROCS(0)\n\n// ProbeCPUs probes the CPU usage of the process\n// if CPUs are not busy, most threads are likely waiting for I/O, so we should scale\n// if CPUs are already busy we won't gain much by scaling and want to avoid the overhead of doing so\nfunc ProbeCPUs(probeTime time.Duration, maxCPUUsage float64, abort chan struct{}) bool {\n\tvar cpuStart, cpuEnd C.struct_timespec\n\n\t// note: clock_gettime is a POSIX function\n\t// on Windows we'd need to use QueryPerformanceCounter instead\n\tstart := time.Now()\n\tC.clock_gettime(C.CLOCK_PROCESS_CPUTIME_ID, &cpuStart)\n\n\tselect {\n\tcase <-abort:\n\t\treturn false\n\tcase <-time.After(probeTime):\n\t}\n\n\tC.clock_gettime(C.CLOCK_PROCESS_CPUTIME_ID, &cpuEnd)\n\telapsedTime := float64(time.Since(start).Nanoseconds())\n\telapsedCpuTime := float64(cpuEnd.tv_sec-cpuStart.tv_sec)*1e9 + float64(cpuEnd.tv_nsec-cpuStart.tv_nsec)\n\tcpuUsage := elapsedCpuTime / elapsedTime / float64(cpuCount)\n\n\treturn cpuUsage < maxCPUUsage\n}\n"
  },
  {
    "path": "internal/cpu/cpu_windows.go",
    "content": "package cpu\n\nimport (\n\t\"time\"\n)\n\n// ProbeCPUs fallback that always determines that the CPU limits are not reached\nfunc ProbeCPUs(probeTime time.Duration, _ float64, abort chan struct{}) bool {\n\tselect {\n\tcase <-abort:\n\t\treturn false\n\tcase <-time.After(probeTime):\n\t\treturn true\n\t}\n}\n"
  },
  {
    "path": "internal/extgen/arginfo.go",
    "content": "package extgen\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"strings\"\n)\n\ntype arginfoGenerator struct {\n\tgenerator *Generator\n}\n\nfunc (ag *arginfoGenerator) generate() error {\n\tgenStubPath := os.Getenv(\"GEN_STUB_SCRIPT\")\n\tif genStubPath == \"\" {\n\t\tgenStubPath = \"/usr/local/src/php/build/gen_stub.php\"\n\t}\n\n\tif _, err := os.Stat(genStubPath); err != nil {\n\t\treturn fmt.Errorf(`the PHP \"gen_stub.php\" file couldn't be found under %q, you can set the \"GEN_STUB_SCRIPT\" environment variable to set a custom location`, genStubPath)\n\t}\n\n\tstubFile := ag.generator.BaseName + \".stub.php\"\n\tcmd := exec.Command(\"php\", genStubPath, filepath.Join(ag.generator.BuildDir, stubFile))\n\n\toutput, err := cmd.CombinedOutput()\n\tif err != nil {\n\t\tlog.Print(\"gen_stub.php output:\\n\", string(output))\n\t\treturn fmt.Errorf(\"running gen_stub script: %w\\nOutput: %s\", err, string(output))\n\t}\n\n\treturn ag.fixArginfoFile(stubFile)\n}\n\nfunc (ag *arginfoGenerator) fixArginfoFile(stubFile string) error {\n\targinfoFile := strings.TrimSuffix(stubFile, \".stub.php\") + \"_arginfo.h\"\n\targinfoPath := filepath.Join(ag.generator.BuildDir, arginfoFile)\n\n\tcontent, err := readFile(arginfoPath)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"reading arginfo file: %w\", err)\n\t}\n\n\t// FIXME: the script generate \"zend_register_internal_class_with_flags\" but it is not recognized by the compiler\n\tfixedContent := strings.ReplaceAll(content,\n\t\t\"zend_register_internal_class_with_flags(&ce, NULL, 0)\",\n\t\t\"zend_register_internal_class(&ce)\")\n\n\treturn writeFile(arginfoPath, fixedContent)\n}\n"
  },
  {
    "path": "internal/extgen/cfile.go",
    "content": "package extgen\n\nimport (\n\t\"github.com/Masterminds/sprig/v3\"\n\n\t\"bytes\"\n\t_ \"embed\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"text/template\"\n)\n\n//go:embed templates/extension.c.tpl\nvar cFileContent string\n\ntype cFileGenerator struct {\n\tgenerator *Generator\n}\n\ntype cTemplateData struct {\n\tBaseName  string\n\tFunctions []phpFunction\n\tClasses   []phpClass\n\tConstants []phpConstant\n\tNamespace string\n}\n\nfunc (cg *cFileGenerator) generate() error {\n\tfilename := filepath.Join(cg.generator.BuildDir, cg.generator.BaseName+\".c\")\n\tcontent, err := cg.buildContent()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn writeFile(filename, content)\n}\n\nfunc (cg *cFileGenerator) buildContent() (string, error) {\n\tvar builder strings.Builder\n\n\ttemplateContent, err := cg.getTemplateContent()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tbuilder.WriteString(templateContent)\n\n\tfor _, fn := range cg.generator.Functions {\n\t\tfnGen := PHPFuncGenerator{\n\t\t\tparamParser: &ParameterParser{},\n\t\t\tnamespace:   cg.generator.Namespace,\n\t\t}\n\t\tbuilder.WriteString(fnGen.generate(fn))\n\t}\n\n\treturn builder.String(), nil\n}\n\nfunc (cg *cFileGenerator) getTemplateContent() (string, error) {\n\tfuncMap := sprig.FuncMap()\n\tfuncMap[\"namespacedClassName\"] = NamespacedName\n\tfuncMap[\"cString\"] = escapeCString\n\n\ttmpl := template.Must(template.New(\"cfile\").Funcs(funcMap).Parse(cFileContent))\n\n\tvar buf bytes.Buffer\n\tif err := tmpl.Execute(&buf, cTemplateData{\n\t\tBaseName:  cg.generator.BaseName,\n\t\tFunctions: cg.generator.Functions,\n\t\tClasses:   cg.generator.Classes,\n\t\tConstants: cg.generator.Constants,\n\t\tNamespace: cg.generator.Namespace,\n\t}); err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn buf.String(), nil\n}\n\n// escapeCString escapes backslashes for C string literals\nfunc escapeCString(s string) string {\n\treturn strings.ReplaceAll(s, `\\`, `\\\\`)\n}\n"
  },
  {
    "path": "internal/extgen/cfile_namespace_test.go",
    "content": "package extgen\n\nimport (\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestNamespacedClassName(t *testing.T) {\n\ttests := []struct {\n\t\tname      string\n\t\tnamespace string\n\t\tclassName string\n\t\texpected  string\n\t}{\n\t\t{\n\t\t\tname:      \"no namespace\",\n\t\t\tnamespace: \"\",\n\t\t\tclassName: \"MySuperClass\",\n\t\t\texpected:  \"MySuperClass\",\n\t\t},\n\t\t{\n\t\t\tname:      \"single level namespace\",\n\t\t\tnamespace: \"MyNamespace\",\n\t\t\tclassName: \"MySuperClass\",\n\t\t\texpected:  \"MyNamespace_MySuperClass\",\n\t\t},\n\t\t{\n\t\t\tname:      \"multi level namespace\",\n\t\t\tnamespace: `Go\\Extension`,\n\t\t\tclassName: \"MySuperClass\",\n\t\t\texpected:  \"Go_Extension_MySuperClass\",\n\t\t},\n\t\t{\n\t\t\tname:      \"deep namespace\",\n\t\t\tnamespace: `My\\Deep\\Nested\\Namespace`,\n\t\t\tclassName: \"TestClass\",\n\t\t\texpected:  \"My_Deep_Nested_Namespace_TestClass\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := NamespacedName(tt.namespace, tt.className)\n\t\t\trequire.Equal(t, tt.expected, result, \"expected %q, got %q\", tt.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestCFileGenerationWithNamespace(t *testing.T) {\n\tcontent := `package main\n\n//export_php:namespace Go\\Extension\n\n//export_php:class MySuperClass\ntype MySuperClass struct{}\n\n//export_php:method MySuperClass test(): string\nfunc (m *MySuperClass) Test() string {\n\treturn \"test\"\n}\n`\n\n\ttmpfile, err := os.CreateTemp(\"\", \"test_cfile_namespace_*.go\")\n\trequire.NoError(t, err, \"Failed to create temp file\")\n\tdefer func() {\n\t\terr := os.Remove(tmpfile.Name())\n\t\tassert.NoError(t, err, \"Failed to remove temp file: %v\", err)\n\t}()\n\n\t_, err = tmpfile.Write([]byte(content))\n\trequire.NoError(t, err, \"Failed to write to temp file\")\n\n\terr = tmpfile.Close()\n\trequire.NoError(t, err, \"Failed to close temp file\")\n\n\tgenerator := &Generator{\n\t\tBaseName:   \"test_extension\",\n\t\tSourceFile: tmpfile.Name(),\n\t\tBuildDir:   t.TempDir(),\n\t\tNamespace:  `Go\\Extension`,\n\t\tClasses: []phpClass{\n\t\t\t{\n\t\t\t\tName:     \"MySuperClass\",\n\t\t\t\tGoStruct: \"MySuperClass\",\n\t\t\t\tMethods: []phpClassMethod{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:       \"test\",\n\t\t\t\t\t\tPhpName:    \"test\",\n\t\t\t\t\t\tSignature:  \"test(): string\",\n\t\t\t\t\t\tReturnType: \"string\",\n\t\t\t\t\t\tClassName:  \"MySuperClass\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tcFileGen := cFileGenerator{generator: generator}\n\tcontentResult, err := cFileGen.getTemplateContent()\n\trequire.NoError(t, err, \"error generating C file\")\n\n\texpectedCall := \"register_class_Go_Extension_MySuperClass()\"\n\trequire.Contains(t, contentResult, expectedCall, \"C file should contain the standard function call\")\n\n\toldCall := \"register_class_MySuperClass()\"\n\trequire.NotContains(t, contentResult, oldCall, \"C file should not contain old non-namespaced call\")\n}\n\nfunc TestCFileGenerationWithoutNamespace(t *testing.T) {\n\tgenerator := &Generator{\n\t\tBaseName:  \"test_extension\",\n\t\tBuildDir:  t.TempDir(),\n\t\tNamespace: \"\",\n\t\tClasses: []phpClass{\n\t\t\t{\n\t\t\t\tName:     \"MySuperClass\",\n\t\t\t\tGoStruct: \"MySuperClass\",\n\t\t\t},\n\t\t},\n\t}\n\n\tcFileGen := cFileGenerator{generator: generator}\n\tcontentResult, err := cFileGen.getTemplateContent()\n\trequire.NoError(t, err, \"error generating C file\")\n\n\texpectedCall := \"register_class_MySuperClass()\"\n\trequire.Contains(t, contentResult, expectedCall, \"C file should not contain the standard function call\")\n}\n\nfunc TestCFileGenerationWithNamespacedConstants(t *testing.T) {\n\ttests := []struct {\n\t\tname      string\n\t\tnamespace string\n\t\tconstants []phpConstant\n\t\tcontains  []string\n\t}{\n\t\t{\n\t\t\tname:      \"integer constant with namespace\",\n\t\t\tnamespace: `Go\\Extension`,\n\t\t\tconstants: []phpConstant{\n\t\t\t\t{Name: \"TEST_INT\", Value: \"42\", PhpType: phpInt},\n\t\t\t},\n\t\t\tcontains: []string{\n\t\t\t\t`REGISTER_NS_LONG_CONSTANT(\"Go\\\\Extension\", \"TEST_INT\", 42, CONST_CS | CONST_PERSISTENT);`,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:      \"string constant with namespace\",\n\t\t\tnamespace: `Go\\Extension`,\n\t\t\tconstants: []phpConstant{\n\t\t\t\t{Name: \"TEST_STRING\", Value: `\"hello\"`, PhpType: phpString},\n\t\t\t},\n\t\t\tcontains: []string{\n\t\t\t\t`REGISTER_NS_STRING_CONSTANT(\"Go\\\\Extension\", \"TEST_STRING\", \"hello\", CONST_CS | CONST_PERSISTENT);`,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:      \"float constant with namespace\",\n\t\t\tnamespace: `Go\\Extension`,\n\t\t\tconstants: []phpConstant{\n\t\t\t\t{Name: \"TEST_FLOAT\", Value: \"3.14\", PhpType: phpFloat},\n\t\t\t},\n\t\t\tcontains: []string{\n\t\t\t\t`REGISTER_NS_DOUBLE_CONSTANT(\"Go\\\\Extension\", \"TEST_FLOAT\", 3.14, CONST_CS | CONST_PERSISTENT);`,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:      \"bool constant with namespace\",\n\t\t\tnamespace: `Go\\Extension`,\n\t\t\tconstants: []phpConstant{\n\t\t\t\t{Name: \"TEST_BOOL\", Value: \"true\", PhpType: phpBool},\n\t\t\t},\n\t\t\tcontains: []string{\n\t\t\t\t`REGISTER_NS_BOOL_CONSTANT(\"Go\\\\Extension\", \"TEST_BOOL\", true, CONST_CS | CONST_PERSISTENT);`,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:      \"iota constant with namespace\",\n\t\t\tnamespace: `Go\\Extension`,\n\t\t\tconstants: []phpConstant{\n\t\t\t\t{Name: \"STATUS_OK\", Value: \"STATUS_OK\", PhpType: phpInt, IsIota: true},\n\t\t\t},\n\t\t\tcontains: []string{\n\t\t\t\t`REGISTER_NS_LONG_CONSTANT(\"Go\\\\Extension\", \"STATUS_OK\", STATUS_OK, CONST_CS | CONST_PERSISTENT);`,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:      \"multiple constants with deep namespace\",\n\t\t\tnamespace: `My\\Deep\\Namespace`,\n\t\t\tconstants: []phpConstant{\n\t\t\t\t{Name: \"CONST_INT\", Value: \"100\", PhpType: phpInt},\n\t\t\t\t{Name: \"CONST_STR\", Value: `\"value\"`, PhpType: phpString},\n\t\t\t\t{Name: \"CONST_FLOAT\", Value: \"1.5\", PhpType: phpFloat},\n\t\t\t},\n\t\t\tcontains: []string{\n\t\t\t\t`REGISTER_NS_LONG_CONSTANT(\"My\\\\Deep\\\\Namespace\", \"CONST_INT\", 100, CONST_CS | CONST_PERSISTENT);`,\n\t\t\t\t`REGISTER_NS_STRING_CONSTANT(\"My\\\\Deep\\\\Namespace\", \"CONST_STR\", \"value\", CONST_CS | CONST_PERSISTENT);`,\n\t\t\t\t`REGISTER_NS_DOUBLE_CONSTANT(\"My\\\\Deep\\\\Namespace\", \"CONST_FLOAT\", 1.5, CONST_CS | CONST_PERSISTENT);`,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:      \"single level namespace\",\n\t\t\tnamespace: `TestNamespace`,\n\t\t\tconstants: []phpConstant{\n\t\t\t\t{Name: \"MY_CONST\", Value: \"999\", PhpType: phpInt},\n\t\t\t},\n\t\t\tcontains: []string{\n\t\t\t\t`REGISTER_NS_LONG_CONSTANT(\"TestNamespace\", \"MY_CONST\", 999, CONST_CS | CONST_PERSISTENT);`,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:      \"namespace with trailing backslash\",\n\t\t\tnamespace: `TestIntegration\\Extension`,\n\t\t\tconstants: []phpConstant{\n\t\t\t\t{Name: \"VERSION\", Value: `\"1.0.0\"`, PhpType: phpString},\n\t\t\t},\n\t\t\tcontains: []string{\n\t\t\t\t`REGISTER_NS_STRING_CONSTANT(\"TestIntegration\\\\Extension\", \"VERSION\", \"1.0.0\", CONST_CS | CONST_PERSISTENT);`,\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgenerator := &Generator{\n\t\t\t\tBaseName:  \"test_ext\",\n\t\t\t\tNamespace: tt.namespace,\n\t\t\t\tConstants: tt.constants,\n\t\t\t}\n\n\t\t\tcGen := cFileGenerator{generator: generator}\n\t\t\tcontent, err := cGen.buildContent()\n\t\t\trequire.NoError(t, err, \"Failed to build C file content\")\n\n\t\t\tfor _, expected := range tt.contains {\n\t\t\t\tassert.Contains(t, content, expected, \"Generated C content should contain '%s'\", expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestCFileGenerationWithoutNamespacedConstants(t *testing.T) {\n\ttests := []struct {\n\t\tname      string\n\t\tnamespace string\n\t\tconstants []phpConstant\n\t\tcontains  []string\n\t}{\n\t\t{\n\t\t\tname:      \"integer constant without namespace\",\n\t\t\tnamespace: \"\",\n\t\t\tconstants: []phpConstant{\n\t\t\t\t{Name: \"GLOBAL_INT\", Value: \"42\", PhpType: phpInt},\n\t\t\t},\n\t\t\tcontains: []string{\n\t\t\t\t`REGISTER_LONG_CONSTANT(\"GLOBAL_INT\", 42, CONST_CS | CONST_PERSISTENT);`,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:      \"string constant without namespace\",\n\t\t\tnamespace: \"\",\n\t\t\tconstants: []phpConstant{\n\t\t\t\t{Name: \"GLOBAL_STRING\", Value: `\"test\"`, PhpType: phpString},\n\t\t\t},\n\t\t\tcontains: []string{\n\t\t\t\t`REGISTER_STRING_CONSTANT(\"GLOBAL_STRING\", \"test\", CONST_CS | CONST_PERSISTENT);`,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:      \"float constant without namespace\",\n\t\t\tnamespace: \"\",\n\t\t\tconstants: []phpConstant{\n\t\t\t\t{Name: \"GLOBAL_FLOAT\", Value: \"2.71\", PhpType: phpFloat},\n\t\t\t},\n\t\t\tcontains: []string{\n\t\t\t\t`REGISTER_DOUBLE_CONSTANT(\"GLOBAL_FLOAT\", 2.71, CONST_CS | CONST_PERSISTENT);`,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:      \"bool constant without namespace\",\n\t\t\tnamespace: \"\",\n\t\t\tconstants: []phpConstant{\n\t\t\t\t{Name: \"GLOBAL_BOOL\", Value: \"false\", PhpType: phpBool},\n\t\t\t},\n\t\t\tcontains: []string{\n\t\t\t\t`REGISTER_BOOL_CONSTANT(\"GLOBAL_BOOL\", false, CONST_CS | CONST_PERSISTENT);`,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:      \"iota constant without namespace\",\n\t\t\tnamespace: \"\",\n\t\t\tconstants: []phpConstant{\n\t\t\t\t{Name: \"ERROR_CODE\", Value: \"ERROR_CODE\", PhpType: phpInt, IsIota: true},\n\t\t\t},\n\t\t\tcontains: []string{\n\t\t\t\t`REGISTER_LONG_CONSTANT(\"ERROR_CODE\", ERROR_CODE, CONST_CS | CONST_PERSISTENT);`,\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgenerator := &Generator{\n\t\t\t\tBaseName:  \"test_ext\",\n\t\t\t\tNamespace: tt.namespace,\n\t\t\t\tConstants: tt.constants,\n\t\t\t}\n\n\t\t\tcGen := cFileGenerator{generator: generator}\n\t\t\tcontent, err := cGen.buildContent()\n\t\t\trequire.NoError(t, err, \"Failed to build C file content\")\n\n\t\t\tfor _, expected := range tt.contains {\n\t\t\t\tassert.Contains(t, content, expected, \"Generated C content should contain '%s'\", expected)\n\t\t\t}\n\n\t\t\tassert.NotContains(t, content, \"REGISTER_NS_\", \"Content should NOT contain namespaced constant macros when namespace is empty\")\n\t\t})\n\t}\n}\n\nfunc TestCFileTemplateFunctionMapCString(t *testing.T) {\n\tgenerator := &Generator{\n\t\tBaseName:  \"test_ext\",\n\t\tNamespace: `My\\Namespace\\Test`,\n\t\tConstants: []phpConstant{\n\t\t\t{Name: \"MY_CONST\", Value: \"123\", PhpType: phpInt},\n\t\t},\n\t}\n\n\tcGen := cFileGenerator{generator: generator}\n\tcontent, err := cGen.getTemplateContent()\n\trequire.NoError(t, err, \"Failed to get template content\")\n\n\tassert.Contains(t, content, `\"My\\\\Namespace\\\\Test\"`, \"Template should properly escape namespace backslashes using cString filter\")\n\tassert.NotContains(t, content, `\"My\\Namespace\\Test\"`, \"Template should not contain unescaped namespace (single backslashes)\")\n}\n"
  },
  {
    "path": "internal/extgen/cfile_phpmethod_test.go",
    "content": "package extgen\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestCFile_NamespacedPHPMethods(t *testing.T) {\n\ttests := []struct {\n\t\tname      string\n\t\tnamespace string\n\t\tclasses   []phpClass\n\t\texpected  []string\n\t}{\n\t\t{\n\t\t\tname:      \"no namespace - regular PHP_METHOD\",\n\t\t\tnamespace: \"\",\n\t\t\tclasses: []phpClass{\n\t\t\t\t{\n\t\t\t\t\tName:     \"TestClass\",\n\t\t\t\t\tGoStruct: \"TestClass\",\n\t\t\t\t\tMethods: []phpClassMethod{\n\t\t\t\t\t\t{Name: \"testMethod\", PhpName: \"testMethod\", ClassName: \"TestClass\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: []string{\n\t\t\t\t\"PHP_METHOD(TestClass, __construct)\",\n\t\t\t\t\"PHP_METHOD(TestClass, testMethod)\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:      \"single level namespace\",\n\t\t\tnamespace: \"MyNamespace\",\n\t\t\tclasses: []phpClass{\n\t\t\t\t{\n\t\t\t\t\tName:     \"TestClass\",\n\t\t\t\t\tGoStruct: \"TestClass\",\n\t\t\t\t\tMethods: []phpClassMethod{\n\t\t\t\t\t\t{Name: \"testMethod\", PhpName: \"testMethod\", ClassName: \"TestClass\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: []string{\n\t\t\t\t\"PHP_METHOD(MyNamespace_TestClass, __construct)\",\n\t\t\t\t\"PHP_METHOD(MyNamespace_TestClass, testMethod)\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:      \"multi level namespace\",\n\t\t\tnamespace: `Go\\Extension`,\n\t\t\tclasses: []phpClass{\n\t\t\t\t{\n\t\t\t\t\tName:     \"MySuperClass\",\n\t\t\t\t\tGoStruct: \"MySuperClass\",\n\t\t\t\t\tMethods: []phpClassMethod{\n\t\t\t\t\t\t{Name: \"getName\", PhpName: \"getName\", ClassName: \"MySuperClass\"},\n\t\t\t\t\t\t{Name: \"setName\", PhpName: \"setName\", ClassName: \"MySuperClass\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: []string{\n\t\t\t\t\"PHP_METHOD(Go_Extension_MySuperClass, __construct)\",\n\t\t\t\t\"PHP_METHOD(Go_Extension_MySuperClass, getName)\",\n\t\t\t\t\"PHP_METHOD(Go_Extension_MySuperClass, setName)\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:      \"multiple classes with namespace\",\n\t\t\tnamespace: `Go\\Extension`,\n\t\t\tclasses: []phpClass{\n\t\t\t\t{\n\t\t\t\t\tName:     \"ClassA\",\n\t\t\t\t\tGoStruct: \"ClassA\",\n\t\t\t\t\tMethods: []phpClassMethod{\n\t\t\t\t\t\t{Name: \"methodA\", PhpName: \"methodA\", ClassName: \"ClassA\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tName:     \"ClassB\",\n\t\t\t\t\tGoStruct: \"ClassB\",\n\t\t\t\t\tMethods: []phpClassMethod{\n\t\t\t\t\t\t{Name: \"methodB\", PhpName: \"methodB\", ClassName: \"ClassB\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: []string{\n\t\t\t\t\"PHP_METHOD(Go_Extension_ClassA, __construct)\",\n\t\t\t\t\"PHP_METHOD(Go_Extension_ClassA, methodA)\",\n\t\t\t\t\"PHP_METHOD(Go_Extension_ClassB, __construct)\",\n\t\t\t\t\"PHP_METHOD(Go_Extension_ClassB, methodB)\",\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgenerator := &Generator{\n\t\t\t\tBaseName:  \"test_extension\",\n\t\t\t\tNamespace: tt.namespace,\n\t\t\t\tClasses:   tt.classes,\n\t\t\t\tBuildDir:  t.TempDir(),\n\t\t\t}\n\n\t\t\tcFileGen := cFileGenerator{generator: generator}\n\t\t\tcontent, err := cFileGen.getTemplateContent()\n\t\t\trequire.NoError(t, err, \"error generating C template content: %v\", err)\n\n\t\t\tfor _, expected := range tt.expected {\n\t\t\t\trequire.Contains(t, content, expected, \"Expected to find %q in C template content\", expected)\n\t\t\t}\n\n\t\t\tif tt.namespace != \"\" {\n\t\t\t\tfor _, class := range tt.classes {\n\t\t\t\t\toldConstructor := \"PHP_METHOD(\" + class.Name + \", __construct)\"\n\t\t\t\t\trequire.NotContains(t, content, oldConstructor, \"Did not expect to find old constructor declaration %q in namespaced content\", oldConstructor)\n\n\t\t\t\t\tfor _, method := range class.Methods {\n\t\t\t\t\t\toldMethod := \"PHP_METHOD(\" + class.Name + \", \" + method.PhpName + \")\"\n\t\t\t\t\t\trequire.NotContains(t, content, oldMethod, \"Did not expect to find old method declaration %q in namespaced content\", oldMethod)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestCFile_PHP_METHOD_Integration(t *testing.T) {\n\tgenerator := &Generator{\n\t\tBaseName:  \"test_extension\",\n\t\tNamespace: `Go\\Extension`,\n\t\tFunctions: []phpFunction{\n\t\t\t{Name: \"testFunc\", ReturnType: \"void\"},\n\t\t},\n\t\tClasses: []phpClass{\n\t\t\t{\n\t\t\t\tName:     \"MySuperClass\",\n\t\t\t\tGoStruct: \"MySuperClass\",\n\t\t\t\tMethods: []phpClassMethod{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:       \"getName\",\n\t\t\t\t\t\tPhpName:    \"getName\",\n\t\t\t\t\t\tReturnType: \"string\",\n\t\t\t\t\t\tClassName:  \"MySuperClass\",\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tName:       \"setName\",\n\t\t\t\t\t\tPhpName:    \"setName\",\n\t\t\t\t\t\tReturnType: \"void\",\n\t\t\t\t\t\tClassName:  \"MySuperClass\",\n\t\t\t\t\t\tParams: []phpParameter{\n\t\t\t\t\t\t\t{Name: \"name\", PhpType: \"string\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tBuildDir: t.TempDir(),\n\t}\n\n\tcFileGen := cFileGenerator{generator: generator}\n\tfullContent, err := cFileGen.buildContent()\n\trequire.NoError(t, err, \"error generating full C file: %v\", err)\n\n\texpectedDeclarations := []string{\n\t\t\"PHP_FUNCTION(Go_Extension_testFunc)\",\n\t\t\"PHP_METHOD(Go_Extension_MySuperClass, __construct)\",\n\t\t\"PHP_METHOD(Go_Extension_MySuperClass, getName)\",\n\t\t\"PHP_METHOD(Go_Extension_MySuperClass, setName)\",\n\t}\n\n\tfor _, expected := range expectedDeclarations {\n\t\trequire.Contains(t, fullContent, expected, \"Expected to find %q in full C file content\", expected)\n\t}\n\n\toldDeclarations := []string{\n\t\t\"PHP_FUNCTION(testFunc)\",\n\t\t\"PHP_METHOD(MySuperClass, __construct)\",\n\t\t\"PHP_METHOD(MySuperClass, getName)\",\n\t\t\"PHP_METHOD(MySuperClass, setName)\",\n\t}\n\n\tfor _, old := range oldDeclarations {\n\t\trequire.NotContains(t, fullContent, old, \"Did not expect to find old declaration %q in full C file content\", old)\n\t}\n}\n\nfunc TestCFile_ClassMethodStringReturn(t *testing.T) {\n\tgenerator := &Generator{\n\t\tBaseName: \"test_extension\",\n\t\tClasses: []phpClass{\n\t\t\t{\n\t\t\t\tName:     \"TestClass\",\n\t\t\t\tGoStruct: \"TestClass\",\n\t\t\t\tMethods: []phpClassMethod{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:       \"getString\",\n\t\t\t\t\t\tPhpName:    \"getString\",\n\t\t\t\t\t\tReturnType: \"string\",\n\t\t\t\t\t\tClassName:  \"TestClass\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tBuildDir: t.TempDir(),\n\t}\n\n\tcFileGen := cFileGenerator{generator: generator}\n\tcontent, err := cFileGen.getTemplateContent()\n\trequire.NoError(t, err)\n\n\trequire.Contains(t, content, \"if (result)\", \"Expected NULL check for string return\")\n\trequire.Contains(t, content, \"RETURN_STR(result)\", \"Expected RETURN_STR macro\")\n\trequire.Contains(t, content, \"RETURN_EMPTY_STRING()\", \"Expected RETURN_EMPTY_STRING fallback\")\n}\n"
  },
  {
    "path": "internal/extgen/cfile_test.go",
    "content": "package extgen\n\nimport (\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestCFileGenerator_Generate(t *testing.T) {\n\ttmpDir := t.TempDir()\n\n\tgenerator := &Generator{\n\t\tBaseName: \"test_extension\",\n\t\tBuildDir: tmpDir,\n\t\tFunctions: []phpFunction{\n\t\t\t{\n\t\t\t\tName:       \"simpleFunction\",\n\t\t\t\tReturnType: phpString,\n\t\t\t\tParams: []phpParameter{\n\t\t\t\t\t{Name: \"input\", PhpType: phpString},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tName:       \"complexFunction\",\n\t\t\t\tReturnType: phpArray,\n\t\t\t\tParams: []phpParameter{\n\t\t\t\t\t{Name: \"data\", PhpType: phpString},\n\t\t\t\t\t{Name: \"count\", PhpType: phpInt, IsNullable: true},\n\t\t\t\t\t{Name: \"options\", PhpType: phpArray, HasDefault: true, DefaultValue: \"[]\"},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tClasses: []phpClass{\n\t\t\t{\n\t\t\t\tName:     \"TestClass\",\n\t\t\t\tGoStruct: \"TestStruct\",\n\t\t\t\tProperties: []phpClassProperty{\n\t\t\t\t\t{Name: \"id\", PhpType: phpInt},\n\t\t\t\t\t{Name: \"name\", PhpType: phpString},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tcGen := cFileGenerator{generator}\n\trequire.NoError(t, cGen.generate())\n\n\texpectedFile := filepath.Join(tmpDir, \"test_extension.c\")\n\trequire.FileExists(t, expectedFile, \"Expected C file was not created: %s\", expectedFile)\n\n\tcontent, err := readFile(expectedFile)\n\trequire.NoError(t, err)\n\n\ttestCFileBasicStructure(t, content, \"test_extension\")\n\ttestCFileFunctions(t, content, generator.Functions)\n\ttestCFileClasses(t, content, generator.Classes)\n}\n\nfunc TestCFileGenerator_BuildContent(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\tbaseName    string\n\t\tfunctions   []phpFunction\n\t\tclasses     []phpClass\n\t\tcontains    []string\n\t\tnotContains []string\n\t}{\n\t\t{\n\t\t\tname:     \"empty extension\",\n\t\t\tbaseName: \"empty\",\n\t\t\tcontains: []string{\n\t\t\t\t\"#include <php.h>\",\n\t\t\t\t\"#include <Zend/zend_API.h>\",\n\t\t\t\t`#include \"empty.h\"`,\n\t\t\t\t\"PHP_MINIT_FUNCTION(empty)\",\n\t\t\t\t\"empty_module_entry\",\n\t\t\t\t\"return SUCCESS;\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"extension with functions only\",\n\t\t\tbaseName: \"func_only\",\n\t\t\tfunctions: []phpFunction{\n\t\t\t\t{Name: \"testFunc\", ReturnType: phpString},\n\t\t\t},\n\t\t\tcontains: []string{\n\t\t\t\t\"PHP_FUNCTION(testFunc)\",\n\t\t\t\t`#include \"func_only.h\"`,\n\t\t\t\t\"func_only_module_entry\",\n\t\t\t\t\"PHP_MINIT_FUNCTION(func_only)\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"extension with classes only\",\n\t\t\tbaseName: \"class_only\",\n\t\t\tclasses: []phpClass{\n\t\t\t\t{Name: \"MyClass\", GoStruct: \"MyStruct\"},\n\t\t\t},\n\t\t\tcontains: []string{\n\t\t\t\t\"register_all_classes()\",\n\t\t\t\t\"register_class_MyClass();\",\n\t\t\t\t\"PHP_METHOD(MyClass, __construct)\",\n\t\t\t\t`#include \"class_only.h\"`,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"extension with functions and classes\",\n\t\t\tbaseName: \"full\",\n\t\t\tfunctions: []phpFunction{\n\t\t\t\t{Name: \"doSomething\", ReturnType: phpVoid},\n\t\t\t},\n\t\t\tclasses: []phpClass{\n\t\t\t\t{Name: \"FullClass\", GoStruct: \"FullStruct\"},\n\t\t\t},\n\t\t\tcontains: []string{\n\t\t\t\t\"PHP_FUNCTION(doSomething)\",\n\t\t\t\t\"PHP_METHOD(FullClass, __construct)\",\n\t\t\t\t\"register_all_classes()\",\n\t\t\t\t\"register_class_FullClass();\",\n\t\t\t\t`#include \"full.h\"`,\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgenerator := &Generator{\n\t\t\t\tBaseName:  tt.baseName,\n\t\t\t\tFunctions: tt.functions,\n\t\t\t\tClasses:   tt.classes,\n\t\t\t}\n\n\t\t\tcGen := cFileGenerator{generator}\n\t\t\tcontent, err := cGen.buildContent()\n\t\t\trequire.NoError(t, err)\n\n\t\t\tfor _, expected := range tt.contains {\n\t\t\t\tassert.Contains(t, content, expected, \"Generated C content should contain '%s'\", expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestCFileGenerator_GetTemplateContent(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\tbaseName    string\n\t\tclasses     []phpClass\n\t\tcontains    []string\n\t\tnotContains []string\n\t}{\n\t\t{\n\t\t\tname:     \"extension without classes\",\n\t\t\tbaseName: \"myext\",\n\t\t\tcontains: []string{\n\t\t\t\t`#include \"myext.h\"`,\n\t\t\t\t`#include \"myext_arginfo.h\"`,\n\t\t\t\t\"PHP_MINIT_FUNCTION(myext)\",\n\t\t\t\t\"myext_module_entry\",\n\t\t\t\t\"return SUCCESS;\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"extension with classes\",\n\t\t\tbaseName: \"complex_name\",\n\t\t\tclasses: []phpClass{\n\t\t\t\t{Name: \"TestClass\", GoStruct: \"TestStruct\"},\n\t\t\t\t{Name: \"AnotherClass\", GoStruct: \"AnotherStruct\"},\n\t\t\t},\n\t\t\tcontains: []string{\n\t\t\t\t`#include \"complex_name.h\"`,\n\t\t\t\t`#include \"complex_name_arginfo.h\"`,\n\t\t\t\t\"PHP_MINIT_FUNCTION(complex_name)\",\n\t\t\t\t\"complex_name_module_entry\",\n\t\t\t\t\"register_all_classes()\",\n\t\t\t\t\"register_class_TestClass();\",\n\t\t\t\t\"register_class_AnotherClass();\",\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgenerator := &Generator{\n\t\t\t\tBaseName: tt.baseName,\n\t\t\t\tClasses:  tt.classes,\n\t\t\t}\n\t\t\tcGen := cFileGenerator{generator}\n\t\t\tcontent, err := cGen.getTemplateContent()\n\t\t\trequire.NoError(t, err)\n\n\t\t\tfor _, expected := range tt.contains {\n\t\t\t\tassert.Contains(t, content, expected, \"Template content should contain '%s'\", expected)\n\t\t\t}\n\n\t\t\tfor _, notExpected := range tt.notContains {\n\t\t\t\tassert.NotContains(t, content, notExpected, \"Template content should NOT contain '%s'\", notExpected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestCFileIntegrationWithGenerators(t *testing.T) {\n\ttmpDir := t.TempDir()\n\n\tfunctions := []phpFunction{\n\t\t{\n\t\t\tName:             \"processData\",\n\t\t\tReturnType:       phpArray,\n\t\t\tIsReturnNullable: true,\n\t\t\tParams: []phpParameter{\n\t\t\t\t{Name: \"input\", PhpType: phpString},\n\t\t\t\t{Name: \"options\", PhpType: phpArray, HasDefault: true, DefaultValue: \"[]\"},\n\t\t\t\t{Name: \"callback\", PhpType: phpObject, IsNullable: true},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:       \"validateInput\",\n\t\t\tReturnType: phpBool,\n\t\t\tParams: []phpParameter{\n\t\t\t\t{Name: \"data\", PhpType: phpString, IsNullable: true},\n\t\t\t\t{Name: \"strict\", PhpType: phpBool, HasDefault: true, DefaultValue: \"false\"},\n\t\t\t},\n\t\t},\n\t}\n\n\tclasses := []phpClass{\n\t\t{\n\t\t\tName:     \"DataProcessor\",\n\t\t\tGoStruct: \"DataProcessorStruct\",\n\t\t\tProperties: []phpClassProperty{\n\t\t\t\t{Name: \"mode\", PhpType: phpString},\n\t\t\t\t{Name: \"timeout\", PhpType: phpInt, IsNullable: true},\n\t\t\t\t{Name: \"options\", PhpType: phpArray},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:     \"Result\",\n\t\t\tGoStruct: \"ResultStruct\",\n\t\t\tProperties: []phpClassProperty{\n\t\t\t\t{Name: \"success\", PhpType: phpBool},\n\t\t\t\t{Name: \"data\", PhpType: phpMixed, IsNullable: true},\n\t\t\t\t{Name: \"errors\", PhpType: phpArray},\n\t\t\t},\n\t\t},\n\t}\n\n\tgenerator := &Generator{\n\t\tBaseName:  \"integration_test\",\n\t\tBuildDir:  tmpDir,\n\t\tFunctions: functions,\n\t\tClasses:   classes,\n\t}\n\n\tcGen := cFileGenerator{generator}\n\trequire.NoError(t, cGen.generate())\n\n\tcontent, err := readFile(filepath.Join(tmpDir, \"integration_test.c\"))\n\trequire.NoError(t, err)\n\n\tfor _, fn := range functions {\n\t\texpectedFunc := \"PHP_FUNCTION(\" + fn.Name + \")\"\n\t\tassert.Contains(t, content, expectedFunc, \"Generated C file should contain function: %s\", expectedFunc)\n\t}\n\n\tfor _, class := range classes {\n\t\texpectedMethod := \"PHP_METHOD(\" + class.Name + \", __construct)\"\n\t\tassert.Contains(t, content, expectedMethod, \"Generated C file should contain class method: %s\", expectedMethod)\n\t}\n\n\tassert.Contains(t, content, \"register_all_classes()\", \"Generated C file should contain class registration call\")\n\tassert.Contains(t, content, \"integration_test_module_entry\", \"Generated C file should contain integration_test_module_entry\")\n}\n\nfunc TestCFileErrorHandling(t *testing.T) {\n\t// Test with invalid build directory\n\tgenerator := &Generator{\n\t\tBaseName: \"test\",\n\t\tBuildDir: \"/invalid/readonly/path\",\n\t\tFunctions: []phpFunction{\n\t\t\t{Name: \"test\", ReturnType: phpVoid},\n\t\t},\n\t}\n\n\tcGen := cFileGenerator{generator}\n\terr := cGen.generate()\n\tassert.Error(t, err, \"Expected error when writing to invalid directory\")\n}\n\nfunc TestCFileSpecialCharacters(t *testing.T) {\n\ttests := []struct {\n\t\tbaseName string\n\t\texpected string\n\t}{\n\t\t{\"simple\", \"simple\"},\n\t\t{\"my_extension\", \"my_extension\"},\n\t\t{\"ext-with-dashes\", \"ext-with-dashes\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.baseName, func(t *testing.T) {\n\t\t\tgenerator := &Generator{\n\t\t\t\tBaseName: tt.baseName,\n\t\t\t\tFunctions: []phpFunction{\n\t\t\t\t\t{Name: \"test\", ReturnType: phpVoid},\n\t\t\t\t},\n\t\t\t}\n\n\t\t\tcGen := cFileGenerator{generator}\n\t\t\tcontent, err := cGen.buildContent()\n\t\t\trequire.NoError(t, err)\n\n\t\t\texpectedInclude := `#include \"` + tt.expected + `.h\"`\n\t\t\tassert.Contains(t, content, expectedInclude, \"Content should contain include: %s\", expectedInclude)\n\t\t})\n\t}\n}\n\nfunc testCFileBasicStructure(t *testing.T, content, baseName string) {\n\trequiredElements := []string{\n\t\t\"#include <php.h>\",\n\t\t\"#include <Zend/zend_API.h>\",\n\t\t`#include \"_cgo_export.h\"`,\n\t\t`#include \"` + baseName + `.h\"`,\n\t\t`#include \"` + baseName + `_arginfo.h\"`,\n\t\t\"PHP_MINIT_FUNCTION(\" + baseName + \")\",\n\t\tbaseName + \"_module_entry\",\n\t}\n\n\tfor _, element := range requiredElements {\n\t\tassert.Contains(t, content, element, \"C file should contain: %s\", element)\n\t}\n}\n\nfunc testCFileFunctions(t *testing.T, content string, functions []phpFunction) {\n\tfor _, fn := range functions {\n\t\tphpFunc := \"PHP_FUNCTION(\" + fn.Name + \")\"\n\t\tassert.Contains(t, content, phpFunc, \"C file should contain function declaration: %s\", phpFunc)\n\t}\n}\n\nfunc testCFileClasses(t *testing.T, content string, classes []phpClass) {\n\tif len(classes) == 0 {\n\t\t// Si pas de classes, ne devrait pas contenir register_all_classes\n\t\tassert.NotContains(t, content, \"register_all_classes()\", \"C file should NOT contain register_all_classes call when no classes\")\n\t\treturn\n\t}\n\n\tassert.Contains(t, content, \"void register_all_classes() {\", \"C file should contain register_all_classes function\")\n\tassert.Contains(t, content, \"register_all_classes();\", \"C file should contain register_all_classes call in MINIT\")\n\n\tfor _, class := range classes {\n\t\texpectedCall := \"register_class_\" + class.Name + \"();\"\n\t\tassert.Contains(t, content, expectedCall, \"C file should contain class registration call: %s\", expectedCall)\n\n\t\tconstructor := \"PHP_METHOD(\" + class.Name + \", __construct)\"\n\t\tassert.Contains(t, content, constructor, \"C file should contain constructor: %s\", constructor)\n\t}\n}\n\nfunc TestCFileContentValidation(t *testing.T) {\n\tgenerator := &Generator{\n\t\tBaseName: \"syntax_test\",\n\t\tFunctions: []phpFunction{\n\t\t\t{\n\t\t\t\tName:       \"testFunction\",\n\t\t\t\tReturnType: phpString,\n\t\t\t\tParams: []phpParameter{\n\t\t\t\t\t{Name: \"param\", PhpType: phpString},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tClasses: []phpClass{\n\t\t\t{Name: \"TestClass\", GoStruct: \"TestStruct\"},\n\t\t},\n\t}\n\n\tcGen := cFileGenerator{generator}\n\tcontent, err := cGen.buildContent()\n\trequire.NoError(t, err)\n\n\tsyntaxElements := []string{\n\t\t\"{\", \"}\", \"(\", \")\", \";\",\n\t\t\"static\", \"void\", \"int\",\n\t\t\"#include\",\n\t}\n\n\tfor _, element := range syntaxElements {\n\t\tassert.Contains(t, content, element, \"Generated C content should contain basic C syntax: %s\", element)\n\t}\n\n\topenBraces := strings.Count(content, \"{\")\n\tcloseBraces := strings.Count(content, \"}\")\n\n\tassert.Equal(t, openBraces, closeBraces, \"Unbalanced braces in generated C code: %d open, %d close\", openBraces, closeBraces)\n\tassert.False(t, strings.Contains(content, \";;\"), \"Generated C code contains double semicolons\")\n\tassert.False(t, strings.Contains(content, \"{{\") || strings.Contains(content, \"}}\"), \"Generated C code contains unresolved template syntax\")\n}\n\nfunc TestCFileConstants(t *testing.T) {\n\ttests := []struct {\n\t\tname      string\n\t\tbaseName  string\n\t\tconstants []phpConstant\n\t\tclasses   []phpClass\n\t\tcontains  []string\n\t}{\n\t\t{\n\t\t\tname:     \"global constants only\",\n\t\t\tbaseName: \"const_test\",\n\t\t\tconstants: []phpConstant{\n\t\t\t\t{\n\t\t\t\t\tName:    \"GLOBAL_INT\",\n\t\t\t\t\tValue:   \"42\",\n\t\t\t\t\tPhpType: phpInt,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tName:    \"GLOBAL_STRING\",\n\t\t\t\t\tValue:   `\"test\"`,\n\t\t\t\t\tPhpType: phpString,\n\t\t\t\t},\n\t\t\t},\n\t\t\tcontains: []string{\n\t\t\t\t`REGISTER_LONG_CONSTANT(\"GLOBAL_INT\", 42, CONST_CS | CONST_PERSISTENT);`,\n\t\t\t\t`REGISTER_STRING_CONSTANT(\"GLOBAL_STRING\", \"test\", CONST_CS | CONST_PERSISTENT);`,\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgenerator := &Generator{\n\t\t\t\tBaseName:  tt.baseName,\n\t\t\t\tConstants: tt.constants,\n\t\t\t\tClasses:   tt.classes,\n\t\t\t}\n\n\t\t\tcGen := cFileGenerator{generator}\n\t\t\tcontent, err := cGen.buildContent()\n\t\t\trequire.NoError(t, err)\n\n\t\t\tfor _, expected := range tt.contains {\n\t\t\t\tassert.Contains(t, content, expected, \"Generated C content should contain '%s'\", expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestCFileTemplateErrorHandling(t *testing.T) {\n\tgenerator := &Generator{\n\t\tBaseName: \"error_test\",\n\t}\n\n\tcGen := cFileGenerator{generator}\n\n\t_, err := cGen.getTemplateContent()\n\tassert.NoError(t, err, \"getTemplateContent() should not fail with valid template\")\n}\n\nfunc TestEscapeCString(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"simple namespace with single backslash\",\n\t\t\tinput:    `Go\\Extension`,\n\t\t\texpected: `Go\\\\Extension`,\n\t\t},\n\t\t{\n\t\t\tname:     \"namespace with multiple backslashes\",\n\t\t\tinput:    `My\\Deep\\Namespace`,\n\t\t\texpected: `My\\\\Deep\\\\Namespace`,\n\t\t},\n\t\t{\n\t\t\tname:     \"complex nested namespace\",\n\t\t\tinput:    `TestIntegration\\Extension\\Module`,\n\t\t\texpected: `TestIntegration\\\\Extension\\\\Module`,\n\t\t},\n\t\t{\n\t\t\tname:     \"empty string\",\n\t\t\tinput:    \"\",\n\t\t\texpected: \"\",\n\t\t},\n\t\t{\n\t\t\tname:     \"single backslash\",\n\t\t\tinput:    `\\`,\n\t\t\texpected: `\\\\`,\n\t\t},\n\t\t{\n\t\t\tname:     \"multiple consecutive backslashes\",\n\t\t\tinput:    `\\\\\\`,\n\t\t\texpected: `\\\\\\\\\\\\`,\n\t\t},\n\t\t{\n\t\t\tname:     \"string without backslashes\",\n\t\t\tinput:    \"TestNamespace\",\n\t\t\texpected: \"TestNamespace\",\n\t\t},\n\t\t{\n\t\t\tname:     \"leading backslash\",\n\t\t\tinput:    `\\Leading`,\n\t\t\texpected: `\\\\Leading`,\n\t\t},\n\t\t{\n\t\t\tname:     \"trailing backslash\",\n\t\t\tinput:    `Trailing\\`,\n\t\t\texpected: `Trailing\\\\`,\n\t\t},\n\t\t{\n\t\t\tname:     \"mixed alphanumeric with backslashes\",\n\t\t\tinput:    `Path123\\To456\\File789`,\n\t\t\texpected: `Path123\\\\To456\\\\File789`,\n\t\t},\n\t\t{\n\t\t\tname:     \"unicode characters with backslashes\",\n\t\t\tinput:    `Namespace\\Über\\Test`,\n\t\t\texpected: `Namespace\\\\Über\\\\Test`,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := escapeCString(tt.input)\n\t\t\tassert.Equal(t, tt.expected, result, \"escapeCString(%q) should return %q, got %q\", tt.input, tt.expected, result)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/extgen/classparser.go",
    "content": "package extgen\n\nimport (\n\t\"bufio\"\n\t\"fmt\"\n\t\"go/ast\"\n\t\"go/parser\"\n\t\"go/token\"\n\t\"os\"\n\t\"regexp\"\n\t\"strings\"\n)\n\nvar phpClassRegex = regexp.MustCompile(`//\\s*export_php:class\\s+(\\w+)`)\nvar phpMethodRegex = regexp.MustCompile(`//\\s*export_php:method\\s+(\\w+)::([^{}\\n]+)(?:\\s*{\\s*})?`)\nvar methodSignatureRegex = regexp.MustCompile(`(\\w+)\\s*\\(([^)]*)\\)\\s*:\\s*(\\??[\\w|]+)`)\nvar methodParamTypeNameRegex = regexp.MustCompile(`(\\??[\\w|]+)\\s+\\$?(\\w+)`)\n\ntype exportDirective struct {\n\tline      int\n\tclassName string\n}\n\ntype classParser struct{}\n\nfunc (cp *classParser) Parse(filename string) ([]phpClass, error) {\n\treturn cp.parse(filename)\n}\n\nfunc (cp *classParser) parse(filename string) (classes []phpClass, err error) {\n\tfset := token.NewFileSet()\n\tnode, err := parser.ParseFile(fset, filename, nil, parser.ParseComments)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"parsing file: %w\", err)\n\t}\n\n\tvalidator := Validator{}\n\n\texportDirectives := cp.collectExportDirectives(node, fset)\n\tmethods, err := cp.parseMethods(filename)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"parsing methods: %w\", err)\n\t}\n\n\t// match structs to directives\n\tmatchedDirectives := make(map[int]bool)\n\n\tvar genDecl *ast.GenDecl\n\tvar ok bool\n\tfor _, decl := range node.Decls {\n\t\tif genDecl, ok = decl.(*ast.GenDecl); !ok || genDecl.Tok != token.TYPE {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, spec := range genDecl.Specs {\n\t\t\tvar typeSpec *ast.TypeSpec\n\t\t\tif typeSpec, ok = spec.(*ast.TypeSpec); !ok {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tvar structType *ast.StructType\n\t\t\tif structType, ok = typeSpec.Type.(*ast.StructType); !ok {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tvar phpCl string\n\t\t\tvar directiveLine int\n\t\t\tif phpCl, directiveLine = cp.extractPHPClassCommentWithLine(genDecl.Doc, fset); phpCl == \"\" {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tmatchedDirectives[directiveLine] = true\n\n\t\t\tclass := phpClass{\n\t\t\t\tName:     phpCl,\n\t\t\t\tGoStruct: typeSpec.Name.Name,\n\t\t\t}\n\n\t\t\tclass.Properties = cp.parseStructFields(structType.Fields.List)\n\n\t\t\t// associate methods with this class\n\t\t\tfor _, method := range methods {\n\t\t\t\tif method.ClassName == phpCl {\n\t\t\t\t\tclass.Methods = append(class.Methods, method)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif err := validator.validateClass(class); err != nil {\n\t\t\t\tfmt.Printf(\"Warning: Invalid class '%s': %v\\n\", class.Name, err)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tclasses = append(classes, class)\n\t\t}\n\t}\n\n\tfor _, directive := range exportDirectives {\n\t\tif !matchedDirectives[directive.line] {\n\t\t\treturn nil, fmt.Errorf(\"//export_php class directive at line %d is not followed by a struct declaration\", directive.line)\n\t\t}\n\t}\n\n\treturn classes, nil\n}\n\nfunc (cp *classParser) collectExportDirectives(node *ast.File, fset *token.FileSet) []exportDirective {\n\tvar directives []exportDirective\n\n\tfor _, commentGroup := range node.Comments {\n\t\tfor _, comment := range commentGroup.List {\n\t\t\tif matches := phpClassRegex.FindStringSubmatch(comment.Text); matches != nil {\n\t\t\t\tpos := fset.Position(comment.Pos())\n\t\t\t\tdirectives = append(directives, exportDirective{\n\t\t\t\t\tline:      pos.Line,\n\t\t\t\t\tclassName: matches[1],\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t}\n\n\treturn directives\n}\n\nfunc (cp *classParser) extractPHPClassCommentWithLine(commentGroup *ast.CommentGroup, fset *token.FileSet) (string, int) {\n\tif commentGroup == nil {\n\t\treturn \"\", 0\n\t}\n\n\tfor _, comment := range commentGroup.List {\n\t\tif matches := phpClassRegex.FindStringSubmatch(comment.Text); matches != nil {\n\t\t\tpos := fset.Position(comment.Pos())\n\t\t\treturn matches[1], pos.Line\n\t\t}\n\t}\n\n\treturn \"\", 0\n}\n\nfunc (cp *classParser) parseStructFields(fields []*ast.Field) []phpClassProperty {\n\tvar properties []phpClassProperty\n\n\tfor _, field := range fields {\n\t\tfor _, name := range field.Names {\n\t\t\tprop := cp.parseStructField(name.Name, field)\n\t\t\tproperties = append(properties, prop)\n\t\t}\n\t}\n\n\treturn properties\n}\n\nfunc (cp *classParser) parseStructField(fieldName string, field *ast.Field) phpClassProperty {\n\tprop := phpClassProperty{Name: fieldName}\n\n\t// check if field is a pointer (nullable)\n\tif starExpr, isPointer := field.Type.(*ast.StarExpr); isPointer {\n\t\tprop.IsNullable = true\n\t\tprop.GoType = cp.typeToString(starExpr.X)\n\t} else {\n\t\tprop.IsNullable = false\n\t\tprop.GoType = cp.typeToString(field.Type)\n\t}\n\n\tprop.PhpType = cp.goTypeToPHPType(prop.GoType)\n\n\treturn prop\n}\n\nfunc (cp *classParser) typeToString(expr ast.Expr) string {\n\tswitch t := expr.(type) {\n\tcase *ast.Ident:\n\t\treturn t.Name\n\tcase *ast.StarExpr:\n\t\treturn \"*\" + cp.typeToString(t.X)\n\tcase *ast.ArrayType:\n\t\treturn \"[]\" + cp.typeToString(t.Elt)\n\tcase *ast.MapType:\n\t\treturn \"map[\" + cp.typeToString(t.Key) + \"]\" + cp.typeToString(t.Value)\n\tdefault:\n\t\treturn \"any\"\n\t}\n}\n\nvar goToPhpTypeMap = map[string]phpType{\n\t\"string\": phpString,\n\t\"int\":    phpInt, \"int64\": phpInt, \"int32\": phpInt, \"int16\": phpInt, \"int8\": phpInt,\n\t\"uint\": phpInt, \"uint64\": phpInt, \"uint32\": phpInt, \"uint16\": phpInt, \"uint8\": phpInt,\n\t\"float64\": phpFloat, \"float32\": phpFloat,\n\t\"bool\": phpBool,\n\t\"any\":  phpMixed,\n}\n\nfunc (cp *classParser) goTypeToPHPType(goType string) phpType {\n\tgoType = strings.TrimPrefix(goType, \"*\")\n\n\tif phpType, exists := goToPhpTypeMap[goType]; exists {\n\t\treturn phpType\n\t}\n\n\tif strings.HasPrefix(goType, \"[]\") || strings.HasPrefix(goType, \"map[\") {\n\t\treturn phpArray\n\t}\n\n\treturn phpMixed\n}\n\nfunc (cp *classParser) parseMethods(filename string) (methods []phpClassMethod, err error) {\n\tfile, err := os.Open(filename)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tdefer func() {\n\t\te := file.Close()\n\t\tif err != nil {\n\t\t\terr = e\n\t\t}\n\t}()\n\n\tscanner := bufio.NewScanner(file)\n\tvar currentMethod *phpClassMethod\n\n\tlineNumber := 0\n\tfor scanner.Scan() {\n\t\tlineNumber++\n\t\tline := strings.TrimSpace(scanner.Text())\n\n\t\tif matches := phpMethodRegex.FindStringSubmatch(line); matches != nil {\n\t\t\tclassName := strings.TrimSpace(matches[1])\n\t\t\tsignature := strings.TrimSpace(matches[2])\n\n\t\t\tmethod, err := cp.parseMethodSignature(className, signature)\n\t\t\tif err != nil {\n\t\t\t\tfmt.Printf(\"Warning: Error parsing method signature %q: %v\\n\", signature, err)\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tvalidator := Validator{}\n\t\t\tphpFunc := phpFunction{\n\t\t\t\tName:             method.Name,\n\t\t\t\tSignature:        method.Signature,\n\t\t\t\tParams:           method.Params,\n\t\t\t\tReturnType:       method.ReturnType,\n\t\t\t\tIsReturnNullable: method.isReturnNullable,\n\t\t\t}\n\n\t\t\tif err := validator.validateTypes(phpFunc); err != nil {\n\t\t\t\tfmt.Printf(\"Warning: Method \\\"%s::%s\\\" uses unsupported types: %v\\n\", className, method.Name, err)\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tmethod.lineNumber = lineNumber\n\t\t\tcurrentMethod = method\n\t\t}\n\n\t\tif currentMethod != nil && strings.HasPrefix(line, \"func \") {\n\t\t\tgoFunc, err := cp.extractGoMethodFunction(scanner, line)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"extracting Go method function: %w\", err)\n\t\t\t}\n\n\t\t\tcurrentMethod.GoFunction = goFunc\n\n\t\t\tvalidator := Validator{}\n\t\t\tphpFunc := phpFunction{\n\t\t\t\tName:             currentMethod.Name,\n\t\t\t\tSignature:        currentMethod.Signature,\n\t\t\t\tGoFunction:       currentMethod.GoFunction,\n\t\t\t\tParams:           currentMethod.Params,\n\t\t\t\tReturnType:       currentMethod.ReturnType,\n\t\t\t\tIsReturnNullable: currentMethod.isReturnNullable,\n\t\t\t}\n\n\t\t\tif err := validator.validateGoFunctionSignatureWithOptions(phpFunc, true); err != nil {\n\t\t\t\tfmt.Printf(\"Warning: Go method signature mismatch for '%s::%s': %v\\n\", currentMethod.ClassName, currentMethod.Name, err)\n\t\t\t\tcurrentMethod = nil\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tmethods = append(methods, *currentMethod)\n\t\t\tcurrentMethod = nil\n\t\t}\n\t}\n\n\tif currentMethod != nil {\n\t\treturn nil, fmt.Errorf(\"//export_php:method directive at line %d is not followed by a function declaration\", currentMethod.lineNumber)\n\t}\n\n\treturn methods, scanner.Err()\n}\n\nfunc (cp *classParser) parseMethodSignature(className, signature string) (*phpClassMethod, error) {\n\tmatches := methodSignatureRegex.FindStringSubmatch(signature)\n\n\tif len(matches) != 4 {\n\t\treturn nil, fmt.Errorf(\"invalid method signature format\")\n\t}\n\n\tmethodName := matches[1]\n\tparamsStr := strings.TrimSpace(matches[2])\n\treturnTypeStr := strings.TrimSpace(matches[3])\n\n\tisReturnNullable := strings.HasPrefix(returnTypeStr, \"?\")\n\treturnType := strings.TrimPrefix(returnTypeStr, \"?\")\n\n\tvar params []phpParameter\n\tif paramsStr != \"\" {\n\t\tparamParts := strings.SplitSeq(paramsStr, \",\")\n\t\tfor part := range paramParts {\n\t\t\tparam, err := cp.parseMethodParameter(strings.TrimSpace(part))\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"parsing parameter '%s': %w\", part, err)\n\t\t\t}\n\n\t\t\tparams = append(params, param)\n\t\t}\n\t}\n\n\treturn &phpClassMethod{\n\t\tName:             methodName,\n\t\tPhpName:          methodName,\n\t\tClassName:        className,\n\t\tSignature:        signature,\n\t\tParams:           params,\n\t\tReturnType:       phpType(returnType),\n\t\tisReturnNullable: isReturnNullable,\n\t}, nil\n}\n\nfunc (cp *classParser) parseMethodParameter(paramStr string) (phpParameter, error) {\n\tparts := strings.Split(paramStr, \"=\")\n\ttypePart := strings.TrimSpace(parts[0])\n\n\tparam := phpParameter{HasDefault: len(parts) > 1}\n\n\tif param.HasDefault {\n\t\tparam.DefaultValue = cp.sanitizeDefaultValue(strings.TrimSpace(parts[1]))\n\t}\n\n\tmatches := methodParamTypeNameRegex.FindStringSubmatch(typePart)\n\n\tif len(matches) < 3 {\n\t\treturn phpParameter{}, fmt.Errorf(\"invalid parameter format: %s\", paramStr)\n\t}\n\n\ttypeStr := strings.TrimSpace(matches[1])\n\tparam.Name = strings.TrimSpace(matches[2])\n\tparam.IsNullable = strings.HasPrefix(typeStr, \"?\")\n\tparam.PhpType = phpType(strings.TrimPrefix(typeStr, \"?\"))\n\n\treturn param, nil\n}\n\nfunc (cp *classParser) sanitizeDefaultValue(value string) string {\n\tif strings.HasPrefix(value, \"[\") && strings.HasSuffix(value, \"]\") {\n\t\treturn value\n\t}\n\n\tif strings.ToLower(value) == \"null\" {\n\t\treturn \"null\"\n\t}\n\n\treturn strings.Trim(value, `'\"`)\n}\n\nfunc (cp *classParser) extractGoMethodFunction(scanner *bufio.Scanner, firstLine string) (string, error) {\n\tgoFunc := firstLine + \"\\n\"\n\tbraceCount := 1\n\n\tfor scanner.Scan() {\n\t\tline := scanner.Text()\n\t\tgoFunc += line + \"\\n\"\n\n\t\tfor _, char := range line {\n\t\t\tswitch char {\n\t\t\tcase '{':\n\t\t\t\tbraceCount++\n\t\t\tcase '}':\n\t\t\t\tbraceCount--\n\t\t\t}\n\t\t}\n\n\t\tif braceCount == 0 {\n\t\t\tbreak\n\t\t}\n\t}\n\n\treturn goFunc, nil\n}\n"
  },
  {
    "path": "internal/extgen/classparser_test.go",
    "content": "package extgen\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestClassParser(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    string\n\t\texpected int\n\t}{\n\t\t{\n\t\t\tname: \"single class\",\n\t\t\tinput: `package main\n\n//export_php:class User\ntype UserStruct struct {\n\tname string\n\tAge  int\n}`,\n\t\t\texpected: 1,\n\t\t},\n\t\t{\n\t\t\tname: \"multiple classes\",\n\t\t\tinput: `package main\n\n//export_php:class User\ntype UserStruct struct {\n\tname string\n\tAge  int\n}\n\n//export_php:class Product\ntype ProductStruct struct {\n\tTitle string\n\tPrice float64\n}`,\n\t\t\texpected: 2,\n\t\t},\n\t\t{\n\t\t\tname: \"no php classes\",\n\t\t\tinput: `package main\n\ntype RegularStruct struct {\n\tData string\n}`,\n\t\t\texpected: 0,\n\t\t},\n\t\t{\n\t\t\tname: \"class with nullable fields\",\n\t\t\tinput: `package main\n\n//export_php:class OptionalData\ntype OptionalStruct struct {\n\tRequired string\n\tOptional *string\n\tCount    *int\n}`,\n\t\t\texpected: 1,\n\t\t},\n\t\t{\n\t\t\tname: \"class with methods\",\n\t\t\tinput: `package main\n\n//export_php:class User\ntype UserStruct struct {\n\tname string\n\tAge  int\n}\n\n//export_php:method User::getName(): string\nfunc GetUserName(u UserStruct) string {\n\treturn u.name\n}\n\n//export_php:method User::setAge(int $age): void\nfunc SetUserAge(u *UserStruct, age int) {\n\tu.Age = age\n}`,\n\t\t\texpected: 1,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ttmpDir := t.TempDir()\n\t\t\tfileName := filepath.Join(tmpDir, tt.name+\".go\")\n\t\t\trequire.NoError(t, os.WriteFile(fileName, []byte(tt.input), 0644))\n\n\t\t\tparser := classParser{}\n\t\t\tclasses, err := parser.parse(fileName)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Len(t, classes, tt.expected, \"parse() got wrong number of classes\")\n\n\t\t\tif tt.name == \"single class\" && len(classes) > 0 {\n\t\t\t\tclass := classes[0]\n\t\t\t\tassert.Equal(t, \"User\", class.Name, \"Expected class name 'User'\")\n\t\t\t\tassert.Equal(t, \"UserStruct\", class.GoStruct, \"Expected Go struct 'UserStruct'\")\n\t\t\t\tassert.Len(t, class.Properties, 2, \"Expected 2 properties\")\n\t\t\t}\n\n\t\t\tif tt.name == \"class with nullable fields\" && len(classes) > 0 {\n\t\t\t\tclass := classes[0]\n\t\t\t\tif len(class.Properties) >= 3 {\n\t\t\t\t\tassert.False(t, class.Properties[0].IsNullable, \"Required field should not be nullable\")\n\t\t\t\t\tassert.True(t, class.Properties[1].IsNullable, \"Optional field should be nullable\")\n\t\t\t\t\tassert.True(t, class.Properties[2].IsNullable, \"Count field should be nullable\")\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestClassMethods(t *testing.T) {\n\tvar input = []byte(`package main\n\n//export_php:class User\ntype UserStruct struct {\n\tname string\n\tAge  int\n}\n\n//export_php:method User::getName(): string\nfunc GetUserName(u UserStruct) unsafe.Pointer {\n\treturn nil\n}\n\n//export_php:method User::setAge(int $age): void\nfunc SetUserAge(u *UserStruct, age int64) {\n\tu.Age = int(age)\n}\n\n//export_php:method User::getInfo(string $prefix = \"User\"): string\nfunc GetUserInfo(u UserStruct, prefix *C.zend_string) unsafe.Pointer {\n\treturn nil\n}`)\n\n\ttmpDir := t.TempDir()\n\tfileName := filepath.Join(tmpDir, \"test.go\")\n\trequire.NoError(t, os.WriteFile(fileName, input, 0644))\n\n\tparser := classParser{}\n\tclasses, err := parser.parse(fileName)\n\trequire.NoError(t, err)\n\n\trequire.Len(t, classes, 1, \"Expected 1 class\")\n\n\tclass := classes[0]\n\trequire.Len(t, class.Methods, 3, \"Expected 3 methods\")\n\n\tgetName := class.Methods[0]\n\tassert.Equal(t, \"getName\", getName.Name, \"Expected method name 'getName'\")\n\tassert.Equal(t, phpString, getName.ReturnType, \"Expected return type 'string'\")\n\tassert.Empty(t, getName.Params, \"Expected 0 params\")\n\tassert.Equal(t, \"User\", getName.ClassName, \"Expected class name 'User'\")\n\n\tsetAge := class.Methods[1]\n\tassert.Equal(t, \"setAge\", setAge.Name, \"Expected method name 'setAge'\")\n\tassert.Equal(t, phpVoid, setAge.ReturnType, \"Expected return type 'void'\")\n\trequire.Len(t, setAge.Params, 1, \"Expected 1 param\")\n\n\tparam := setAge.Params[0]\n\tassert.Equal(t, \"age\", param.Name, \"Expected param name 'age'\")\n\tassert.Equal(t, phpInt, param.PhpType, \"Expected param type 'int'\")\n\tassert.False(t, param.IsNullable, \"Expected param to not be nullable\")\n\tassert.False(t, param.HasDefault, \"Expected param to not have default value\")\n\n\tgetInfo := class.Methods[2]\n\tassert.Equal(t, \"getInfo\", getInfo.Name, \"Expected method name 'getInfo'\")\n\tassert.Equal(t, phpString, getInfo.ReturnType, \"Expected return type 'string'\")\n\trequire.Len(t, getInfo.Params, 1, \"Expected 1 param\")\n\n\tparam = getInfo.Params[0]\n\tassert.Equal(t, \"prefix\", param.Name, \"Expected param name 'prefix'\")\n\tassert.Equal(t, phpString, param.PhpType, \"Expected param type 'string'\")\n\tassert.True(t, param.HasDefault, \"Expected param to have default value\")\n\tassert.Equal(t, \"User\", param.DefaultValue, \"Expected default value 'User'\")\n}\n\nfunc TestMethodParameterParsing(t *testing.T) {\n\ttests := []struct {\n\t\tname          string\n\t\tparamStr      string\n\t\texpectedParam phpParameter\n\t\texpectError   bool\n\t}{\n\t\t{\n\t\t\tname:     \"simple int parameter\",\n\t\t\tparamStr: \"int $age\",\n\t\t\texpectedParam: phpParameter{\n\t\t\t\tName:       \"age\",\n\t\t\t\tPhpType:    phpInt,\n\t\t\t\tIsNullable: false,\n\t\t\t\tHasDefault: false,\n\t\t\t},\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"nullable string parameter\",\n\t\t\tparamStr: \"?string $name\",\n\t\t\texpectedParam: phpParameter{\n\t\t\t\tName:       \"name\",\n\t\t\t\tPhpType:    phpString,\n\t\t\t\tIsNullable: true,\n\t\t\t\tHasDefault: false,\n\t\t\t},\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"parameter with default value\",\n\t\t\tparamStr: `string $prefix = \"default\"`,\n\t\t\texpectedParam: phpParameter{\n\t\t\t\tName:         \"prefix\",\n\t\t\t\tPhpType:      phpString,\n\t\t\t\tIsNullable:   false,\n\t\t\t\tHasDefault:   true,\n\t\t\t\tDefaultValue: \"default\",\n\t\t\t},\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"nullable parameter with default null\",\n\t\t\tparamStr: \"?int $count = null\",\n\t\t\texpectedParam: phpParameter{\n\t\t\t\tName:         \"count\",\n\t\t\t\tPhpType:      phpInt,\n\t\t\t\tIsNullable:   true,\n\t\t\t\tHasDefault:   true,\n\t\t\t\tDefaultValue: \"null\",\n\t\t\t},\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname:        \"invalid parameter format\",\n\t\t\tparamStr:    \"invalid\",\n\t\t\texpectError: true,\n\t\t},\n\t}\n\n\tparser := classParser{}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tparam, err := parser.parseMethodParameter(tt.paramStr)\n\n\t\t\tif tt.expectError {\n\t\t\t\tassert.Error(t, err, \"Expected error for parameter '%s', but got none\", tt.paramStr)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\trequire.NoError(t, err, \"parseMethodParameter(%s) error\", tt.paramStr)\n\n\t\t\tassert.Equal(t, tt.expectedParam.Name, param.Name, \"Expected name '%s'\", tt.expectedParam.Name)\n\t\t\tassert.Equal(t, tt.expectedParam.PhpType, param.PhpType, \"Expected type '%s'\", tt.expectedParam.PhpType)\n\t\t\tassert.Equal(t, tt.expectedParam.IsNullable, param.IsNullable, \"Expected isNullable %v\", tt.expectedParam.IsNullable)\n\t\t\tassert.Equal(t, tt.expectedParam.HasDefault, param.HasDefault, \"Expected hasDefault %v\", tt.expectedParam.HasDefault)\n\t\t\tassert.Equal(t, tt.expectedParam.DefaultValue, param.DefaultValue, \"Expected defaultValue '%s'\", tt.expectedParam.DefaultValue)\n\t\t})\n\t}\n}\n\nfunc TestGoTypeToPHPType(t *testing.T) {\n\ttests := []struct {\n\t\tgoType   string\n\t\texpected phpType\n\t}{\n\t\t{\"string\", phpString},\n\t\t{\"*string\", phpString},\n\t\t{\"int\", phpInt},\n\t\t{\"int64\", phpInt},\n\t\t{\"*int\", phpInt},\n\t\t{\"float64\", phpFloat},\n\t\t{\"*float32\", phpFloat},\n\t\t{\"bool\", phpBool},\n\t\t{\"*bool\", phpBool},\n\t\t{\"[]string\", phpArray},\n\t\t{\"map[string]int\", phpArray},\n\t\t{\"*[]int\", phpArray},\n\t\t{\"any\", phpMixed},\n\t\t{\"CustomType\", phpMixed},\n\t}\n\n\tparser := classParser{}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.goType, func(t *testing.T) {\n\t\t\tresult := parser.goTypeToPHPType(tt.goType)\n\t\t\tassert.Equal(t, tt.expected, result, \"goTypeToPHPType(%s) = %s, want %s\", tt.goType, result, tt.expected)\n\t\t})\n\t}\n}\n\nfunc TestTypeToString(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    string\n\t\texpected []phpType\n\t}{\n\t\t{\n\t\t\tname: \"basic types\",\n\t\t\tinput: `package main\n\n//export_php:class TestClass\ntype TestStruct struct {\n\tStringField string\n\tIntField    int\n\tFloatField  float64\n\tBoolField   bool\n}`,\n\t\t\texpected: []phpType{phpString, phpInt, phpFloat, phpBool},\n\t\t},\n\t\t{\n\t\t\tname: \"pointer types\",\n\t\t\tinput: `package main\n\n//export_php:class NullableClass\ntype NullableStruct struct {\n\tNullableString *string\n\tNullableInt    *int\n\tNullableFloat  *float64\n\tNullableBool   *bool\n}`,\n\t\t\texpected: []phpType{phpString, phpInt, phpFloat, phpBool},\n\t\t},\n\t\t{\n\t\t\tname: \"collection types\",\n\t\t\tinput: `package main\n\n//export_php:class CollectionClass\ntype CollectionStruct struct {\n\tStringSlice []string\n\tIntMap      map[string]int\n\tMixedSlice  []any\n}`,\n\t\t\texpected: []phpType{phpArray, phpArray, phpArray},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ttmpDir := t.TempDir()\n\t\t\tfileName := filepath.Join(tmpDir, tt.name+\".go\")\n\t\t\trequire.NoError(t, os.WriteFile(fileName, []byte(tt.input), 0o644))\n\n\t\t\tparser := classParser{}\n\t\t\tclasses, err := parser.parse(fileName)\n\t\t\trequire.NoError(t, err)\n\n\t\t\trequire.Len(t, classes, 1, \"Expected 1 class\")\n\n\t\t\tclass := classes[0]\n\t\t\trequire.Len(t, class.Properties, len(tt.expected), \"Expected %d properties\", len(tt.expected))\n\n\t\t\tfor i, expectedType := range tt.expected {\n\t\t\t\tassert.Equal(t, expectedType, class.Properties[i].PhpType, \"Property %d: expected type %s\", i, expectedType)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestClassParserUnsupportedTypes(t *testing.T) {\n\ttests := []struct {\n\t\tname            string\n\t\tinput           string\n\t\texpectedClasses int\n\t\texpectedMethods int\n\t\thasWarning      bool\n\t}{\n\t\t{\n\t\t\tname: \"method with array parameter should be rejected\",\n\t\t\tinput: `package main\n\n//export_php:class TestClass\ntype TestClass struct {\n\tName string\n}\n\n//export_php:method TestClass::arrayMethod(array $data): string\nfunc (tc *TestClass) arrayMethod(data any) unsafe.Pointer {\n\treturn nil\n}`,\n\t\t\texpectedClasses: 1,\n\t\t\texpectedMethods: 0,\n\t\t\thasWarning:      true,\n\t\t},\n\t\t{\n\t\t\tname: \"method with object parameter should be rejected\",\n\t\t\tinput: `package main\n\n//export_php:class TestClass\ntype TestClass struct {\n\tName string\n}\n\n//export_php:method TestClass::objectMethod(object $obj): string\nfunc (tc *TestClass) objectMethod(obj any) unsafe.Pointer {\n\treturn nil\n}`,\n\t\t\texpectedClasses: 1,\n\t\t\texpectedMethods: 0,\n\t\t\thasWarning:      true,\n\t\t},\n\t\t{\n\t\t\tname: \"method with mixed parameter should be rejected\",\n\t\t\tinput: `package main\n\n//export_php:class TestClass\ntype TestClass struct {\n\tName string\n}\n\n//export_php:method TestClass::mixedMethod(mixed $value): string\nfunc (tc *TestClass) mixedMethod(value any) unsafe.Pointer {\n\treturn nil\n}`,\n\t\t\texpectedClasses: 1,\n\t\t\texpectedMethods: 0,\n\t\t\thasWarning:      true,\n\t\t},\n\t\t{\n\t\t\tname: \"method with array return type should be rejected\",\n\t\t\tinput: `package main\n\n//export_php:class TestClass\ntype TestClass struct {\n\tName string\n}\n\n//export_php:method TestClass::arrayReturn(string $name): array\nfunc (tc *TestClass) arrayReturn(name *C.zend_string) any {\n\treturn []string{\"result\"}\n}`,\n\t\t\texpectedClasses: 1,\n\t\t\texpectedMethods: 0,\n\t\t\thasWarning:      true,\n\t\t},\n\t\t{\n\t\t\tname: \"method with object return type should be rejected\",\n\t\t\tinput: `package main\n\n//export_php:class TestClass\ntype TestClass struct {\n\tName string\n}\n\n//export_php:method TestClass::objectReturn(string $name): object\nfunc (tc *TestClass) objectReturn(name *C.zend_string) any {\n\treturn map[string]any{\"key\": \"value\"}\n}`,\n\t\t\texpectedClasses: 1,\n\t\t\texpectedMethods: 0,\n\t\t\thasWarning:      true,\n\t\t},\n\t\t{\n\t\t\tname: \"valid scalar types should pass\",\n\t\t\tinput: `package main\n\n//export_php:class TestClass\ntype TestClass struct {\n\tName string\n}\n\n//export_php:method TestClass::validMethod(string $name, int $count, float $rate, bool $active): string\nfunc validMethod(tc *TestClass, name *C.zend_string, count int64, rate float64, active bool) unsafe.Pointer {\n\treturn nil\n}`,\n\t\t\texpectedClasses: 1,\n\t\t\texpectedMethods: 1,\n\t\t\thasWarning:      false,\n\t\t},\n\t\t{\n\t\t\tname: \"valid void return should pass\",\n\t\t\tinput: `package main\n\n//export_php:class TestClass\ntype TestClass struct {\n\tName string\n}\n\n//export_php:method TestClass::voidMethod(string $message): void\nfunc voidMethod(tc *TestClass, message *C.zend_string) {\n\t// Do something\n}`,\n\t\t\texpectedClasses: 1,\n\t\t\texpectedMethods: 1,\n\t\t\thasWarning:      false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ttmpDir := t.TempDir()\n\t\t\tfileName := filepath.Join(tmpDir, tt.name+\".go\")\n\t\t\trequire.NoError(t, os.WriteFile(fileName, []byte(tt.input), 0644))\n\n\t\t\tparser := &classParser{}\n\t\t\tclasses, err := parser.parse(fileName)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Len(t, classes, tt.expectedClasses, \"parse() got wrong number of classes\")\n\t\t\tif len(classes) > 0 {\n\t\t\t\tassert.Len(t, classes[0].Methods, tt.expectedMethods, \"parse() got wrong number of methods\")\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestClassParserGoTypeMismatch(t *testing.T) {\n\ttests := []struct {\n\t\tname            string\n\t\tinput           string\n\t\texpectedClasses int\n\t\texpectedMethods int\n\t\thasWarning      bool\n\t}{\n\t\t{\n\t\t\tname: \"method parameter count mismatch should be rejected\",\n\t\t\tinput: `package main\n\n//export_php:class TestClass\ntype TestClass struct {\n\tName string\n}\n\n//export_php:method TestClass::countMismatch(string $name, int $count): string\nfunc (tc *TestClass) countMismatch(name *C.zend_string) unsafe.Pointer {\n\treturn nil\n}`,\n\t\t\texpectedClasses: 1,\n\t\t\texpectedMethods: 0,\n\t\t\thasWarning:      true,\n\t\t},\n\t\t{\n\t\t\tname: \"method parameter type mismatch should be rejected\",\n\t\t\tinput: `package main\n\n//export_php:class TestClass\ntype TestClass struct {\n\tName string\n}\n\n//export_php:method TestClass::typeMismatch(string $name, int $count): string\nfunc (tc *TestClass) typeMismatch(name *C.zend_string, count string) unsafe.Pointer {\n\treturn nil\n}`,\n\t\t\texpectedClasses: 1,\n\t\t\texpectedMethods: 0,\n\t\t\thasWarning:      true,\n\t\t},\n\t\t{\n\t\t\tname: \"method return type mismatch should be rejected\",\n\t\t\tinput: `package main\n\n//export_php:class TestClass\ntype TestClass struct {\n\tName string\n}\n\n//export_php:method TestClass::returnMismatch(string $name): int\nfunc (tc *TestClass) returnMismatch(name *C.zend_string) string {\n\treturn \"\"\n}`,\n\t\t\texpectedClasses: 1,\n\t\t\texpectedMethods: 0,\n\t\t\thasWarning:      true,\n\t\t},\n\t\t{\n\t\t\tname: \"valid matching types should pass\",\n\t\t\tinput: `package main\n\n//export_php:class TestClass\ntype TestClass struct {\n\tName string\n}\n\n//export_php:method TestClass::validMatch(string $name, int $count): string\nfunc validMatch(tc *TestClass, name *C.zend_string, count int64) unsafe.Pointer {\n\treturn nil\n}`,\n\t\t\texpectedClasses: 1,\n\t\t\texpectedMethods: 1,\n\t\t\thasWarning:      false,\n\t\t},\n\t\t{\n\t\t\tname: \"valid bool types should pass\",\n\t\t\tinput: `package main\n\n//export_php:class TestClass\ntype TestClass struct {\n\tName string\n}\n\n//export_php:method TestClass::validBool(bool $flag): bool\nfunc validBool(tc *TestClass, flag bool) bool {\n\treturn flag\n}`,\n\t\t\texpectedClasses: 1,\n\t\t\texpectedMethods: 1,\n\t\t\thasWarning:      false,\n\t\t},\n\t\t{\n\t\t\tname: \"valid float types should pass\",\n\t\t\tinput: `package main\n\n//export_php:class TestClass\ntype TestClass struct {\n\tName string\n}\n\n//export_php:method TestClass::validFloat(float $value): float\nfunc validFloat(tc *TestClass, value float64) float64 {\n\treturn value\n}`,\n\t\t\texpectedClasses: 1,\n\t\t\texpectedMethods: 1,\n\t\t\thasWarning:      false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ttmpDir := t.TempDir()\n\t\t\tfileName := filepath.Join(tmpDir, tt.name+\".go\")\n\t\t\trequire.NoError(t, os.WriteFile(fileName, []byte(tt.input), 0644))\n\n\t\t\tparser := &classParser{}\n\t\t\tclasses, err := parser.parse(fileName)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Len(t, classes, tt.expectedClasses, \"parse() got wrong number of classes\")\n\t\t\tif len(classes) > 0 {\n\t\t\t\tassert.Len(t, classes[0].Methods, tt.expectedMethods, \"parse() got wrong number of methods\")\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/extgen/constants_test.go",
    "content": "package extgen\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestConstantsIntegration(t *testing.T) {\n\ttmpDir := t.TempDir()\n\ttestFile := filepath.Join(tmpDir, \"test.go\")\n\n\tcontent := `package main\n\n//export_php:const\nconst STATUS_OK = iota\n\n//export_php:const\nconst MAX_CONNECTIONS = 100\n\n//export_php:const: function test(): void\nfunc Test() {\n    // Implementation\n}\n\nfunc main() {}\n`\n\n\trequire.NoError(t, os.WriteFile(testFile, []byte(content), 0644))\n\n\tgenerator := &Generator{\n\t\tBaseName:   \"testext\",\n\t\tSourceFile: testFile,\n\t\tBuildDir:   filepath.Join(tmpDir, \"build\"),\n\t}\n\n\trequire.NoError(t, generator.parseSource())\n\tassert.Len(t, generator.Constants, 2, \"Expected 2 constants\")\n\n\texpectedConstants := map[string]struct {\n\t\tValue  string\n\t\tIsIota bool\n\t}{\n\t\t\"STATUS_OK\":       {\"0\", true},\n\t\t\"MAX_CONNECTIONS\": {\"100\", false},\n\t}\n\n\tfor _, constant := range generator.Constants {\n\t\texpected, exists := expectedConstants[constant.Name]\n\t\tassert.True(t, exists, \"Unexpected constant: %s\", constant.Name)\n\t\tif !exists {\n\t\t\tcontinue\n\t\t}\n\n\t\tassert.Equal(t, expected.Value, constant.Value, \"Constant %s: value mismatch\", constant.Name)\n\t\tassert.Equal(t, expected.IsIota, constant.IsIota, \"Constant %s: isIota mismatch\", constant.Name)\n\t}\n\n\trequire.NoError(t, generator.setupBuildDirectory())\n\trequire.NoError(t, generator.generateStubFile())\n\n\tstubPath := filepath.Join(generator.BuildDir, generator.BaseName+\".stub.php\")\n\tstubContent, err := os.ReadFile(stubPath)\n\trequire.NoError(t, err)\n\n\tstubStr := string(stubContent)\n\n\tassert.Contains(t, stubStr, \"* @cvalue\", \"Stub does not contain @cvalue annotation for iota constant\")\n\tassert.Contains(t, stubStr, \"const STATUS_OK = UNKNOWN;\", \"Stub does not contain STATUS_OK constant with UNKNOWN value\")\n\tassert.Contains(t, stubStr, \"const MAX_CONNECTIONS = 100;\", \"Stub does not contain MAX_CONNECTIONS constant with explicit value\")\n\n\trequire.NoError(t, generator.generateCFile())\n\n\tcPath := filepath.Join(generator.BuildDir, generator.BaseName+\".c\")\n\tcContent, err := os.ReadFile(cPath)\n\trequire.NoError(t, err)\n\n\tcStr := string(cContent)\n\n\tassert.Contains(t, cStr, `REGISTER_LONG_CONSTANT(\"STATUS_OK\", STATUS_OK, CONST_CS | CONST_PERSISTENT);`, \"C file does not contain STATUS_OK registration\")\n\tassert.Contains(t, cStr, `REGISTER_LONG_CONSTANT(\"MAX_CONNECTIONS\", 100, CONST_CS | CONST_PERSISTENT);`, \"C file does not contain MAX_CONNECTIONS registration\")\n}\n\nfunc TestConstantsIntegrationOctal(t *testing.T) {\n\ttmpDir := t.TempDir()\n\ttestFile := filepath.Join(tmpDir, \"test.go\")\n\n\tcontent := `package main\n\n//export_php:const\nconst FILE_PERM = 0o755\n\n//export_php:const\nconst OTHER_PERM = 0o644\n\n//export_php:const\nconst REGULAR_INT = 42\n\nfunc main() {}\n`\n\n\trequire.NoError(t, os.WriteFile(testFile, []byte(content), 0644))\n\n\tgenerator := &Generator{\n\t\tBaseName:   \"octalstest\",\n\t\tSourceFile: testFile,\n\t\tBuildDir:   filepath.Join(tmpDir, \"build\"),\n\t}\n\n\trequire.NoError(t, generator.parseSource())\n\tassert.Len(t, generator.Constants, 3, \"Expected 3 constants\")\n\n\t// Verify CValue conversion\n\tfor _, constant := range generator.Constants {\n\t\tswitch constant.Name {\n\t\tcase \"FILE_PERM\":\n\t\t\tassert.Equal(t, \"0o755\", constant.Value, \"FILE_PERM value mismatch\")\n\t\t\tassert.Equal(t, \"493\", constant.CValue(), \"FILE_PERM CValue mismatch\")\n\t\tcase \"OTHER_PERM\":\n\t\t\tassert.Equal(t, \"0o644\", constant.Value, \"OTHER_PERM value mismatch\")\n\t\t\tassert.Equal(t, \"420\", constant.CValue(), \"OTHER_PERM CValue mismatch\")\n\t\tcase \"REGULAR_INT\":\n\t\t\tassert.Equal(t, \"42\", constant.Value, \"REGULAR_INT value mismatch\")\n\t\t\tassert.Equal(t, \"42\", constant.CValue(), \"REGULAR_INT CValue mismatch\")\n\t\t}\n\t}\n\n\trequire.NoError(t, generator.setupBuildDirectory())\n\n\t// Test C file generation\n\trequire.NoError(t, generator.generateCFile())\n\n\tcPath := filepath.Join(generator.BuildDir, generator.BaseName+\".c\")\n\tcContent, err := os.ReadFile(cPath)\n\trequire.NoError(t, err)\n\n\tcStr := string(cContent)\n\n\t// Verify C file uses decimal values for octal constants\n\tassert.Contains(t, cStr, `REGISTER_LONG_CONSTANT(\"FILE_PERM\", 493, CONST_CS | CONST_PERSISTENT);`, \"C file does not contain FILE_PERM registration with decimal value 493\")\n\tassert.Contains(t, cStr, `REGISTER_LONG_CONSTANT(\"OTHER_PERM\", 420, CONST_CS | CONST_PERSISTENT);`, \"C file does not contain OTHER_PERM registration with decimal value 420\")\n\tassert.Contains(t, cStr, `REGISTER_LONG_CONSTANT(\"REGULAR_INT\", 42, CONST_CS | CONST_PERSISTENT);`, \"C file does not contain REGULAR_INT registration with value 42\")\n\n\t// Test header file generation\n\trequire.NoError(t, generator.generateHeaderFile())\n\n\thPath := filepath.Join(generator.BuildDir, generator.BaseName+\".h\")\n\thContent, err := os.ReadFile(hPath)\n\trequire.NoError(t, err)\n\n\thStr := string(hContent)\n\n\t// Verify header file uses decimal values for octal constants in #define\n\tassert.Contains(t, hStr, \"#define FILE_PERM 493\", \"Header file does not contain FILE_PERM #define with decimal value 493\")\n\tassert.Contains(t, hStr, \"#define OTHER_PERM 420\", \"Header file does not contain OTHER_PERM #define with decimal value 420\")\n\tassert.Contains(t, hStr, \"#define REGULAR_INT 42\", \"Header file does not contain REGULAR_INT #define with value 42\")\n}\n"
  },
  {
    "path": "internal/extgen/constparser.go",
    "content": "package extgen\n\nimport (\n\t\"bufio\"\n\t\"fmt\"\n\t\"os\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n)\n\nvar constRegex = regexp.MustCompile(`//\\s*export_php:const$`)\nvar classConstRegex = regexp.MustCompile(`//\\s*export_php:classconst\\s+(\\w+)$`)\nvar constDeclRegex = regexp.MustCompile(`const\\s+(\\w+)\\s*=\\s*(.+)`)\n\ntype ConstantParser struct{}\n\nfunc (cp *ConstantParser) parse(filename string) (constants []phpConstant, err error) {\n\tfile, err := os.Open(filename)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer func() {\n\t\te := file.Close()\n\t\tif err == nil {\n\t\t\terr = e\n\t\t}\n\t}()\n\n\tscanner := bufio.NewScanner(file)\n\n\tlineNumber := 0\n\texpectConstDecl := false\n\texpectClassConstDecl := false\n\tcurrentClassName := \"\"\n\tcurrentConstantValue := 0\n\tinConstBlock := false\n\texportAllInBlock := false\n\tlastConstValue := \"\"\n\tlastConstWasIota := false\n\n\tfor scanner.Scan() {\n\t\tlineNumber++\n\t\tline := strings.TrimSpace(scanner.Text())\n\n\t\tif constRegex.MatchString(line) {\n\t\t\texpectConstDecl = true\n\t\t\texpectClassConstDecl = false\n\t\t\tcurrentClassName = \"\"\n\n\t\t\tcontinue\n\t\t}\n\n\t\tif matches := classConstRegex.FindStringSubmatch(line); len(matches) == 2 {\n\t\t\texpectClassConstDecl = true\n\t\t\texpectConstDecl = false\n\t\t\tcurrentClassName = matches[1]\n\n\t\t\tcontinue\n\t\t}\n\n\t\tif strings.HasPrefix(line, \"const (\") {\n\t\t\tinConstBlock = true\n\t\t\tif expectConstDecl || expectClassConstDecl {\n\t\t\t\texportAllInBlock = true\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tif inConstBlock && line == \")\" {\n\t\t\tinConstBlock = false\n\t\t\texportAllInBlock = false\n\t\t\texpectConstDecl = false\n\t\t\texpectClassConstDecl = false\n\t\t\tcurrentClassName = \"\"\n\t\t\tlastConstValue = \"\"\n\t\t\tlastConstWasIota = false\n\t\t\tcontinue\n\t\t}\n\n\t\tif (expectConstDecl || expectClassConstDecl) && strings.HasPrefix(line, \"const \") && !inConstBlock {\n\t\t\tmatches := constDeclRegex.FindStringSubmatch(line)\n\t\t\tif len(matches) == 3 {\n\t\t\t\tname := matches[1]\n\t\t\t\tvalue := strings.TrimSpace(matches[2])\n\n\t\t\t\tconstant := phpConstant{\n\t\t\t\t\tName:       name,\n\t\t\t\t\tValue:      value,\n\t\t\t\t\tIsIota:     value == \"iota\",\n\t\t\t\t\tlineNumber: lineNumber,\n\t\t\t\t\tClassName:  currentClassName,\n\t\t\t\t}\n\n\t\t\t\tconstant.PhpType = determineConstantType(value)\n\n\t\t\t\tif constant.IsIota {\n\t\t\t\t\tconstant.Value = fmt.Sprintf(\"%d\", currentConstantValue)\n\t\t\t\t\tconstant.PhpType = phpInt\n\t\t\t\t\tcurrentConstantValue++\n\t\t\t\t\tlastConstWasIota = true\n\t\t\t\t\tlastConstValue = constant.Value\n\t\t\t\t}\n\n\t\t\t\tconstants = append(constants, constant)\n\t\t\t} else {\n\t\t\t\treturn nil, fmt.Errorf(\"invalid constant declaration at line %d: %s\", lineNumber, line)\n\t\t\t}\n\t\t\texpectConstDecl = false\n\t\t\texpectClassConstDecl = false\n\t\t} else if inConstBlock && (expectConstDecl || expectClassConstDecl || exportAllInBlock) {\n\t\t\tconstBlockDeclRegex := regexp.MustCompile(`^(\\w+)\\s*=\\s*(.+)$`)\n\t\t\tif matches := constBlockDeclRegex.FindStringSubmatch(line); len(matches) == 3 {\n\t\t\t\tname := matches[1]\n\t\t\t\tvalue := strings.TrimSpace(matches[2])\n\n\t\t\t\tconstant := phpConstant{\n\t\t\t\t\tName:       name,\n\t\t\t\t\tValue:      value,\n\t\t\t\t\tIsIota:     value == \"iota\",\n\t\t\t\t\tlineNumber: lineNumber,\n\t\t\t\t\tClassName:  currentClassName,\n\t\t\t\t}\n\n\t\t\t\tconstant.PhpType = determineConstantType(value)\n\n\t\t\t\tif constant.IsIota {\n\t\t\t\t\tconstant.Value = fmt.Sprintf(\"%d\", currentConstantValue)\n\t\t\t\t\tconstant.PhpType = phpInt\n\t\t\t\t\tcurrentConstantValue++\n\t\t\t\t\tlastConstWasIota = true\n\t\t\t\t\tlastConstValue = constant.Value\n\t\t\t\t} else {\n\t\t\t\t\tlastConstWasIota = false\n\t\t\t\t\tlastConstValue = value\n\t\t\t\t}\n\n\t\t\t\tconstants = append(constants, constant)\n\t\t\t\texpectConstDecl = false\n\t\t\t\texpectClassConstDecl = false\n\t\t\t} else {\n\t\t\t\tconstNameRegex := regexp.MustCompile(`^(\\w+)$`)\n\t\t\t\tif matches := constNameRegex.FindStringSubmatch(line); len(matches) == 2 {\n\t\t\t\t\tname := matches[1]\n\n\t\t\t\t\tconstant := phpConstant{\n\t\t\t\t\t\tName:       name,\n\t\t\t\t\t\tValue:      \"\",\n\t\t\t\t\t\tIsIota:     lastConstWasIota,\n\t\t\t\t\t\tlineNumber: lineNumber,\n\t\t\t\t\t\tClassName:  currentClassName,\n\t\t\t\t\t}\n\n\t\t\t\t\tif lastConstWasIota {\n\t\t\t\t\t\tconstant.Value = fmt.Sprintf(\"%d\", currentConstantValue)\n\t\t\t\t\t\tconstant.PhpType = phpInt\n\t\t\t\t\t\tcurrentConstantValue++\n\t\t\t\t\t\tlastConstValue = constant.Value\n\t\t\t\t\t} else {\n\t\t\t\t\t\tconstant.Value = lastConstValue\n\t\t\t\t\t\tconstant.PhpType = determineConstantType(lastConstValue)\n\t\t\t\t\t}\n\n\t\t\t\t\tconstants = append(constants, constant)\n\t\t\t\t\texpectConstDecl = false\n\t\t\t\t\texpectClassConstDecl = false\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (expectConstDecl || expectClassConstDecl) && !strings.HasPrefix(line, \"//\") && line != \"\" && !inConstBlock {\n\t\t\t// we expected a const declaration but found something else, reset\n\t\t\texpectConstDecl = false\n\t\t\texpectClassConstDecl = false\n\t\t\tcurrentClassName = \"\"\n\t\t}\n\t}\n\n\treturn constants, scanner.Err()\n}\n\n// determineConstantType analyzes the value and determines its type\nfunc determineConstantType(value string) phpType {\n\tvalue = strings.TrimSpace(value)\n\n\tif (strings.HasPrefix(value, `\"`) && strings.HasSuffix(value, `\"`)) ||\n\t\t(strings.HasPrefix(value, \"`\") && strings.HasSuffix(value, \"`\")) {\n\t\treturn phpString\n\t}\n\n\tif value == \"true\" || value == \"false\" {\n\t\treturn phpBool\n\t}\n\n\t// check for integer literals, including hex, octal, binary\n\tif _, err := strconv.ParseInt(value, 0, 64); err == nil {\n\t\treturn phpInt\n\t}\n\n\tif _, err := strconv.ParseFloat(value, 64); err == nil {\n\t\treturn phpFloat\n\t}\n\n\treturn phpInt\n}\n"
  },
  {
    "path": "internal/extgen/constparser_test.go",
    "content": "package extgen\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestConstantParser(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    string\n\t\texpected int\n\t}{\n\t\t{\n\t\t\tname: \"single constant\",\n\t\t\tinput: `package main\n\n//export_php:const\nconst MyConstant = \"test_value\"`,\n\t\t\texpected: 1,\n\t\t},\n\t\t{\n\t\t\tname: \"multiple constants\",\n\t\t\tinput: `package main\n\n//export_php:const\nconst FirstConstant = \"first\"\n\n//export_php:const\nconst SecondConstant = 42\n\n//export_php:const\nconst ThirdConstant = true`,\n\t\t\texpected: 3,\n\t\t},\n\t\t{\n\t\t\tname: \"iota constant\",\n\t\t\tinput: `package main\n\n//export_php:const\nconst IotaConstant = iota`,\n\t\t\texpected: 1,\n\t\t},\n\t\t{\n\t\t\tname: \"mixed constants and iota\",\n\t\t\tinput: `package main\n\n//export_php:const\nconst StringConst = \"hello\"\n\n//export_php:const\nconst IotaConst = iota\n\n//export_php:const\nconst IntConst = 123`,\n\t\t\texpected: 3,\n\t\t},\n\t\t{\n\t\t\tname: \"no php constants\",\n\t\t\tinput: `package main\n\nconst RegularConstant = \"not exported\"\n\nfunc someFunction() {\n\t// Just regular code\n}`,\n\t\t\texpected: 0,\n\t\t},\n\t\t{\n\t\t\tname: \"constant with complex value\",\n\t\t\tinput: `package main\n\n//export_php:const\nconst ComplexConstant = \"string with spaces and symbols !@#$%\"`,\n\t\t\texpected: 1,\n\t\t},\n\t\t{\n\t\t\tname: \"directive without constant\",\n\t\t\tinput: `package main\n\n//export_php:const\nvar notAConstant = \"this is a variable\"`,\n\t\t\texpected: 0,\n\t\t},\n\t\t{\n\t\t\tname: \"mixed export and non-export constants\",\n\t\t\tinput: `package main\n\nconst RegularConst = \"regular\"\n\n//export_php:const\nconst ExportedConst = \"exported\"\n\nconst AnotherRegular = 456\n\n//export_php:const\nconst AnotherExported = 789`,\n\t\t\texpected: 2,\n\t\t},\n\t\t{\n\t\t\tname: \"numeric constants\",\n\t\t\tinput: `package main\n\n//export_php:const\nconst IntConstant = 42\n\n//export_php:const\nconst FloatConstant = 3.14\n\n//export_php:const\nconst HexConstant = 0xFF`,\n\t\t\texpected: 3,\n\t\t},\n\t\t{\n\t\t\tname: \"boolean constants\",\n\t\t\tinput: `package main\n\n//export_php:const\nconst TrueConstant = true\n\n//export_php:const\nconst FalseConstant = false`,\n\t\t\texpected: 2,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ttmpDir := t.TempDir()\n\t\t\ttmpFile := filepath.Join(tmpDir, tt.name+\".go\")\n\t\t\trequire.NoError(t, os.WriteFile(tmpFile, []byte(tt.input), 0644))\n\n\t\t\tparser := &ConstantParser{}\n\t\t\tconstants, err := parser.parse(tmpFile)\n\t\t\tassert.NoError(t, err, \"parse() error\")\n\n\t\t\tassert.Len(t, constants, tt.expected, \"parse() got wrong number of constants\")\n\n\t\t\tif tt.name == \"single constant\" && len(constants) > 0 {\n\t\t\t\tc := constants[0]\n\t\t\t\tassert.Equal(t, \"MyConstant\", c.Name, \"Expected constant name 'MyConstant'\")\n\t\t\t\tassert.Equal(t, `\"test_value\"`, c.Value, `Expected constant value '\"test_value\"'`)\n\t\t\t\tassert.Equal(t, phpString, c.PhpType, \"Expected constant type 'string'\")\n\t\t\t\tassert.False(t, c.IsIota, \"Expected isIota to be false for string constant\")\n\t\t\t}\n\n\t\t\tif tt.name == \"iota constant\" && len(constants) > 0 {\n\t\t\t\tc := constants[0]\n\t\t\t\tassert.Equal(t, \"IotaConstant\", c.Name, \"Expected constant name 'IotaConstant'\")\n\t\t\t\tassert.True(t, c.IsIota, \"Expected isIota to be true\")\n\t\t\t\tassert.Equal(t, \"0\", c.Value, \"Expected iota constant value to be '0'\")\n\t\t\t}\n\n\t\t\tif tt.name == \"multiple constants\" && len(constants) == 3 {\n\t\t\t\texpectedNames := []string{\"FirstConstant\", \"SecondConstant\", \"ThirdConstant\"}\n\t\t\t\texpectedValues := []string{`\"first\"`, \"42\", \"true\"}\n\t\t\t\texpectedTypes := []phpType{phpString, phpInt, phpBool}\n\n\t\t\t\tfor i, c := range constants {\n\t\t\t\t\tassert.Equal(t, expectedNames[i], c.Name, \"Expected constant name '%s'\", expectedNames[i])\n\t\t\t\t\tassert.Equal(t, expectedValues[i], c.Value, \"Expected constant value '%s'\", expectedValues[i])\n\t\t\t\t\tassert.Equal(t, expectedTypes[i], c.PhpType, \"Expected constant type '%s'\", expectedTypes[i])\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestConstantParserErrors(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\tinput       string\n\t\texpectError bool\n\t}{\n\t\t{\n\t\t\tname: \"invalid constant declaration\",\n\t\t\tinput: `package main\n\n//export_php:const\nconst = \"missing name\"`,\n\t\t\texpectError: true,\n\t\t},\n\t\t{\n\t\t\tname: \"malformed constant\",\n\t\t\tinput: `package main\n\n//export_php:const\nconst InvalidSyntax`,\n\t\t\texpectError: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ttmpDir := t.TempDir()\n\t\t\ttmpFile := filepath.Join(tmpDir, tt.name+\".go\")\n\t\t\trequire.NoError(t, os.WriteFile(tmpFile, []byte(tt.input), 0644))\n\n\t\t\tparser := &ConstantParser{}\n\t\t\t_, err := parser.parse(tmpFile)\n\t\t\trequire.NotNil(t, err)\n\n\t\t\tif tt.expectError {\n\t\t\t\tassert.Error(t, err, \"Expected error but got none\")\n\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.NoError(t, err)\n\t\t})\n\t}\n}\n\nfunc TestConstantParserIotaSequence(t *testing.T) {\n\tinput := `package main\n\n//export_php:const\nconst FirstIota = iota\n\n//export_php:const\nconst SecondIota = iota\n\n//export_php:const\nconst ThirdIota = iota`\n\n\ttmpDir := t.TempDir()\n\tfileName := filepath.Join(tmpDir, \"test.go\")\n\trequire.NoError(t, os.WriteFile(fileName, []byte(input), 0644))\n\n\tparser := &ConstantParser{}\n\tconstants, err := parser.parse(fileName)\n\tassert.NoError(t, err, \"parse() error\")\n\n\tassert.Len(t, constants, 3, \"Expected 3 constants\")\n\n\texpectedValues := []string{\"0\", \"1\", \"2\"}\n\tfor i, c := range constants {\n\t\tassert.True(t, c.IsIota, \"Expected constant %d to be iota\", i)\n\t\tassert.Equal(t, expectedValues[i], c.Value, \"Expected constant %d value to be '%s'\", i, expectedValues[i])\n\t}\n}\n\nfunc TestConstantParserConstBlock(t *testing.T) {\n\tinput := `package main\n\nconst (\n\t// export_php:const\n\tSTATUS_PENDING = iota\n\n\t// export_php:const\n\tSTATUS_PROCESSING\n\n\t// export_php:const\n\tSTATUS_COMPLETED\n)`\n\n\ttmpDir := t.TempDir()\n\tfileName := filepath.Join(tmpDir, \"test.go\")\n\trequire.NoError(t, os.WriteFile(fileName, []byte(input), 0644))\n\n\tparser := &ConstantParser{}\n\tconstants, err := parser.parse(fileName)\n\tassert.NoError(t, err, \"parse() error\")\n\n\tassert.Len(t, constants, 3, \"Expected 3 constants\")\n\n\texpectedNames := []string{\"STATUS_PENDING\", \"STATUS_PROCESSING\", \"STATUS_COMPLETED\"}\n\texpectedValues := []string{\"0\", \"1\", \"2\"}\n\n\tfor i, c := range constants {\n\t\tassert.Equal(t, expectedNames[i], c.Name, \"Expected constant %d name to be '%s'\", i, expectedNames[i])\n\t\tassert.True(t, c.IsIota, \"Expected constant %d to be iota\", i)\n\t\tassert.Equal(t, expectedValues[i], c.Value, \"Expected constant %d value to be '%s'\", i, expectedValues[i])\n\t\tassert.Equal(t, phpInt, c.PhpType, \"Expected constant %d to be phpInt type\", i)\n\t}\n}\n\nfunc TestConstantParserConstBlockWithBlockLevelDirective(t *testing.T) {\n\tinput := `package main\n\n// export_php:const\nconst (\n\tSTATUS_PENDING = iota\n\tSTATUS_PROCESSING\n\tSTATUS_COMPLETED\n)`\n\n\ttmpDir := t.TempDir()\n\tfileName := filepath.Join(tmpDir, \"test.go\")\n\trequire.NoError(t, os.WriteFile(fileName, []byte(input), 0644))\n\n\tparser := &ConstantParser{}\n\tconstants, err := parser.parse(fileName)\n\tassert.NoError(t, err, \"parse() error\")\n\n\tassert.Len(t, constants, 3, \"Expected 3 constants\")\n\n\texpectedNames := []string{\"STATUS_PENDING\", \"STATUS_PROCESSING\", \"STATUS_COMPLETED\"}\n\texpectedValues := []string{\"0\", \"1\", \"2\"}\n\n\tfor i, c := range constants {\n\t\tassert.Equal(t, expectedNames[i], c.Name, \"Expected constant %d name to be '%s'\", i, expectedNames[i])\n\t\tassert.True(t, c.IsIota, \"Expected constant %d to be iota\", i)\n\t\tassert.Equal(t, expectedValues[i], c.Value, \"Expected constant %d value to be '%s'\", i, expectedValues[i])\n\t\tassert.Equal(t, phpInt, c.PhpType, \"Expected constant %d to be phpInt type\", i)\n\t}\n}\n\nfunc TestConstantParserMixedConstBlockAndIndividual(t *testing.T) {\n\tinput := `package main\n\n// export_php:const\nconst INDIVIDUAL = 42\n\nconst (\n\t// export_php:const\n\tBLOCK_ONE = iota\n\n\t// export_php:const\n\tBLOCK_TWO\n)\n\n// export_php:const\nconst ANOTHER_INDIVIDUAL = \"test\"`\n\n\ttmpDir := t.TempDir()\n\tfileName := filepath.Join(tmpDir, \"test.go\")\n\trequire.NoError(t, os.WriteFile(fileName, []byte(input), 0644))\n\n\tparser := &ConstantParser{}\n\tconstants, err := parser.parse(fileName)\n\tassert.NoError(t, err, \"parse() error\")\n\n\tassert.Len(t, constants, 4, \"Expected 4 constants\")\n\n\tassert.Equal(t, \"INDIVIDUAL\", constants[0].Name)\n\tassert.Equal(t, \"42\", constants[0].Value)\n\tassert.Equal(t, phpInt, constants[0].PhpType)\n\n\tassert.Equal(t, \"BLOCK_ONE\", constants[1].Name)\n\tassert.Equal(t, \"0\", constants[1].Value)\n\tassert.True(t, constants[1].IsIota)\n\n\tassert.Equal(t, \"BLOCK_TWO\", constants[2].Name)\n\tassert.Equal(t, \"1\", constants[2].Value)\n\tassert.True(t, constants[2].IsIota)\n\n\tassert.Equal(t, \"ANOTHER_INDIVIDUAL\", constants[3].Name)\n\tassert.Equal(t, `\"test\"`, constants[3].Value)\n\tassert.Equal(t, phpString, constants[3].PhpType)\n}\n\nfunc TestConstantParserClassConstBlock(t *testing.T) {\n\tinput := `package main\n\n// export_php:classconst Config\nconst (\n\tMODE_DEBUG = 1\n\tMODE_PRODUCTION = 2\n\tMODE_TEST = 3\n)`\n\n\ttmpDir := t.TempDir()\n\tfileName := filepath.Join(tmpDir, \"test.go\")\n\trequire.NoError(t, os.WriteFile(fileName, []byte(input), 0644))\n\n\tparser := &ConstantParser{}\n\tconstants, err := parser.parse(fileName)\n\tassert.NoError(t, err, \"parse() error\")\n\n\tassert.Len(t, constants, 3, \"Expected 3 class constants\")\n\n\texpectedNames := []string{\"MODE_DEBUG\", \"MODE_PRODUCTION\", \"MODE_TEST\"}\n\texpectedValues := []string{\"1\", \"2\", \"3\"}\n\n\tfor i, c := range constants {\n\t\tassert.Equal(t, expectedNames[i], c.Name, \"Expected constant %d name to be '%s'\", i, expectedNames[i])\n\t\tassert.Equal(t, \"Config\", c.ClassName, \"Expected constant %d to belong to Config class\", i)\n\t\tassert.Equal(t, expectedValues[i], c.Value, \"Expected constant %d value to be '%s'\", i, expectedValues[i])\n\t\tassert.Equal(t, phpInt, c.PhpType, \"Expected constant %d to be phpInt type\", i)\n\t}\n}\n\nfunc TestConstantParserClassConstBlockWithIota(t *testing.T) {\n\tinput := `package main\n\n// export_php:classconst Status\nconst (\n\tSTATUS_PENDING = iota\n\tSTATUS_ACTIVE\n\tSTATUS_COMPLETED\n)`\n\n\ttmpDir := t.TempDir()\n\tfileName := filepath.Join(tmpDir, \"test.go\")\n\trequire.NoError(t, os.WriteFile(fileName, []byte(input), 0644))\n\n\tparser := &ConstantParser{}\n\tconstants, err := parser.parse(fileName)\n\tassert.NoError(t, err, \"parse() error\")\n\n\tassert.Len(t, constants, 3, \"Expected 3 class constants\")\n\n\texpectedNames := []string{\"STATUS_PENDING\", \"STATUS_ACTIVE\", \"STATUS_COMPLETED\"}\n\texpectedValues := []string{\"0\", \"1\", \"2\"}\n\n\tfor i, c := range constants {\n\t\tassert.Equal(t, expectedNames[i], c.Name, \"Expected constant %d name to be '%s'\", i, expectedNames[i])\n\t\tassert.Equal(t, \"Status\", c.ClassName, \"Expected constant %d to belong to Status class\", i)\n\t\tassert.True(t, c.IsIota, \"Expected constant %d to be iota\", i)\n\t\tassert.Equal(t, expectedValues[i], c.Value, \"Expected constant %d value to be '%s'\", i, expectedValues[i])\n\t\tassert.Equal(t, phpInt, c.PhpType, \"Expected constant %d to be phpInt type\", i)\n\t}\n}\n\nfunc TestConstantParserTypeDetection(t *testing.T) {\n\ttests := []struct {\n\t\tname         string\n\t\tvalue        string\n\t\texpectedType phpType\n\t}{\n\t\t{\"string with double quotes\", `\"hello world\"`, phpString},\n\t\t{\"string with backticks\", \"`hello world`\", phpString},\n\t\t{\"boolean true\", \"true\", phpBool},\n\t\t{\"boolean false\", \"false\", phpBool},\n\t\t{\"integer\", \"42\", phpInt},\n\t\t{\"negative integer\", \"-42\", phpInt},\n\t\t{\"hex integer\", \"0xFF\", phpInt},\n\t\t{\"octal integer\", \"0755\", phpInt},\n\t\t{\"go octal integer\", \"0o755\", phpInt},\n\t\t{\"binary integer\", \"0b1010\", phpInt},\n\t\t{\"float\", \"3.14\", phpFloat},\n\t\t{\"negative float\", \"-3.14\", phpFloat},\n\t\t{\"scientific notation\", \"1e10\", phpFloat},\n\t\t{\"unknown type\", \"someFunction()\", phpInt},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := determineConstantType(tt.value)\n\t\t\tassert.Equal(t, tt.expectedType, result, \"determineConstantType(%s) expected %s\", tt.value, tt.expectedType)\n\t\t})\n\t}\n}\n\nfunc TestConstantParserClassConstants(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    string\n\t\texpected int\n\t}{\n\t\t{\n\t\t\tname: \"single class constant\",\n\t\t\tinput: `package main\n\n//export_php:classconst MyClass\nconst STATUS_ACTIVE = 1`,\n\t\t\texpected: 1,\n\t\t},\n\t\t{\n\t\t\tname: \"multiple class constants\",\n\t\t\tinput: `package main\n\n//export_php:classconst User\nconst STATUS_ACTIVE = \"active\"\n\n//export_php:classconst User\nconst STATUS_INACTIVE = \"inactive\"\n\n//export_php:classconst Order\nconst STATE_PENDING = 0`,\n\t\t\texpected: 3,\n\t\t},\n\t\t{\n\t\t\tname: \"mixed global and class constants\",\n\t\t\tinput: `package main\n\n//export_php:const\nconst GLOBAL_CONST = \"global\"\n\n//export_php:classconst MyClass\nconst CLASS_CONST = 42\n\n//export_php:const\nconst ANOTHER_GLOBAL = true`,\n\t\t\texpected: 3,\n\t\t},\n\t\t{\n\t\t\tname: \"class constant with iota\",\n\t\t\tinput: `package main\n\n//export_php:classconst Status\nconst FIRST = iota\n\n//export_php:classconst Status\nconst SECOND = iota`,\n\t\t\texpected: 2,\n\t\t},\n\t\t{\n\t\t\tname: \"invalid class constant directive\",\n\t\t\tinput: `package main\n\n//export_php:classconst\nconst INVALID = \"missing class name\"`,\n\t\t\texpected: 0,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ttmpDir := t.TempDir()\n\t\t\ttmpFile := filepath.Join(tmpDir, tt.name+\".go\")\n\t\t\trequire.NoError(t, os.WriteFile(tmpFile, []byte(tt.input), 0644))\n\n\t\t\tparser := &ConstantParser{}\n\t\t\tconstants, err := parser.parse(tmpFile)\n\t\t\tassert.NoError(t, err, \"parse() error\")\n\n\t\t\tassert.Len(t, constants, tt.expected, \"parse() got wrong number of constants\")\n\n\t\t\tif tt.name == \"single class constant\" && len(constants) > 0 {\n\t\t\t\tc := constants[0]\n\t\t\t\tassert.Equal(t, \"STATUS_ACTIVE\", c.Name, \"Expected constant name 'STATUS_ACTIVE'\")\n\t\t\t\tassert.Equal(t, \"MyClass\", c.ClassName, \"Expected class name 'MyClass'\")\n\t\t\t\tassert.Equal(t, \"1\", c.Value, \"Expected constant value '1'\")\n\t\t\t\tassert.Equal(t, phpInt, c.PhpType, \"Expected constant type 'int'\")\n\t\t\t}\n\n\t\t\tif tt.name == \"multiple class constants\" && len(constants) == 3 {\n\t\t\t\texpectedClasses := []string{\"User\", \"User\", \"Order\"}\n\t\t\t\texpectedNames := []string{\"STATUS_ACTIVE\", \"STATUS_INACTIVE\", \"STATE_PENDING\"}\n\t\t\t\texpectedValues := []string{`\"active\"`, `\"inactive\"`, \"0\"}\n\n\t\t\t\tfor i, c := range constants {\n\t\t\t\t\tassert.Equal(t, expectedClasses[i], c.ClassName, \"Expected class name '%s'\", expectedClasses[i])\n\t\t\t\t\tassert.Equal(t, expectedNames[i], c.Name, \"Expected constant name '%s'\", expectedNames[i])\n\t\t\t\t\tassert.Equal(t, expectedValues[i], c.Value, \"Expected constant value '%s'\", expectedValues[i])\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif tt.name == \"mixed global and class constants\" && len(constants) == 3 {\n\t\t\t\tassert.Empty(t, constants[0].ClassName, \"First constant should be global\")\n\t\t\t\tassert.Equal(t, \"MyClass\", constants[1].ClassName, \"Second constant should belong to MyClass\")\n\t\t\t\tassert.Empty(t, constants[2].ClassName, \"Third constant should be global\")\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestConstantParserRegexMatch(t *testing.T) {\n\ttestCases := []struct {\n\t\tline     string\n\t\texpected bool\n\t}{\n\t\t{\"//export_php:const\", true},\n\t\t{\"// export_php:const\", true},\n\t\t{\"//  export_php:const\", true},\n\t\t{\"//export_php:const \", false}, // should not match with trailing content\n\t\t{\"//export_php\", false},\n\t\t{\"//export_php:function\", false},\n\t\t{\"//export_php:class\", false},\n\t\t{\"// some other comment\", false},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.line, func(t *testing.T) {\n\t\t\tmatches := constRegex.MatchString(tc.line)\n\t\t\tassert.Equal(t, tc.expected, matches, \"Expected regex match for line '%s'\", tc.line)\n\t\t})\n\t}\n}\n\nfunc TestConstantParserClassConstRegex(t *testing.T) {\n\ttestCases := []struct {\n\t\tline        string\n\t\tshouldMatch bool\n\t\tclassName   string\n\t}{\n\t\t{\"//export_php:classconst MyClass\", true, \"MyClass\"},\n\t\t{\"// export_php:classconst User\", true, \"User\"},\n\t\t{\"//  export_php:classconst  Status\", true, \"Status\"},\n\t\t{\"//export_php:classconst Order123\", true, \"Order123\"},\n\t\t{\"//export_php:classconst\", false, \"\"},\n\t\t{\"//export_php:classconst \", false, \"\"},\n\t\t{\"//export_php:classconst MyClass extra\", false, \"\"},\n\t\t{\"//export_php:const\", false, \"\"},\n\t\t{\"//export_php:function\", false, \"\"},\n\t\t{\"// some other comment\", false, \"\"},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.line, func(t *testing.T) {\n\t\t\tmatches := classConstRegex.FindStringSubmatch(tc.line)\n\n\t\t\tif tc.shouldMatch {\n\t\t\t\tassert.Len(t, matches, 2, \"Expected 2 matches for line '%s'\", tc.line)\n\t\t\t\tif len(matches) != 2 {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tassert.Equal(t, tc.className, matches[1], \"Expected class name '%s'\", tc.className)\n\t\t\t} else {\n\t\t\t\tassert.Empty(t, matches, \"Expected no matches for line '%s'\", tc.line)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestConstantParserDeclRegex(t *testing.T) {\n\ttestCases := []struct {\n\t\tline        string\n\t\tshouldMatch bool\n\t\tname        string\n\t\tvalue       string\n\t}{\n\t\t{`const MyConst = \"value\"`, true, \"MyConst\", `\"value\"`},\n\t\t{\"const IntConst = 42\", true, \"IntConst\", \"42\"},\n\t\t{\"const BoolConst = true\", true, \"BoolConst\", \"true\"},\n\t\t{\"const IotaConst = iota\", true, \"IotaConst\", \"iota\"},\n\t\t{\"const ComplexValue = someFunction()\", true, \"ComplexValue\", \"someFunction()\"},\n\t\t{`const SpacedName = \"with spaces\"`, true, \"SpacedName\", `\"with spaces\"`},\n\t\t{`var notAConst = \"value\"`, false, \"\", \"\"},\n\t\t{\"const\", false, \"\", \"\"},\n\t\t{\"const =\", false, \"\", \"\"},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.line, func(t *testing.T) {\n\t\t\tmatches := constDeclRegex.FindStringSubmatch(tc.line)\n\n\t\t\tif tc.shouldMatch {\n\t\t\t\tassert.Len(t, matches, 3, \"Expected 3 matches for line '%s'\", tc.line)\n\t\t\t\tif len(matches) != 3 {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tassert.Equal(t, tc.name, matches[1], \"Expected name '%s'\", tc.name)\n\t\t\t\tassert.Equal(t, tc.value, matches[2], \"Expected value '%s'\", tc.value)\n\t\t\t} else {\n\t\t\t\tassert.Empty(t, matches, \"Expected no matches for line '%s'\", tc.line)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestPHPConstantCValue(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tconstant phpConstant\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname: \"octal notation 0o35\",\n\t\t\tconstant: phpConstant{\n\t\t\t\tName:    \"OctalConst\",\n\t\t\t\tValue:   \"0o35\",\n\t\t\t\tPhpType: phpInt,\n\t\t\t},\n\t\t\texpected: \"29\", // 0o35 = 29 in decimal\n\t\t},\n\t\t{\n\t\t\tname: \"octal notation 0o755\",\n\t\t\tconstant: phpConstant{\n\t\t\t\tName:    \"OctalPerm\",\n\t\t\t\tValue:   \"0o755\",\n\t\t\t\tPhpType: phpInt,\n\t\t\t},\n\t\t\texpected: \"493\", // 0o755 = 493 in decimal\n\t\t},\n\t\t{\n\t\t\tname: \"regular integer\",\n\t\t\tconstant: phpConstant{\n\t\t\t\tName:    \"RegularInt\",\n\t\t\t\tValue:   \"42\",\n\t\t\t\tPhpType: phpInt,\n\t\t\t},\n\t\t\texpected: \"42\",\n\t\t},\n\t\t{\n\t\t\tname: \"hex integer\",\n\t\t\tconstant: phpConstant{\n\t\t\t\tName:    \"HexInt\",\n\t\t\t\tValue:   \"0xFF\",\n\t\t\t\tPhpType: phpInt,\n\t\t\t},\n\t\t\texpected: \"0xFF\", // hex should remain unchanged\n\t\t},\n\t\t{\n\t\t\tname: \"string constant\",\n\t\t\tconstant: phpConstant{\n\t\t\t\tName:    \"StringConst\",\n\t\t\t\tValue:   `\"hello\"`,\n\t\t\t\tPhpType: phpString,\n\t\t\t},\n\t\t\texpected: `\"hello\"`, // strings should remain unchanged\n\t\t},\n\t\t{\n\t\t\tname: \"boolean constant\",\n\t\t\tconstant: phpConstant{\n\t\t\t\tName:    \"BoolConst\",\n\t\t\t\tValue:   \"true\",\n\t\t\t\tPhpType: phpBool,\n\t\t\t},\n\t\t\texpected: \"true\", // booleans should remain unchanged\n\t\t},\n\t\t{\n\t\t\tname: \"float constant\",\n\t\t\tconstant: phpConstant{\n\t\t\t\tName:    \"FloatConst\",\n\t\t\t\tValue:   \"3.14\",\n\t\t\t\tPhpType: phpFloat,\n\t\t\t},\n\t\t\texpected: \"3.14\", // floats should remain unchanged\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := tt.constant.CValue()\n\t\t\tassert.Equal(t, tt.expected, result, \"CValue() expected %s\", tt.expected)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/extgen/docs.go",
    "content": "package extgen\n\nimport (\n\t\"bytes\"\n\t_ \"embed\"\n\t\"path/filepath\"\n\t\"text/template\"\n)\n\n//go:embed templates/README.md.tpl\nvar docFileContent string\n\ntype DocumentationGenerator struct {\n\tgenerator *Generator\n}\n\ntype DocTemplateData struct {\n\tBaseName  string\n\tFunctions []phpFunction\n\tClasses   []phpClass\n}\n\nfunc (dg *DocumentationGenerator) generate() error {\n\tfilename := filepath.Join(dg.generator.BuildDir, \"README.md\")\n\tcontent, err := dg.generateMarkdown()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn writeFile(filename, content)\n}\n\nfunc (dg *DocumentationGenerator) generateMarkdown() (string, error) {\n\ttmpl := template.Must(template.New(\"readme\").Parse(docFileContent))\n\n\tvar buf bytes.Buffer\n\tif err := tmpl.Execute(&buf, DocTemplateData{\n\t\tBaseName:  dg.generator.BaseName,\n\t\tFunctions: dg.generator.Functions,\n\t\tClasses:   dg.generator.Classes,\n\t}); err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn buf.String(), nil\n}\n"
  },
  {
    "path": "internal/extgen/docs_test.go",
    "content": "package extgen\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestDocumentationGenerator_Generate(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\tgenerator   *Generator\n\t\texpectError bool\n\t}{\n\t\t{\n\t\t\tname: \"simple extension with functions\",\n\t\t\tgenerator: &Generator{\n\t\t\t\tBaseName: \"testextension\",\n\t\t\t\tBuildDir: \"\",\n\t\t\t\tFunctions: []phpFunction{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:       \"greet\",\n\t\t\t\t\t\tReturnType: phpString,\n\t\t\t\t\t\tParams: []phpParameter{\n\t\t\t\t\t\t\t{Name: \"name\", PhpType: phpString},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tSignature: \"greet(string $name): string\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tClasses: []phpClass{},\n\t\t\t},\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname: \"extension with classes\",\n\t\t\tgenerator: &Generator{\n\t\t\t\tBaseName:  \"classextension\",\n\t\t\t\tBuildDir:  \"\",\n\t\t\t\tFunctions: []phpFunction{},\n\t\t\t\tClasses: []phpClass{\n\t\t\t\t\t{\n\t\t\t\t\t\tName: \"TestClass\",\n\t\t\t\t\t\tProperties: []phpClassProperty{\n\t\t\t\t\t\t\t{Name: \"name\", PhpType: phpString},\n\t\t\t\t\t\t\t{Name: \"count\", PhpType: phpInt, IsNullable: true},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname: \"extension with both functions and classes\",\n\t\t\tgenerator: &Generator{\n\t\t\t\tBaseName: \"fullextension\",\n\t\t\t\tBuildDir: \"\",\n\t\t\t\tFunctions: []phpFunction{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:             \"calculate\",\n\t\t\t\t\t\tReturnType:       phpInt,\n\t\t\t\t\t\tIsReturnNullable: true,\n\t\t\t\t\t\tParams: []phpParameter{\n\t\t\t\t\t\t\t{Name: \"base\", PhpType: phpInt},\n\t\t\t\t\t\t\t{Name: \"multiplier\", PhpType: phpInt, HasDefault: true, DefaultValue: \"2\", IsNullable: true},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tSignature: \"calculate(int $base, ?int $multiplier = 2): ?int\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tClasses: []phpClass{\n\t\t\t\t\t{\n\t\t\t\t\t\tName: \"Calculator\",\n\t\t\t\t\t\tProperties: []phpClassProperty{\n\t\t\t\t\t\t\t{Name: \"precision\", PhpType: phpInt},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname: \"empty extension\",\n\t\t\tgenerator: &Generator{\n\t\t\t\tBaseName:  \"emptyextension\",\n\t\t\t\tBuildDir:  \"\",\n\t\t\t\tFunctions: []phpFunction{},\n\t\t\t\tClasses:   []phpClass{},\n\t\t\t},\n\t\t\texpectError: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ttempDir := t.TempDir()\n\t\t\ttt.generator.BuildDir = tempDir\n\n\t\t\tdocGen := &DocumentationGenerator{\n\t\t\t\tgenerator: tt.generator,\n\t\t\t}\n\n\t\t\terr := docGen.generate()\n\n\t\t\tif tt.expectError {\n\t\t\t\tassert.Error(t, err, \"generate() expected error but got none\")\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.NoError(t, err, \"generate() unexpected error\")\n\n\t\t\treadmePath := filepath.Join(tempDir, \"README.md\")\n\t\t\trequire.FileExists(t, readmePath)\n\n\t\t\tcontent, err := os.ReadFile(readmePath)\n\t\t\trequire.NoError(t, err, \"Failed to read generated README.md\")\n\n\t\t\tcontentStr := string(content)\n\n\t\t\tassert.Contains(t, contentStr, \"# \"+tt.generator.BaseName+\" Extension\", \"README should contain extension title\")\n\t\t\tassert.Contains(t, contentStr, \"Auto-generated PHP extension from Go code.\", \"README should contain description\")\n\n\t\t\tif len(tt.generator.Functions) > 0 {\n\t\t\t\tassert.Contains(t, contentStr, \"## Functions\", \"README should contain functions section when functions exist\")\n\n\t\t\t\tfor _, fn := range tt.generator.Functions {\n\t\t\t\t\tassert.Contains(t, contentStr, \"### \"+fn.Name, \"README should contain function %s\", fn.Name)\n\t\t\t\t\tassert.Contains(t, contentStr, fn.Signature, \"README should contain function signature for %s\", fn.Name)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif len(tt.generator.Classes) > 0 {\n\t\t\t\tassert.Contains(t, contentStr, \"## Classes\", \"README should contain classes section when classes exist\")\n\n\t\t\t\tfor _, class := range tt.generator.Classes {\n\t\t\t\t\tassert.Contains(t, contentStr, \"### \"+class.Name, \"README should contain class %s\", class.Name)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDocumentationGenerator_GenerateMarkdown(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\tgenerator   *Generator\n\t\tcontains    []string\n\t\tnotContains []string\n\t}{\n\t\t{\n\t\t\tname: \"function with parameters\",\n\t\t\tgenerator: &Generator{\n\t\t\t\tBaseName: \"testextension\",\n\t\t\t\tFunctions: []phpFunction{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:       \"processData\",\n\t\t\t\t\t\tReturnType: phpArray,\n\t\t\t\t\t\tParams: []phpParameter{\n\t\t\t\t\t\t\t{Name: \"data\", PhpType: phpString},\n\t\t\t\t\t\t\t{Name: \"options\", PhpType: phpArray, IsNullable: true},\n\t\t\t\t\t\t\t{Name: \"count\", PhpType: phpInt, HasDefault: true, DefaultValue: \"10\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tSignature: \"processData(string $data, ?array $options, int $count = 10): array\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tClasses: []phpClass{},\n\t\t\t},\n\t\t\tcontains: []string{\n\t\t\t\t\"# testextension Extension\",\n\t\t\t\t\"## Functions\",\n\t\t\t\t\"### processData\",\n\t\t\t\t\"**Parameters:**\",\n\t\t\t\t\"- `data` (string)\",\n\t\t\t\t\"- `options` (array) (nullable)\",\n\t\t\t\t\"- `count` (int) (default: 10)\",\n\t\t\t\t\"**Returns:** array\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"nullable return type\",\n\t\t\tgenerator: &Generator{\n\t\t\t\tBaseName: \"nullableext\",\n\t\t\t\tFunctions: []phpFunction{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:             \"maybeGetValue\",\n\t\t\t\t\t\tReturnType:       phpString,\n\t\t\t\t\t\tIsReturnNullable: true,\n\t\t\t\t\t\tParams:           []phpParameter{},\n\t\t\t\t\t\tSignature:        \"maybeGetValue(): ?string\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tClasses: []phpClass{},\n\t\t\t},\n\t\t\tcontains: []string{\n\t\t\t\t\"**Returns:** string (nullable)\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"class with properties\",\n\t\t\tgenerator: &Generator{\n\t\t\t\tBaseName:  \"classext\",\n\t\t\t\tFunctions: []phpFunction{},\n\t\t\t\tClasses: []phpClass{\n\t\t\t\t\t{\n\t\t\t\t\t\tName: \"DataProcessor\",\n\t\t\t\t\t\tProperties: []phpClassProperty{\n\t\t\t\t\t\t\t{Name: \"name\", PhpType: phpString},\n\t\t\t\t\t\t\t{Name: \"config\", PhpType: phpArray, IsNullable: true},\n\t\t\t\t\t\t\t{Name: \"enabled\", PhpType: phpBool},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tcontains: []string{\n\t\t\t\t\"## Classes\",\n\t\t\t\t\"### DataProcessor\",\n\t\t\t\t\"**Properties:**\",\n\t\t\t\t\"- `name`: string\",\n\t\t\t\t\"- `config`: array (nullable)\",\n\t\t\t\t\"- `enabled`: bool\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"extension with no functions or classes\",\n\t\t\tgenerator: &Generator{\n\t\t\t\tBaseName:  \"emptyext\",\n\t\t\t\tFunctions: []phpFunction{},\n\t\t\t\tClasses:   []phpClass{},\n\t\t\t},\n\t\t\tcontains: []string{\n\t\t\t\t\"# emptyext Extension\",\n\t\t\t\t\"Auto-generated PHP extension from Go code.\",\n\t\t\t},\n\t\t\tnotContains: []string{\n\t\t\t\t\"## Functions\",\n\t\t\t\t\"## Classes\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"function with no parameters\",\n\t\t\tgenerator: &Generator{\n\t\t\t\tBaseName: \"noparamext\",\n\t\t\t\tFunctions: []phpFunction{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:       \"getCurrentTime\",\n\t\t\t\t\t\tReturnType: phpInt,\n\t\t\t\t\t\tParams:     []phpParameter{},\n\t\t\t\t\t\tSignature:  \"getCurrentTime(): int\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tClasses: []phpClass{},\n\t\t\t},\n\t\t\tcontains: []string{\n\t\t\t\t\"### getCurrentTime\",\n\t\t\t\t\"**Returns:** int\",\n\t\t\t},\n\t\t\tnotContains: []string{\n\t\t\t\t\"**Parameters:**\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"class with no properties\",\n\t\t\tgenerator: &Generator{\n\t\t\t\tBaseName:  \"nopropsext\",\n\t\t\t\tFunctions: []phpFunction{},\n\t\t\t\tClasses: []phpClass{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:       \"EmptyClass\",\n\t\t\t\t\t\tProperties: []phpClassProperty{},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tcontains: []string{\n\t\t\t\t\"### EmptyClass\",\n\t\t\t},\n\t\t\tnotContains: []string{\n\t\t\t\t\"**Properties:**\",\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tdocGen := &DocumentationGenerator{\n\t\t\t\tgenerator: tt.generator,\n\t\t\t}\n\n\t\t\tresult, err := docGen.generateMarkdown()\n\t\t\tif !assert.NoError(t, err, \"generateMarkdown() unexpected error\") {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tfor _, expected := range tt.contains {\n\t\t\t\tassert.Contains(t, result, expected, \"generateMarkdown() should contain '%s'\", expected)\n\t\t\t}\n\n\t\t\tfor _, notExpected := range tt.notContains {\n\t\t\t\tassert.NotContains(t, result, notExpected, \"generateMarkdown() should NOT contain '%s'\", notExpected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDocumentationGenerator_Generate_InvalidDirectory(t *testing.T) {\n\tgenerator := &Generator{\n\t\tBaseName:  \"test\",\n\t\tBuildDir:  \"/nonexistent/directory\",\n\t\tFunctions: []phpFunction{},\n\t\tClasses:   []phpClass{},\n\t}\n\n\tdocGen := &DocumentationGenerator{\n\t\tgenerator: generator,\n\t}\n\n\terr := docGen.generate()\n\tassert.Error(t, err, \"generate() expected error for invalid directory but got none\")\n}\n\nfunc TestDocumentationGenerator_TemplateError(t *testing.T) {\n\tgenerator := &Generator{\n\t\tBaseName: \"test\",\n\t\tFunctions: []phpFunction{\n\t\t\t{\n\t\t\t\tName:       \"test\",\n\t\t\t\tReturnType: phpString,\n\t\t\t\tSignature:  \"test(): string\",\n\t\t\t},\n\t\t},\n\t\tClasses: []phpClass{},\n\t}\n\n\tdocGen := &DocumentationGenerator{\n\t\tgenerator: generator,\n\t}\n\n\tresult, err := docGen.generateMarkdown()\n\tassert.NoError(t, err, \"generateMarkdown() unexpected error\")\n\tassert.NotEmpty(t, result, \"generateMarkdown() returned empty result\")\n}\n\nfunc BenchmarkDocumentationGenerator_GenerateMarkdown(b *testing.B) {\n\tgenerator := &Generator{\n\t\tBaseName: \"benchext\",\n\t\tFunctions: []phpFunction{\n\t\t\t{\n\t\t\t\tName:       \"function1\",\n\t\t\t\tReturnType: phpString,\n\t\t\t\tParams: []phpParameter{\n\t\t\t\t\t{Name: \"param1\", PhpType: phpString},\n\t\t\t\t\t{Name: \"param2\", PhpType: phpInt, HasDefault: true, DefaultValue: \"0\"},\n\t\t\t\t},\n\t\t\t\tSignature: \"function1(string $param1, int $param2 = 0): string\",\n\t\t\t},\n\t\t\t{\n\t\t\t\tName:             \"function2\",\n\t\t\t\tReturnType:       phpArray,\n\t\t\t\tIsReturnNullable: true,\n\t\t\t\tParams: []phpParameter{\n\t\t\t\t\t{Name: \"data\", PhpType: phpArray, IsNullable: true},\n\t\t\t\t},\n\t\t\t\tSignature: \"function2(?array $data): ?array\",\n\t\t\t},\n\t\t},\n\t\tClasses: []phpClass{\n\t\t\t{\n\t\t\t\tName: \"TestClass\",\n\t\t\t\tProperties: []phpClassProperty{\n\t\t\t\t\t{Name: \"prop1\", PhpType: phpString},\n\t\t\t\t\t{Name: \"prop2\", PhpType: phpInt, IsNullable: true},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tdocGen := &DocumentationGenerator{\n\t\tgenerator: generator,\n\t}\n\n\tfor b.Loop() {\n\t\t_, err := docGen.generateMarkdown()\n\t\tassert.NoError(b, err)\n\t}\n}\n"
  },
  {
    "path": "internal/extgen/errors.go",
    "content": "package extgen\n\nimport \"fmt\"\n\ntype GeneratorError struct {\n\tStage   string\n\tMessage string\n\tErr     error\n}\n\nfunc (e *GeneratorError) Error() string {\n\tif e.Err == nil {\n\t\treturn fmt.Sprintf(\"generator error at %s: %s\", e.Stage, e.Message)\n\t}\n\n\treturn fmt.Sprintf(\"generator error at %s: %s: %v\", e.Stage, e.Message, e.Err)\n}\n"
  },
  {
    "path": "internal/extgen/funcparser.go",
    "content": "package extgen\n\nimport (\n\t\"bufio\"\n\t\"fmt\"\n\t\"os\"\n\t\"regexp\"\n\t\"strings\"\n)\n\nvar phpFuncRegex = regexp.MustCompile(`//\\s*export_php:function\\s+([^{}\\n]+)(?:\\s*{\\s*})?`)\nvar signatureRegex = regexp.MustCompile(`(\\w+)\\s*\\(([^)]*)\\)\\s*:\\s*(\\??[\\w|]+)`)\nvar typeNameRegex = regexp.MustCompile(`(\\??[\\w|]+)\\s+\\$?(\\w+)`)\n\ntype FuncParser struct{}\n\nfunc (fp *FuncParser) parse(filename string) (functions []phpFunction, err error) {\n\tfile, err := os.Open(filename)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer func() {\n\t\te := file.Close()\n\t\tif err == nil {\n\t\t\terr = e\n\t\t}\n\t}()\n\n\tscanner := bufio.NewScanner(file)\n\tvar currentPHPFunc *phpFunction\n\tvalidator := Validator{}\n\n\tlineNumber := 0\n\tfor scanner.Scan() {\n\t\tlineNumber++\n\t\tline := strings.TrimSpace(scanner.Text())\n\n\t\tif matches := phpFuncRegex.FindStringSubmatch(line); matches != nil {\n\t\t\tsignature := strings.TrimSpace(matches[1])\n\t\t\tphpFunc, err := fp.parseSignature(signature)\n\t\t\tif err != nil {\n\t\t\t\tfmt.Printf(\"Warning: Error parsing signature '%s': %v\\n\", signature, err)\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif err := validator.validateFunction(*phpFunc); err != nil {\n\t\t\t\tfmt.Printf(\"Warning: Invalid function '%s': %v\\n\", phpFunc.Name, err)\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif err := validator.validateTypes(*phpFunc); err != nil {\n\t\t\t\tfmt.Printf(\"Warning: Function '%s' uses unsupported types: %v\\n\", phpFunc.Name, err)\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tphpFunc.lineNumber = lineNumber\n\t\t\tcurrentPHPFunc = phpFunc\n\t\t}\n\n\t\tif currentPHPFunc != nil && strings.HasPrefix(line, \"func \") {\n\t\t\tgoFunc, err := fp.extractGoFunction(scanner, line)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"extracting Go function: %w\", err)\n\t\t\t}\n\n\t\t\tcurrentPHPFunc.GoFunction = goFunc\n\n\t\t\tif err := validator.validateGoFunctionSignatureWithOptions(*currentPHPFunc, false); err != nil {\n\t\t\t\tfmt.Printf(\"Warning: Go function signature mismatch for %q: %v\\n\", currentPHPFunc.Name, err)\n\t\t\t\tcurrentPHPFunc = nil\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfunctions = append(functions, *currentPHPFunc)\n\t\t\tcurrentPHPFunc = nil\n\t\t}\n\t}\n\n\tif currentPHPFunc != nil {\n\t\treturn nil, fmt.Errorf(\"//export_php function directive at line %d is not followed by a function declaration\", currentPHPFunc.lineNumber)\n\t}\n\n\treturn functions, scanner.Err()\n}\n\nfunc (fp *FuncParser) extractGoFunction(scanner *bufio.Scanner, firstLine string) (string, error) {\n\tgoFunc := firstLine + \"\\n\"\n\tbraceCount := 1\n\n\tfor scanner.Scan() {\n\t\tline := scanner.Text()\n\t\tgoFunc += line + \"\\n\"\n\n\t\tfor _, char := range line {\n\t\t\tswitch char {\n\t\t\tcase '{':\n\t\t\t\tbraceCount++\n\t\t\tcase '}':\n\t\t\t\tbraceCount--\n\t\t\t}\n\t\t}\n\n\t\tif braceCount == 0 {\n\t\t\tbreak\n\t\t}\n\t}\n\n\treturn goFunc, nil\n}\n\nfunc (fp *FuncParser) parseSignature(signature string) (*phpFunction, error) {\n\tmatches := signatureRegex.FindStringSubmatch(signature)\n\n\tif len(matches) != 4 {\n\t\treturn nil, fmt.Errorf(\"invalid signature format\")\n\t}\n\n\tname := matches[1]\n\tparamsStr := strings.TrimSpace(matches[2])\n\treturnTypeStr := strings.TrimSpace(matches[3])\n\n\tisReturnNullable := strings.HasPrefix(returnTypeStr, \"?\")\n\treturnType := strings.TrimPrefix(returnTypeStr, \"?\")\n\n\tvar params []phpParameter\n\tif paramsStr != \"\" {\n\t\tparamParts := strings.SplitSeq(paramsStr, \",\")\n\t\tfor part := range paramParts {\n\t\t\tparam, err := fp.parseParameter(strings.TrimSpace(part))\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"parsing parameter '%s': %w\", part, err)\n\t\t\t}\n\t\t\tparams = append(params, param)\n\t\t}\n\t}\n\n\treturn &phpFunction{\n\t\tName:             name,\n\t\tSignature:        signature,\n\t\tParams:           params,\n\t\tReturnType:       phpType(returnType),\n\t\tIsReturnNullable: isReturnNullable,\n\t}, nil\n}\n\nfunc (fp *FuncParser) parseParameter(paramStr string) (phpParameter, error) {\n\tparts := strings.Split(paramStr, \"=\")\n\ttypePart := strings.TrimSpace(parts[0])\n\n\tparam := phpParameter{HasDefault: len(parts) > 1}\n\n\tif param.HasDefault {\n\t\tparam.DefaultValue = fp.sanitizeDefaultValue(strings.TrimSpace(parts[1]))\n\t}\n\n\tmatches := typeNameRegex.FindStringSubmatch(typePart)\n\n\tif len(matches) < 3 {\n\t\treturn phpParameter{}, fmt.Errorf(\"invalid parameter format: %s\", paramStr)\n\t}\n\n\ttypeStr := strings.TrimSpace(matches[1])\n\tparam.Name = strings.TrimSpace(matches[2])\n\tparam.IsNullable = strings.HasPrefix(typeStr, \"?\")\n\tparam.PhpType = phpType(strings.TrimPrefix(typeStr, \"?\"))\n\n\treturn param, nil\n}\n\nfunc (fp *FuncParser) sanitizeDefaultValue(value string) string {\n\tif strings.HasPrefix(value, \"[\") && strings.HasSuffix(value, \"]\") {\n\t\treturn value\n\t}\n\tif strings.ToLower(value) == \"null\" {\n\t\treturn \"null\"\n\t}\n\n\treturn strings.Trim(value, `'\"`)\n}\n"
  },
  {
    "path": "internal/extgen/funcparser_test.go",
    "content": "package extgen\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestFunctionParser(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    string\n\t\texpected int\n\t}{\n\t\t{\n\t\t\tname: \"single function\",\n\t\t\tinput: `package main\n\n//export_php:function testFunc(string $name): string\nfunc testFunc(name *C.zend_string) unsafe.Pointer {\n\treturn String(\"Hello \" + CStringToGoString(name))\n}`,\n\t\t\texpected: 1,\n\t\t},\n\t\t{\n\t\t\tname: \"multiple functions\",\n\t\t\tinput: `package main\n\n//export_php:function func1(int $a): int\nfunc func1(a int64) int64 {\n\treturn a * 2\n}\n\n//export_php:function func2(string $b): string  \nfunc func2(b *C.zend_string) unsafe.Pointer {\n\treturn String(\"processed: \" + CStringToGoString(b))\n}`,\n\t\t\texpected: 2,\n\t\t},\n\t\t{\n\t\t\tname: \"no php functions\",\n\t\t\tinput: `package main\n\nfunc regularFunc() {\n\t// Just a regular Go function\n}`,\n\t\t\texpected: 0,\n\t\t},\n\t\t{\n\t\t\tname: \"mixed functions\",\n\t\t\tinput: `package main\n\n//export_php:function phpFunc(string $data): string\nfunc phpFunc(data *C.zend_string) unsafe.Pointer {\n\treturn String(\"PHP: \" + CStringToGoString(data))\n}\n\nfunc internalFunc() {\n\t// Internal function without export_php comment\n}\n\n//export_php:function anotherPhpFunc(int $num): int\nfunc anotherPhpFunc(num int64) int64 {\n\treturn num * 10\n}`,\n\t\t\texpected: 2,\n\t\t},\n\t\t{\n\t\t\tname: \"wrong args syntax\",\n\t\t\tinput: `package main\n\n//export_php function phpFunc(data string): string\nfunc phpFunc(data *C.zend_string) unsafe.Pointer {\n\treturn String(\"PHP: \" + CStringToGoString(data))\n}`,\n\t\t\texpected: 0,\n\t\t},\n\t\t{\n\t\t\tname: \"decoupled function names\",\n\t\t\tinput: `package main\n\n//export_php:function my_php_function(string $name): string\nfunc myGoFunction(name *C.zend_string) unsafe.Pointer {\n\treturn String(\"Hello \" + CStringToGoString(name))\n}\n\n//export_php:function another_php_func(int $num): int\nfunc someOtherGoName(num int64) int64 {\n\treturn num * 5\n}`,\n\t\t\texpected: 2,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ttmpDir := t.TempDir()\n\t\t\tfileName := filepath.Join(tmpDir, tt.name+\".go\")\n\t\t\trequire.NoError(t, os.WriteFile(fileName, []byte(tt.input), 0644))\n\n\t\t\tparser := &FuncParser{}\n\t\t\tfunctions, err := parser.parse(fileName)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Len(t, functions, tt.expected, \"parse() got wrong number of functions\")\n\n\t\t\tif tt.name == \"single function\" && len(functions) > 0 {\n\t\t\t\tfn := functions[0]\n\t\t\t\tassert.Equal(t, \"testFunc\", fn.Name, \"Expected function name 'testFunc'\")\n\t\t\t\tassert.Equal(t, phpString, fn.ReturnType, \"Expected return type 'string'\")\n\t\t\t\tassert.Len(t, fn.Params, 1, \"Expected 1 parameter\")\n\t\t\t\tif len(fn.Params) > 0 {\n\t\t\t\t\tassert.Equal(t, \"name\", fn.Params[0].Name, \"Expected parameter name 'name'\")\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif tt.name == \"decoupled function names\" && len(functions) >= 2 {\n\t\t\t\tfn1 := functions[0]\n\t\t\t\tassert.Equal(t, \"my_php_function\", fn1.Name, \"Expected PHP function name 'my_php_function'\")\n\t\t\t\tfn2 := functions[1]\n\t\t\t\tassert.Equal(t, \"another_php_func\", fn2.Name, \"Expected PHP function name 'another_php_func'\")\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSignatureParsing(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\tsignature   string\n\t\texpectError bool\n\t\tfuncName    string\n\t\tparamCount  int\n\t\treturnType  phpType\n\t\tnullable    bool\n\t}{\n\t\t{\n\t\t\tname:       \"simple function\",\n\t\t\tsignature:  \"test(name string): string\",\n\t\t\tfuncName:   \"test\",\n\t\t\tparamCount: 1,\n\t\t\treturnType: phpString,\n\t\t\tnullable:   false,\n\t\t},\n\t\t{\n\t\t\tname:       \"nullable return\",\n\t\t\tsignature:  \"test(id int): ?string\",\n\t\t\tfuncName:   \"test\",\n\t\t\tparamCount: 1,\n\t\t\treturnType: phpString,\n\t\t\tnullable:   true,\n\t\t},\n\t\t{\n\t\t\tname:       \"multiple params\",\n\t\t\tsignature:  \"calculate(a int, b float, name string): float\",\n\t\t\tfuncName:   \"calculate\",\n\t\t\tparamCount: 3,\n\t\t\treturnType: phpFloat,\n\t\t\tnullable:   false,\n\t\t},\n\t\t{\n\t\t\tname:       \"no parameters\",\n\t\t\tsignature:  \"getValue(): int\",\n\t\t\tfuncName:   \"getValue\",\n\t\t\tparamCount: 0,\n\t\t\treturnType: phpInt,\n\t\t\tnullable:   false,\n\t\t},\n\t\t{\n\t\t\tname:       \"nullable parameters\",\n\t\t\tsignature:  \"process(?string data, ?int count): bool\",\n\t\t\tfuncName:   \"process\",\n\t\t\tparamCount: 2,\n\t\t\treturnType: phpBool,\n\t\t\tnullable:   false,\n\t\t},\n\t\t{\n\t\t\tname:        \"invalid signature\",\n\t\t\tsignature:   \"invalid syntax here\",\n\t\t\texpectError: true,\n\t\t},\n\t\t{\n\t\t\tname:        \"missing return type\",\n\t\t\tsignature:   \"test(name string)\",\n\t\t\texpectError: true,\n\t\t},\n\t}\n\n\tparser := &FuncParser{}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tfn, err := parser.parseSignature(tt.signature)\n\n\t\t\tif tt.expectError {\n\t\t\t\tassert.Error(t, err, \"parseSignature() expected error but got none\")\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.NoError(t, err, \"parseSignature() unexpected error\")\n\t\t\tassert.Equal(t, tt.funcName, fn.Name, \"parseSignature() name mismatch\")\n\t\t\tassert.Len(t, fn.Params, tt.paramCount, \"parseSignature() param count mismatch\")\n\t\t\tassert.Equal(t, tt.returnType, fn.ReturnType, \"parseSignature() return type mismatch\")\n\t\t\tassert.Equal(t, tt.nullable, fn.IsReturnNullable, \"parseSignature() nullable mismatch\")\n\n\t\t\tif tt.name == \"nullable parameters\" {\n\t\t\t\tif len(fn.Params) >= 2 {\n\t\t\t\t\tassert.True(t, fn.Params[0].IsNullable, \"First parameter should be nullable\")\n\t\t\t\t\tassert.True(t, fn.Params[1].IsNullable, \"Second parameter should be nullable\")\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestParameterParsing(t *testing.T) {\n\ttests := []struct {\n\t\tname             string\n\t\tparamStr         string\n\t\texpectedName     string\n\t\texpectedType     phpType\n\t\texpectedNullable bool\n\t\texpectedDefault  string\n\t\thasDefault       bool\n\t\texpectError      bool\n\t}{\n\t\t{\n\t\t\tname:         \"simple string param\",\n\t\t\tparamStr:     \"string name\",\n\t\t\texpectedName: \"name\",\n\t\t\texpectedType: phpString,\n\t\t},\n\t\t{\n\t\t\tname:             \"nullable int param\",\n\t\t\tparamStr:         \"?int count\",\n\t\t\texpectedName:     \"count\",\n\t\t\texpectedType:     phpInt,\n\t\t\texpectedNullable: true,\n\t\t},\n\t\t{\n\t\t\tname:            \"param with default\",\n\t\t\tparamStr:        \"string message = 'hello'\",\n\t\t\texpectedName:    \"message\",\n\t\t\texpectedType:    phpString,\n\t\t\texpectedDefault: \"hello\",\n\t\t\thasDefault:      true,\n\t\t},\n\t\t{\n\t\t\tname:            \"int with default\",\n\t\t\tparamStr:        \"int limit = 10\",\n\t\t\texpectedName:    \"limit\",\n\t\t\texpectedType:    phpInt,\n\t\t\texpectedDefault: \"10\",\n\t\t\thasDefault:      true,\n\t\t},\n\t\t{\n\t\t\tname:             \"nullable with default\",\n\t\t\tparamStr:         \"?string data = null\",\n\t\t\texpectedName:     \"data\",\n\t\t\texpectedType:     phpString,\n\t\t\texpectedNullable: true,\n\t\t\texpectedDefault:  \"null\",\n\t\t\thasDefault:       true,\n\t\t},\n\t\t{\n\t\t\tname:        \"invalid format\",\n\t\t\tparamStr:    \"invalid\",\n\t\t\texpectError: true,\n\t\t},\n\t}\n\n\tparser := &FuncParser{}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tparam, err := parser.parseParameter(tt.paramStr)\n\n\t\t\tif tt.expectError {\n\t\t\t\tassert.Error(t, err, \"parseParameter() expected error but got none\")\n\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.NoError(t, err, \"parseParameter() unexpected error\")\n\t\t\tassert.Equal(t, tt.expectedName, param.Name, \"parseParameter() name mismatch\")\n\t\t\tassert.Equal(t, tt.expectedType, param.PhpType, \"parseParameter() type mismatch\")\n\t\t\tassert.Equal(t, tt.expectedNullable, param.IsNullable, \"parseParameter() nullable mismatch\")\n\t\t\tassert.Equal(t, tt.hasDefault, param.HasDefault, \"parseParameter() hasDefault mismatch\")\n\n\t\t\tif tt.hasDefault {\n\t\t\t\tassert.Equal(t, tt.expectedDefault, param.DefaultValue, \"parseParameter() defaultValue mismatch\")\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestFunctionParserUnsupportedTypes(t *testing.T) {\n\ttests := []struct {\n\t\tname       string\n\t\tinput      string\n\t\texpected   int\n\t\thasWarning bool\n\t}{\n\t\t{\n\t\t\tname: \"function with array parameter should be rejected\",\n\t\t\tinput: `package main\n\n//export_php:function arrayFunc(array $data): string\nfunc arrayFunc(data any) unsafe.Pointer {\n\treturn String(\"processed\")\n}`,\n\t\t\texpected:   0,\n\t\t\thasWarning: true,\n\t\t},\n\t\t{\n\t\t\tname: \"function with object parameter should be rejected\",\n\t\t\tinput: `package main\n\n//export_php:function objectFunc(object $obj): string\nfunc objectFunc(obj any) unsafe.Pointer {\n\treturn String(\"processed\")\n}`,\n\t\t\texpected:   0,\n\t\t\thasWarning: true,\n\t\t},\n\t\t{\n\t\t\tname: \"function with mixed parameter should be rejected\",\n\t\t\tinput: `package main\n\n//export_php:function mixedFunc(mixed $value): string\nfunc mixedFunc(value any) unsafe.Pointer {\n\treturn String(\"processed\")\n}`,\n\t\t\texpected:   0,\n\t\t\thasWarning: true,\n\t\t},\n\t\t{\n\t\t\tname: \"function with array return type should be rejected\",\n\t\t\tinput: `package main\n\n//export_php:function arrayReturnFunc(string $name): array\nfunc arrayReturnFunc(name *C.zend_string) any {\n\treturn []string{\"result\"}\n}`,\n\t\t\texpected:   0,\n\t\t\thasWarning: true,\n\t\t},\n\t\t{\n\t\t\tname: \"function with object return type should be rejected\",\n\t\t\tinput: `package main\n\n//export_php:function objectReturnFunc(string $name): object\nfunc objectReturnFunc(name *C.zend_string) any {\n\treturn map[string]any{\"key\": \"value\"}\n}`,\n\t\t\texpected:   0,\n\t\t\thasWarning: true,\n\t\t},\n\t\t{\n\t\t\tname: \"valid scalar types should pass\",\n\t\t\tinput: `package main\n\n//export_php:function validFunc(string $name, int $count, float $rate, bool $active): string\nfunc validFunc(name *C.zend_string, count int64, rate float64, active bool) unsafe.Pointer {\n\treturn nil\n}`,\n\t\t\texpected:   1,\n\t\t\thasWarning: false,\n\t\t},\n\t\t{\n\t\t\tname: \"valid void return should pass\",\n\t\t\tinput: `package main\n\n//export_php:function voidFunc(string $message): void\nfunc voidFunc(message *C.zend_string) {\n\t// Do something\n}`,\n\t\t\texpected:   1,\n\t\t\thasWarning: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ttmpDir := t.TempDir()\n\t\t\ttmpFile := filepath.Join(tmpDir, tt.name+\".go\")\n\t\t\trequire.NoError(t, os.WriteFile(tmpFile, []byte(tt.input), 0644))\n\n\t\t\tparser := &FuncParser{}\n\t\t\tfunctions, err := parser.parse(tmpFile)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Len(t, functions, tt.expected, \"parse() got wrong number of functions\")\n\t\t})\n\t}\n}\n\nfunc TestFunctionParserGoTypeMismatch(t *testing.T) {\n\ttests := []struct {\n\t\tname       string\n\t\tinput      string\n\t\texpected   int\n\t\thasWarning bool\n\t}{\n\t\t{\n\t\t\tname: \"parameter count mismatch should be rejected\",\n\t\t\tinput: `package main\n\n//export_php:function countMismatch(string $name, int $count): string\nfunc countMismatch(name *C.zend_string) unsafe.Pointer {\n\treturn nil\n}`,\n\t\t\texpected:   0,\n\t\t\thasWarning: true,\n\t\t},\n\t\t{\n\t\t\tname: \"parameter type mismatch should be rejected\",\n\t\t\tinput: `package main\n\n//export_php:function typeMismatch(string $name, int $count): string\nfunc typeMismatch(name *C.zend_string, count string) unsafe.Pointer {\n\treturn nil\n}`,\n\t\t\texpected:   0,\n\t\t\thasWarning: true,\n\t\t},\n\t\t{\n\t\t\tname: \"return type mismatch should be rejected\",\n\t\t\tinput: `package main\n\n//export_php:function returnMismatch(string $name): int\nfunc returnMismatch(name *C.zend_string) string {\n\treturn \"\"\n}`,\n\t\t\texpected:   0,\n\t\t\thasWarning: true,\n\t\t},\n\t\t{\n\t\t\tname: \"valid matching types should pass\",\n\t\t\tinput: `package main\n\n//export_php:function validMatch(string $name, int $count): string\nfunc validMatch(name *C.zend_string, count int64) unsafe.Pointer {\n\treturn nil\n}`,\n\t\t\texpected:   1,\n\t\t\thasWarning: false,\n\t\t},\n\t\t{\n\t\t\tname: \"valid bool types should pass\",\n\t\t\tinput: `package main\n\n//export_php:function validBool(bool $flag): bool\nfunc validBool(flag bool) bool {\n\treturn flag\n}`,\n\t\t\texpected:   1,\n\t\t\thasWarning: false,\n\t\t},\n\t\t{\n\t\t\tname: \"valid float types should pass\",\n\t\t\tinput: `package main\n\n//export_php:function validFloat(float $value): float\nfunc validFloat(value float64) float64 {\n\treturn value\n}`,\n\t\t\texpected:   1,\n\t\t\thasWarning: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ttmpDir := t.TempDir()\n\t\t\tfileName := filepath.Join(tmpDir, tt.name+\".go\")\n\t\t\trequire.NoError(t, os.WriteFile(fileName, []byte(tt.input), 0644))\n\n\t\t\tparser := &FuncParser{}\n\t\t\tfunctions, err := parser.parse(fileName)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Len(t, functions, tt.expected, \"parse() got wrong number of functions\")\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/extgen/generator.go",
    "content": "package extgen\n\nimport (\n\t\"fmt\"\n\t\"os\"\n)\n\ntype Generator struct {\n\tBaseName   string\n\tSourceFile string\n\tBuildDir   string\n\tFunctions  []phpFunction\n\tClasses    []phpClass\n\tConstants  []phpConstant\n\tNamespace  string\n}\n\n// EXPERIMENTAL\nfunc (g *Generator) Generate() error {\n\tif err := g.setupBuildDirectory(); err != nil {\n\t\treturn fmt.Errorf(\"setup build directory: %w\", err)\n\t}\n\tif err := g.parseSource(); err != nil {\n\t\treturn fmt.Errorf(\"parse source: %w\", err)\n\t}\n\n\tif len(g.Functions) == 0 && len(g.Classes) == 0 && len(g.Constants) == 0 {\n\t\treturn fmt.Errorf(\"no PHP functions, classes, or constants found in source file\")\n\t}\n\n\tgenerators := []struct {\n\t\tname string\n\t\tfn   func() error\n\t}{\n\t\t{\"stub file\", g.generateStubFile},\n\t\t{\"arginfo\", g.generateArginfo},\n\t\t{\"header file\", g.generateHeaderFile},\n\t\t{\"C file\", g.generateCFile},\n\t\t{\"Go file\", g.generateGoFile},\n\t\t{\"documentation\", g.generateDocumentation},\n\t}\n\n\tfor _, gen := range generators {\n\t\tif err := gen.fn(); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (g *Generator) setupBuildDirectory() error {\n\treturn os.MkdirAll(g.BuildDir, 0755)\n}\n\nfunc (g *Generator) parseSource() error {\n\tparser := SourceParser{}\n\n\tfunctions, err := parser.ParseFunctions(g.SourceFile)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"parsing functions: %w\", err)\n\t}\n\tg.Functions = functions\n\n\tclasses, err := parser.ParseClasses(g.SourceFile)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"parsing classes: %w\", err)\n\t}\n\tg.Classes = classes\n\n\tconstants, err := parser.ParseConstants(g.SourceFile)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"parsing constants: %w\", err)\n\t}\n\tg.Constants = constants\n\n\tns, err := parser.ParseNamespace(g.SourceFile)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"parsing namespace: %w\", err)\n\t}\n\tg.Namespace = ns\n\n\treturn nil\n}\n\nfunc (g *Generator) generateStubFile() error {\n\tgenerator := StubGenerator{g}\n\tif err := generator.generate(); err != nil {\n\t\treturn &GeneratorError{\"stub generation\", \"failed to generate stub file\", err}\n\t}\n\n\treturn nil\n}\n\nfunc (g *Generator) generateArginfo() error {\n\tgenerator := arginfoGenerator{generator: g}\n\tif err := generator.generate(); err != nil {\n\t\treturn &GeneratorError{\"arginfo generation\", \"failed to generate arginfo\", err}\n\t}\n\n\treturn nil\n}\n\nfunc (g *Generator) generateHeaderFile() error {\n\tgenerator := HeaderGenerator{g}\n\tif err := generator.generate(); err != nil {\n\t\treturn &GeneratorError{\"header generation\", \"failed to generate header file\", err}\n\t}\n\n\treturn nil\n}\n\nfunc (g *Generator) generateCFile() error {\n\tgenerator := cFileGenerator{g}\n\tif err := generator.generate(); err != nil {\n\t\treturn &GeneratorError{\"C file generation\", \"failed to generate C file\", err}\n\t}\n\n\treturn nil\n}\n\nfunc (g *Generator) generateGoFile() error {\n\tgenerator := GoFileGenerator{g}\n\tif err := generator.generate(); err != nil {\n\t\treturn &GeneratorError{\"Go file generation\", \"failed to generate Go file\", err}\n\t}\n\n\treturn nil\n}\n\nfunc (g *Generator) generateDocumentation() error {\n\tdocGen := DocumentationGenerator{g}\n\tif err := docGen.generate(); err != nil {\n\t\treturn &GeneratorError{\"documentation generation\", \"failed to generate documentation\", err}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/extgen/gofile.go",
    "content": "package extgen\n\nimport (\n\t\"bytes\"\n\t_ \"embed\"\n\t\"fmt\"\n\t\"go/format\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"text/template\"\n\n\t\"github.com/Masterminds/sprig/v3\"\n)\n\n//go:embed templates/extension.go.tpl\nvar goFileContent string\n\ntype GoFileGenerator struct {\n\tgenerator *Generator\n}\n\ntype goTemplateData struct {\n\tPackageName       string\n\tBaseName          string\n\tSanitizedBaseName string\n\tConstants         []phpConstant\n\tVariables         []string\n\tInternalFunctions []string\n\tFunctions         []phpFunction\n\tClasses           []phpClass\n}\n\nfunc (gg *GoFileGenerator) generate() error {\n\tfilename := filepath.Join(gg.generator.BuildDir, gg.generator.BaseName+\"_generated.go\")\n\n\tcontent, err := gg.buildContent()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"building Go file content: %w\", err)\n\t}\n\n\treturn writeFile(filename, content)\n}\n\nfunc (gg *GoFileGenerator) buildContent() (string, error) {\n\tsourceAnalyzer := SourceAnalyzer{}\n\tpackageName, variables, internalFunctions, err := sourceAnalyzer.analyze(gg.generator.SourceFile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"analyzing source file: %w\", err)\n\t}\n\n\tclasses := make([]phpClass, len(gg.generator.Classes))\n\tcopy(classes, gg.generator.Classes)\n\n\ttemplateContent, err := gg.getTemplateContent(goTemplateData{\n\t\tPackageName:       packageName,\n\t\tBaseName:          gg.generator.BaseName,\n\t\tSanitizedBaseName: SanitizePackageName(gg.generator.BaseName),\n\t\tConstants:         gg.generator.Constants,\n\t\tVariables:         variables,\n\t\tInternalFunctions: internalFunctions,\n\t\tFunctions:         gg.generator.Functions,\n\t\tClasses:           classes,\n\t})\n\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"executing template: %w\", err)\n\t}\n\n\tfc, err := format.Source([]byte(templateContent))\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"formatting source: %w\", err)\n\t}\n\n\treturn string(fc), nil\n}\n\nfunc (gg *GoFileGenerator) getTemplateContent(data goTemplateData) (string, error) {\n\tfuncMap := sprig.FuncMap()\n\tfuncMap[\"phpTypeToGoType\"] = gg.phpTypeToGoType\n\tfuncMap[\"isStringOrArray\"] = func(t phpType) bool {\n\t\treturn t == phpString || t == phpArray\n\t}\n\tfuncMap[\"isVoid\"] = func(t phpType) bool {\n\t\treturn t == phpVoid\n\t}\n\tfuncMap[\"extractGoFunctionName\"] = extractGoFunctionName\n\tfuncMap[\"extractGoFunctionSignatureParams\"] = extractGoFunctionSignatureParams\n\tfuncMap[\"extractGoFunctionSignatureReturn\"] = extractGoFunctionSignatureReturn\n\tfuncMap[\"extractGoFunctionCallParams\"] = extractGoFunctionCallParams\n\n\ttmpl := template.Must(template.New(\"gofile\").Funcs(funcMap).Parse(goFileContent))\n\n\tvar buf bytes.Buffer\n\tif err := tmpl.Execute(&buf, data); err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn buf.String(), nil\n}\n\ntype GoMethodSignature struct {\n\tMethodName string\n\tParams     []GoParameter\n\tReturnType string\n}\n\ntype GoParameter struct {\n\tName string\n\tType string\n}\n\nvar phpToGoTypeMap = map[phpType]string{\n\tphpString:   \"string\",\n\tphpInt:      \"int64\",\n\tphpFloat:    \"float64\",\n\tphpBool:     \"bool\",\n\tphpArray:    \"*frankenphp.Array\",\n\tphpMixed:    \"any\",\n\tphpVoid:     \"\",\n\tphpCallable: \"*C.zval\",\n}\n\nfunc (gg *GoFileGenerator) phpTypeToGoType(phpT phpType) string {\n\tif goType, exists := phpToGoTypeMap[phpT]; exists {\n\t\treturn goType\n\t}\n\n\treturn \"any\"\n}\n\n// extractGoFunctionName extracts the Go function name from a Go function signature string.\nfunc extractGoFunctionName(goFunction string) string {\n\tidx := strings.Index(goFunction, \"func \")\n\tif idx == -1 {\n\t\treturn \"\"\n\t}\n\n\tstart := idx + len(\"func \")\n\n\tend := start\n\tfor end < len(goFunction) && goFunction[end] != '(' {\n\t\tend++\n\t}\n\n\tif end >= len(goFunction) {\n\t\treturn \"\"\n\t}\n\n\treturn strings.TrimSpace(goFunction[start:end])\n}\n\n// extractGoFunctionSignatureParams extracts the parameters from a Go function signature.\nfunc extractGoFunctionSignatureParams(goFunction string) string {\n\tstart := strings.IndexByte(goFunction, '(')\n\tif start == -1 {\n\t\treturn \"\"\n\t}\n\tstart++\n\n\tdepth := 1\n\tend := start\n\tfor end < len(goFunction) && depth > 0 {\n\t\tswitch goFunction[end] {\n\t\tcase '(':\n\t\t\tdepth++\n\t\tcase ')':\n\t\t\tdepth--\n\t\t}\n\t\tif depth > 0 {\n\t\t\tend++\n\t\t}\n\t}\n\n\tif end >= len(goFunction) {\n\t\treturn \"\"\n\t}\n\n\treturn strings.TrimSpace(goFunction[start:end])\n}\n\n// extractGoFunctionSignatureReturn extracts the return type from a Go function signature.\nfunc extractGoFunctionSignatureReturn(goFunction string) string {\n\tstart := strings.IndexByte(goFunction, '(')\n\tif start == -1 {\n\t\treturn \"\"\n\t}\n\n\tdepth := 1\n\tpos := start + 1\n\tfor pos < len(goFunction) && depth > 0 {\n\t\tswitch goFunction[pos] {\n\t\tcase '(':\n\t\t\tdepth++\n\t\tcase ')':\n\t\t\tdepth--\n\t\t}\n\t\tpos++\n\t}\n\n\tif pos >= len(goFunction) {\n\t\treturn \"\"\n\t}\n\n\tend := strings.IndexByte(goFunction[pos:], '{')\n\tif end == -1 {\n\t\treturn \"\"\n\t}\n\tend += pos\n\n\treturnType := strings.TrimSpace(goFunction[pos:end])\n\treturn returnType\n}\n\n// extractGoFunctionCallParams extracts just the parameter names for calling a function.\nfunc extractGoFunctionCallParams(goFunction string) string {\n\tparams := extractGoFunctionSignatureParams(goFunction)\n\tif params == \"\" {\n\t\treturn \"\"\n\t}\n\n\tvar names []string\n\tparts := strings.Split(params, \",\")\n\tfor _, part := range parts {\n\t\tpart = strings.TrimSpace(part)\n\t\tif len(part) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\twords := strings.Fields(part)\n\t\tif len(words) > 0 {\n\t\t\tnames = append(names, words[0])\n\t\t}\n\t}\n\n\tvar result strings.Builder\n\tfor i, name := range names {\n\t\tif i > 0 {\n\t\t\tresult.WriteString(\", \")\n\t\t}\n\n\t\tresult.WriteString(name)\n\t}\n\n\treturn result.String()\n}\n"
  },
  {
    "path": "internal/extgen/gofile_test.go",
    "content": "package extgen\n\nimport (\n\t\"bytes\"\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestGoFileGenerator_Generate(t *testing.T) {\n\ttmpDir := t.TempDir()\n\n\tsourceContent := `package main\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"github.com/dunglas/frankenphp/internal/extensions/types\"\n)\n\n//export_php: greet(name string): string\nfunc greet(name *go_string) *go_value {\n\treturn types.String(\"Hello \" + CStringToGoString(name))\n}\n\n//export_php: calculate(a int, b int): int\nfunc calculate(a long, b long) *go_value {\n\tresult := a + b\n\treturn types.Int(result)\n}\n\nfunc internalHelper(data string) string {\n\treturn strings.ToUpper(data)\n}\n\nfunc anotherHelper() {\n\tfmt.Println(\"Internal helper\")\n}`\n\n\tsourceFile := filepath.Join(tmpDir, \"test.go\")\n\trequire.NoError(t, os.WriteFile(sourceFile, []byte(sourceContent), 0644))\n\n\tgenerator := &Generator{\n\t\tBaseName:   \"test\",\n\t\tSourceFile: sourceFile,\n\t\tBuildDir:   tmpDir,\n\t\tFunctions: []phpFunction{\n\t\t\t{\n\t\t\t\tName:       \"greet\",\n\t\t\t\tReturnType: phpString,\n\t\t\t\tGoFunction: `func greet(name *go_string) *go_value {\n\treturn types.String(\"Hello \" + CStringToGoString(name))\n}`,\n\t\t\t},\n\t\t\t{\n\t\t\t\tName:       \"calculate\",\n\t\t\t\tReturnType: phpInt,\n\t\t\t\tGoFunction: `func calculate(a long, b long) *go_value {\n\tresult := a + b\n\treturn types.Int(result)\n}`,\n\t\t\t},\n\t\t},\n\t}\n\n\tgoGen := GoFileGenerator{generator}\n\trequire.NoError(t, goGen.generate())\n\n\tsourceStillExists := filepath.Join(tmpDir, \"test.go\")\n\trequire.FileExists(t, sourceStillExists)\n\tsourceStillContent, err := readFile(sourceStillExists)\n\trequire.NoError(t, err)\n\tassert.Equal(t, sourceContent, sourceStillContent, \"Source file should not be modified\")\n\n\tgeneratedFile := filepath.Join(tmpDir, \"test_generated.go\")\n\trequire.FileExists(t, generatedFile)\n\n\tgeneratedContent, err := readFile(generatedFile)\n\trequire.NoError(t, err)\n\n\ttestGeneratedFileBasicStructure(t, generatedContent, \"main\", \"test\")\n\ttestGeneratedFileWrappers(t, generatedContent, generator.Functions)\n}\n\nfunc TestGoFileGenerator_BuildContent(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\tbaseName    string\n\t\tsourceFile  string\n\t\tfunctions   []phpFunction\n\t\tclasses     []phpClass\n\t\tcontains    []string\n\t\tnotContains []string\n\t}{\n\t\t{\n\t\t\tname:     \"simple extension\",\n\t\t\tbaseName: \"simple\",\n\t\t\tsourceFile: createTempSourceFile(t, `package main\n\n//export_php: test(): void\nfunc test() {\n\t// simple function\n}`),\n\t\t\tfunctions: []phpFunction{\n\t\t\t\t{\n\t\t\t\t\tName:       \"test\",\n\t\t\t\t\tReturnType: phpVoid,\n\t\t\t\t\tGoFunction: \"func test() {\\n\\t// simple function\\n}\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tcontains: []string{\n\t\t\t\t\"package main\",\n\t\t\t\t`#include \"simple.h\"`,\n\t\t\t\t`import \"C\"`,\n\t\t\t\t\"func init()\",\n\t\t\t\t\"frankenphp.RegisterExtension(\",\n\t\t\t\t\"//export go_test\",\n\t\t\t\t\"func go_test()\",\n\t\t\t\t\"test()\", // wrapper calls original function\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"extension with complex imports\",\n\t\t\tbaseName: \"complex\",\n\t\t\tsourceFile: createTempSourceFile(t, `package main\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"encoding/json\"\n\t\"github.com/dunglas/frankenphp/internal/extensions/types\"\n)\n\n//export_php: process(data string): string\nfunc process(data *go_string) *go_value {\n\treturn types.String(fmt.Sprintf(\"processed: %s\", CStringToGoString(data)))\n}`),\n\t\t\tfunctions: []phpFunction{\n\t\t\t\t{\n\t\t\t\t\tName:       \"process\",\n\t\t\t\t\tReturnType: phpString,\n\t\t\t\t\tGoFunction: `func process(data *go_string) *go_value {\n\treturn String(fmt.Sprintf(\"processed: %s\", CStringToGoString(data)))\n}`,\n\t\t\t\t},\n\t\t\t},\n\t\t\tcontains: []string{\n\t\t\t\t\"package main\",\n\t\t\t\t\"//export go_process\",\n\t\t\t\t`\"C\"`,\n\t\t\t\t\"process(\", // wrapper calls original function\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"extension with internal functions\",\n\t\t\tbaseName: \"internal\",\n\t\t\tsourceFile: createTempSourceFile(t, `package main\n\n//export_php: publicFunc(): void\nfunc publicFunc() {}\n\nfunc internalFunc1() string {\n\treturn \"internal\"\n}\n\nfunc internalFunc2(data string) {\n\t// process data internally\n}`),\n\t\t\tfunctions: []phpFunction{\n\t\t\t\t{\n\t\t\t\t\tName:       \"publicFunc\",\n\t\t\t\t\tReturnType: phpVoid,\n\t\t\t\t\tGoFunction: \"func publicFunc() {}\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tcontains: []string{\n\t\t\t\t\"//export go_publicFunc\",\n\t\t\t\t\"func go_publicFunc()\",\n\t\t\t\t\"publicFunc()\", // wrapper calls original function\n\t\t\t},\n\t\t\tnotContains: []string{\n\t\t\t\t\"func internalFunc1() string\",\n\t\t\t\t\"func internalFunc2(data string)\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"runtime/cgo blank import without classes\",\n\t\t\tbaseName: \"no_classes\",\n\t\t\tsourceFile: createTempSourceFile(t, `package main\n\n//export_php: getValue(): string\nfunc getValue() string {\n\treturn \"test\"\n}`),\n\t\t\tfunctions: []phpFunction{\n\t\t\t\t{\n\t\t\t\t\tName:       \"getValue\",\n\t\t\t\t\tReturnType: phpString,\n\t\t\t\t\tGoFunction: `func getValue() string {\n\treturn \"test\"\n}`,\n\t\t\t\t},\n\t\t\t},\n\t\t\tclasses: nil,\n\t\t\tcontains: []string{\n\t\t\t\t`_ \"runtime/cgo\"`,\n\t\t\t\t\"func init()\",\n\t\t\t\t\"frankenphp.RegisterExtension(\",\n\t\t\t},\n\t\t\tnotContains: []string{\n\t\t\t\t\"cgo.NewHandle\",\n\t\t\t\t\"registerGoObject\",\n\t\t\t\t\"getGoObject\",\n\t\t\t\t\"removeGoObject\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"runtime/cgo normal import with classes\",\n\t\t\tbaseName: \"with_classes\",\n\t\t\tsourceFile: createTempSourceFile(t, `package main\n\n//export_php:class TestClass\ntype TestStruct struct {\n\tvalue string\n}\n\n//export_php:method TestClass::getValue(): string\nfunc (ts *TestStruct) GetValue() string {\n\treturn ts.value\n}`),\n\t\t\tfunctions: []phpFunction{},\n\t\t\tclasses: []phpClass{\n\t\t\t\t{\n\t\t\t\t\tName:     \"TestClass\",\n\t\t\t\t\tGoStruct: \"TestStruct\",\n\t\t\t\t\tMethods: []phpClassMethod{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName:       \"GetValue\",\n\t\t\t\t\t\t\tReturnType: phpString,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tcontains: []string{\n\t\t\t\t`\"runtime/cgo\"`,\n\t\t\t\t\"cgo.NewHandle\",\n\t\t\t\t\"func registerGoObject\",\n\t\t\t\t\"func getGoObject\",\n\t\t\t},\n\t\t\tnotContains: []string{\n\t\t\t\t`_ \"runtime/cgo\"`,\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgenerator := &Generator{\n\t\t\t\tBaseName:   tt.baseName,\n\t\t\t\tSourceFile: tt.sourceFile,\n\t\t\t\tFunctions:  tt.functions,\n\t\t\t\tClasses:    tt.classes,\n\t\t\t}\n\n\t\t\tgoGen := GoFileGenerator{generator}\n\t\t\tcontent, err := goGen.buildContent()\n\t\t\trequire.NoError(t, err)\n\n\t\t\tfor _, expected := range tt.contains {\n\t\t\t\tassert.Contains(t, content, expected, \"Generated Go content should contain %q\", expected)\n\t\t\t}\n\n\t\t\tfor _, notExpected := range tt.notContains {\n\t\t\t\tassert.NotContains(t, content, notExpected, \"Generated Go content should NOT contain %q\", notExpected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestGoFileGenerator_PackageNameSanitization(t *testing.T) {\n\ttests := []struct {\n\t\tbaseName        string\n\t\texpectedPackage string\n\t}{\n\t\t{\"simple\", \"main\"},\n\t\t{\"my-extension\", \"main\"},\n\t\t{\"ext.with.dots\", \"main\"},\n\t\t{\"123invalid\", \"main\"},\n\t\t{\"valid_name\", \"main\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.baseName, func(t *testing.T) {\n\t\t\tsourceFile := createTempSourceFile(t, \"package main\\n//export_php: test(): void\\nfunc test() {}\")\n\n\t\t\tgenerator := &Generator{\n\t\t\t\tBaseName:   tt.baseName,\n\t\t\t\tSourceFile: sourceFile,\n\t\t\t\tFunctions: []phpFunction{\n\t\t\t\t\t{Name: \"test\", ReturnType: phpVoid, GoFunction: \"func test() {}\"},\n\t\t\t\t},\n\t\t\t}\n\n\t\t\tgoGen := GoFileGenerator{generator}\n\t\t\tcontent, err := goGen.buildContent()\n\t\t\trequire.NoError(t, err)\n\n\t\t\texpectedPackage := \"package \" + tt.expectedPackage\n\t\t\tassert.Contains(t, content, expectedPackage, \"Generated content should contain '%s'\", expectedPackage)\n\t\t})\n\t}\n}\n\nfunc TestGoFileGenerator_ErrorHandling(t *testing.T) {\n\ttests := []struct {\n\t\tname       string\n\t\tsourceFile string\n\t\texpectErr  bool\n\t}{\n\t\t{\n\t\t\tname:       \"nonexistent file\",\n\t\t\tsourceFile: \"/nonexistent/file.go\",\n\t\t\texpectErr:  true,\n\t\t},\n\t\t{\n\t\t\tname:       \"invalid Go syntax\",\n\t\t\tsourceFile: createTempSourceFile(t, \"invalid go syntax here\"),\n\t\t\texpectErr:  true,\n\t\t},\n\t\t{\n\t\t\tname:       \"valid file\",\n\t\t\tsourceFile: createTempSourceFile(t, \"package main\\nfunc test() {}\"),\n\t\t\texpectErr:  false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgenerator := &Generator{\n\t\t\t\tBaseName:   \"test\",\n\t\t\t\tSourceFile: tt.sourceFile,\n\t\t\t}\n\n\t\t\tgoGen := GoFileGenerator{generator}\n\t\t\t_, err := goGen.buildContent()\n\n\t\t\tif tt.expectErr {\n\t\t\t\tassert.Error(t, err, \"Expected error but got none\")\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err, \"Unexpected error\")\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestGoFileGenerator_ComplexScenario(t *testing.T) {\n\tsourceContent := `package example\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"encoding/json\"\n\t\"github.com/dunglas/frankenphp/internal/extensions/types\"\n)\n\n//export_php: processData(input string, options array): array\nfunc processData(input *go_string, options *go_nullable) *go_value {\n\tdata := CStringToGoString(input)\n\tprocessed := internalProcess(data)\n\treturn types.Array([]any{processed})\n}\n\n//export_php: validateInput(data string): bool\nfunc validateInput(data *go_string) *go_value {\n\tinput := CStringToGoString(data)\n\tisValid := len(input) > 0 && validateFormat(input)\n\treturn types.Bool(isValid)\n}\n\nfunc internalProcess(data string) string {\n\treturn strings.ToUpper(data)\n}\n\nfunc validateFormat(input string) bool {\n\treturn !strings.Contains(input, \"invalid\")\n}\n\nfunc jsonHelper(data any) ([]byte, error) {\n\treturn json.Marshal(data)\n}\n\nfunc debugPrint(msg string) {\n\tfmt.Printf(\"DEBUG: %s\\n\", msg)\n}`\n\n\tsourceFile := createTempSourceFile(t, sourceContent)\n\n\tfunctions := []phpFunction{\n\t\t{\n\t\t\tName:       \"processData\",\n\t\t\tReturnType: phpArray,\n\t\t\tGoFunction: `func processData(input *go_string, options *go_nullable) *go_value {\n\tdata := CStringToGoString(input)\n\tprocessed := internalProcess(data)\n\treturn Array([]any{processed})\n}`,\n\t\t},\n\t\t{\n\t\t\tName:       \"validateInput\",\n\t\t\tReturnType: phpBool,\n\t\t\tGoFunction: `func validateInput(data *go_string) *go_value {\n\tinput := CStringToGoString(data)\n\tisValid := len(input) > 0 && validateFormat(input)\n\treturn Bool(isValid)\n}`,\n\t\t},\n\t}\n\n\tgenerator := &Generator{\n\t\tBaseName:   \"complex-example\",\n\t\tSourceFile: sourceFile,\n\t\tFunctions:  functions,\n\t}\n\n\tgoGen := GoFileGenerator{generator}\n\tcontent, err := goGen.buildContent()\n\trequire.NoError(t, err)\n\tassert.Contains(t, content, \"package example\", \"Package name should match source package\")\n\n\tfor _, fn := range functions {\n\t\texportDirective := \"//export go_\" + fn.Name\n\t\tassert.Contains(t, content, exportDirective, \"Generated content should contain export directive: %s\", exportDirective)\n\t}\n}\n\nfunc TestGoFileGenerator_MethodWrapperWithNullableParams(t *testing.T) {\n\ttmpDir := t.TempDir()\n\n\tsourceContent := `package main\n\nimport \"fmt\"\n\n//export_php:class TestClass\ntype TestStruct struct {\n\tname string\n}\n\n//export_php:method TestClass::processData(string $name, ?int $count, ?bool $enabled): string\nfunc (ts *TestStruct) ProcessData(name string, count *int64, enabled *bool) string {\n\tresult := fmt.Sprintf(\"name=%s\", name)\n\tif count != nil {\n\t\tresult += fmt.Sprintf(\", count=%d\", *count)\n\t}\n\tif enabled != nil {\n\t\tresult += fmt.Sprintf(\", enabled=%t\", *enabled)\n\t}\n\treturn result\n}`\n\n\tsourceFile := filepath.Join(tmpDir, \"test.go\")\n\trequire.NoError(t, os.WriteFile(sourceFile, []byte(sourceContent), 0644))\n\n\tmethods := []phpClassMethod{\n\t\t{\n\t\t\tName:       \"ProcessData\",\n\t\t\tPhpName:    \"processData\",\n\t\t\tClassName:  \"TestClass\",\n\t\t\tSignature:  \"processData(string $name, ?int $count, ?bool $enabled): string\",\n\t\t\tReturnType: phpString,\n\t\t\tParams: []phpParameter{\n\t\t\t\t{Name: \"name\", PhpType: phpString, IsNullable: false},\n\t\t\t\t{Name: \"count\", PhpType: phpInt, IsNullable: true},\n\t\t\t\t{Name: \"enabled\", PhpType: phpBool, IsNullable: true},\n\t\t\t},\n\t\t\tGoFunction: `func (ts *TestStruct) ProcessData(name string, count *int64, enabled *bool) string {\n\tresult := fmt.Sprintf(\"name=%s\", name)\n\tif count != nil {\n\t\tresult += fmt.Sprintf(\", count=%d\", *count)\n\t}\n\tif enabled != nil {\n\t\tresult += fmt.Sprintf(\", enabled=%t\", *enabled)\n\t}\n\treturn result\n}`,\n\t\t},\n\t}\n\n\tclasses := []phpClass{\n\t\t{\n\t\t\tName:     \"TestClass\",\n\t\t\tGoStruct: \"TestStruct\",\n\t\t\tMethods:  methods,\n\t\t},\n\t}\n\n\tgenerator := &Generator{\n\t\tBaseName:   \"nullable_test\",\n\t\tSourceFile: sourceFile,\n\t\tClasses:    classes,\n\t\tBuildDir:   tmpDir,\n\t}\n\n\tgoGen := GoFileGenerator{generator}\n\tcontent, err := goGen.buildContent()\n\trequire.NoError(t, err)\n\n\texpectedWrapperSignature := \"func ProcessData_wrapper(handle C.uintptr_t, name *C.zend_string, count *int64, enabled *bool)\"\n\tassert.Contains(t, content, expectedWrapperSignature, \"Generated content should contain wrapper with nullable pointer types: %s\", expectedWrapperSignature)\n\n\texpectedCall := \"structObj.ProcessData(name, count, enabled)\"\n\tassert.Contains(t, content, expectedCall, \"Generated content should contain correct method call: %s\", expectedCall)\n\n\texportDirective := \"//export ProcessData_wrapper\"\n\tassert.Contains(t, content, exportDirective, \"Generated content should contain export directive: %s\", exportDirective)\n}\n\nfunc TestGoFileGenerator_MethodWrapperWithArrayParams(t *testing.T) {\n\ttmpDir := t.TempDir()\n\n\tsourceContent := `package main\n\nimport \"fmt\"\n\n//export_php:class ArrayClass\ntype ArrayStruct struct {\n\tdata []any\n}\n\n//export_php:method ArrayClass::processArray(array $items): array\nfunc (as *ArrayStruct) ProcessArray(items frankenphp.AssociativeArray) frankenphp.AssociativeArray {\n\tresult := frankenphp.AssociativeArray{}\n\tfor key, value := range items.Map {\n\t\tresult.Set(\"processed_\"+key, value)\n\t}\n\treturn result\n}\n\n//export_php:method ArrayClass::filterData(array $data, string $filter): array\nfunc (as *ArrayStruct) FilterData(data frankenphp.AssociativeArray, filter string) frankenphp.AssociativeArray {\n\tresult := frankenphp.AssociativeArray{}\n\t// Filter logic here\n\treturn result\n}`\n\n\tsourceFile := filepath.Join(tmpDir, \"test.go\")\n\trequire.NoError(t, os.WriteFile(sourceFile, []byte(sourceContent), 0644))\n\n\tmethods := []phpClassMethod{\n\t\t{\n\t\t\tName:       \"ProcessArray\",\n\t\t\tPhpName:    \"processArray\",\n\t\t\tClassName:  \"ArrayClass\",\n\t\t\tSignature:  \"processArray(array $items): array\",\n\t\t\tReturnType: phpArray,\n\t\t\tParams: []phpParameter{\n\t\t\t\t{Name: \"items\", PhpType: phpArray, IsNullable: false},\n\t\t\t},\n\t\t\tGoFunction: `func (as *ArrayStruct) ProcessArray(items frankenphp.AssociativeArray) frankenphp.AssociativeArray {\n\tresult := frankenphp.AssociativeArray{}\n\tfor key, value := range items.Entries() {\n\t\tresult.Set(\"processed_\"+key, value)\n\t}\n\treturn result\n}`,\n\t\t},\n\t\t{\n\t\t\tName:       \"FilterData\",\n\t\t\tPhpName:    \"filterData\",\n\t\t\tClassName:  \"ArrayClass\",\n\t\t\tSignature:  \"filterData(array $data, string $filter): array\",\n\t\t\tReturnType: phpArray,\n\t\t\tParams: []phpParameter{\n\t\t\t\t{Name: \"data\", PhpType: phpArray, IsNullable: false},\n\t\t\t\t{Name: \"filter\", PhpType: phpString, IsNullable: false},\n\t\t\t},\n\t\t\tGoFunction: `func (as *ArrayStruct) FilterData(data frankenphp.AssociativeArray, filter string) frankenphp.AssociativeArray {\n\tresult := frankenphp.AssociativeArray{}\n\treturn result\n}`,\n\t\t},\n\t}\n\n\tclasses := []phpClass{\n\t\t{\n\t\t\tName:     \"ArrayClass\",\n\t\t\tGoStruct: \"ArrayStruct\",\n\t\t\tMethods:  methods,\n\t\t},\n\t}\n\n\tgenerator := &Generator{\n\t\tBaseName:   \"array_test\",\n\t\tSourceFile: sourceFile,\n\t\tClasses:    classes,\n\t\tBuildDir:   tmpDir,\n\t}\n\n\tgoGen := GoFileGenerator{generator}\n\tcontent, err := goGen.buildContent()\n\trequire.NoError(t, err)\n\n\texpectedArrayWrapperSignature := \"func ProcessArray_wrapper(handle C.uintptr_t, items *C.zval) unsafe.Pointer\"\n\tassert.Contains(t, content, expectedArrayWrapperSignature, \"Generated content should contain array wrapper signature: %s\", expectedArrayWrapperSignature)\n\n\texpectedMixedWrapperSignature := \"func FilterData_wrapper(handle C.uintptr_t, data *C.zval, filter *C.zend_string) unsafe.Pointer\"\n\tassert.Contains(t, content, expectedMixedWrapperSignature, \"Generated content should contain mixed wrapper signature: %s\", expectedMixedWrapperSignature)\n\n\texpectedArrayCall := \"structObj.ProcessArray(items)\"\n\tassert.Contains(t, content, expectedArrayCall, \"Generated content should contain array method call: %s\", expectedArrayCall)\n\n\texpectedMixedCall := \"structObj.FilterData(data, filter)\"\n\tassert.Contains(t, content, expectedMixedCall, \"Generated content should contain mixed method call: %s\", expectedMixedCall)\n\n\tassert.Contains(t, content, \"//export ProcessArray_wrapper\", \"Generated content should contain ProcessArray export directive\")\n\tassert.Contains(t, content, \"//export FilterData_wrapper\", \"Generated content should contain FilterData export directive\")\n}\n\nfunc TestGoFileGenerator_Idempotency(t *testing.T) {\n\ttmpDir := t.TempDir()\n\n\tsourceContent := `package main\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n)\n\n//export_php: greet(name string): string\nfunc greet(name *go_string) *go_value {\n\treturn String(\"Hello \" + CStringToGoString(name))\n}\n\nfunc internalHelper(data string) string {\n\treturn strings.ToUpper(data)\n}`\n\n\tsourceFile := filepath.Join(tmpDir, \"test.go\")\n\trequire.NoError(t, os.WriteFile(sourceFile, []byte(sourceContent), 0644))\n\n\tgenerator := &Generator{\n\t\tBaseName:   \"test\",\n\t\tSourceFile: sourceFile,\n\t\tBuildDir:   tmpDir,\n\t\tFunctions: []phpFunction{\n\t\t\t{\n\t\t\t\tName:       \"greet\",\n\t\t\t\tReturnType: phpString,\n\t\t\t\tGoFunction: `func greet(name *go_string) *go_value {\n\treturn String(\"Hello \" + CStringToGoString(name))\n}`,\n\t\t\t},\n\t\t},\n\t}\n\n\tgoGen := GoFileGenerator{generator}\n\trequire.NoError(t, goGen.generate(), \"First generation should succeed\")\n\n\tgeneratedFile := filepath.Join(tmpDir, \"test_generated.go\")\n\trequire.FileExists(t, generatedFile, \"Generated file should exist after first run\")\n\n\tfirstRunContent, err := os.ReadFile(generatedFile)\n\trequire.NoError(t, err)\n\tfirstRunSourceContent, err := os.ReadFile(sourceFile)\n\trequire.NoError(t, err)\n\n\trequire.NoError(t, goGen.generate(), \"Second generation should succeed\")\n\n\tsecondRunContent, err := os.ReadFile(generatedFile)\n\trequire.NoError(t, err)\n\tsecondRunSourceContent, err := os.ReadFile(sourceFile)\n\trequire.NoError(t, err)\n\n\tassert.True(t, bytes.Equal(firstRunContent, secondRunContent), \"Generated file content should be identical between runs\")\n\tassert.True(t, bytes.Equal(firstRunSourceContent, secondRunSourceContent), \"Source file should remain unchanged after both runs\")\n\tassert.Equal(t, sourceContent, string(secondRunSourceContent), \"Source file content should match original\")\n}\n\nfunc TestGoFileGenerator_HeaderComments(t *testing.T) {\n\ttmpDir := t.TempDir()\n\n\tsourceContent := `package main\n\n//export_php: test(): void\nfunc test() {\n\t// simple function\n}`\n\n\tsourceFile := filepath.Join(tmpDir, \"test.go\")\n\trequire.NoError(t, os.WriteFile(sourceFile, []byte(sourceContent), 0644))\n\n\tgenerator := &Generator{\n\t\tBaseName:   \"test\",\n\t\tSourceFile: sourceFile,\n\t\tBuildDir:   tmpDir,\n\t\tFunctions: []phpFunction{\n\t\t\t{\n\t\t\t\tName:       \"test\",\n\t\t\t\tReturnType: phpVoid,\n\t\t\t\tGoFunction: \"func test() {\\n\\t// simple function\\n}\",\n\t\t\t},\n\t\t},\n\t}\n\n\tgoGen := GoFileGenerator{generator}\n\trequire.NoError(t, goGen.generate())\n\n\tgeneratedFile := filepath.Join(tmpDir, \"test_generated.go\")\n\trequire.FileExists(t, generatedFile)\n\n\tassertContainsHeaderComment(t, generatedFile)\n}\n\nfunc TestExtractGoFunctionName(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"simple function\",\n\t\t\tinput:    \"func test() {}\",\n\t\t\texpected: \"test\",\n\t\t},\n\t\t{\n\t\t\tname:     \"function with params\",\n\t\t\tinput:    \"func calculate(a int, b int) int {}\",\n\t\t\texpected: \"calculate\",\n\t\t},\n\t\t{\n\t\t\tname:     \"function with complex params\",\n\t\t\tinput:    \"func process(data *go_string, opts *go_nullable) *go_value {}\",\n\t\t\texpected: \"process\",\n\t\t},\n\t\t{\n\t\t\tname:     \"function with whitespace\",\n\t\t\tinput:    \"func  spacedName  () {}\",\n\t\t\texpected: \"spacedName\",\n\t\t},\n\t\t{\n\t\t\tname:     \"no func keyword\",\n\t\t\tinput:    \"test() {}\",\n\t\t\texpected: \"\",\n\t\t},\n\t\t{\n\t\t\tname:     \"empty string\",\n\t\t\tinput:    \"\",\n\t\t\texpected: \"\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := extractGoFunctionName(tt.input)\n\t\t\tassert.Equal(t, tt.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestExtractGoFunctionSignatureParams(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"no parameters\",\n\t\t\tinput:    \"func test() {}\",\n\t\t\texpected: \"\",\n\t\t},\n\t\t{\n\t\t\tname:     \"single parameter\",\n\t\t\tinput:    \"func test(name string) {}\",\n\t\t\texpected: \"name string\",\n\t\t},\n\t\t{\n\t\t\tname:     \"multiple parameters\",\n\t\t\tinput:    \"func test(a int, b string, c bool) {}\",\n\t\t\texpected: \"a int, b string, c bool\",\n\t\t},\n\t\t{\n\t\t\tname:     \"pointer parameters\",\n\t\t\tinput:    \"func test(data *go_string) {}\",\n\t\t\texpected: \"data *go_string\",\n\t\t},\n\t\t{\n\t\t\tname:     \"nested parentheses\",\n\t\t\tinput:    \"func test(fn func(int) string) {}\",\n\t\t\texpected: \"fn func(int) string\",\n\t\t},\n\t\t{\n\t\t\tname:     \"variadic parameters\",\n\t\t\tinput:    \"func test(args ...string) {}\",\n\t\t\texpected: \"args ...string\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := extractGoFunctionSignatureParams(tt.input)\n\t\t\tassert.Equal(t, tt.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestExtractGoFunctionSignatureReturn(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"no return type\",\n\t\t\tinput:    \"func test() {}\",\n\t\t\texpected: \"\",\n\t\t},\n\t\t{\n\t\t\tname:     \"single return type\",\n\t\t\tinput:    \"func test() string {}\",\n\t\t\texpected: \"string\",\n\t\t},\n\t\t{\n\t\t\tname:     \"pointer return type\",\n\t\t\tinput:    \"func test() *go_value {}\",\n\t\t\texpected: \"*go_value\",\n\t\t},\n\t\t{\n\t\t\tname:     \"multiple return types\",\n\t\t\tinput:    \"func test() (string, error) {}\",\n\t\t\texpected: \"(string, error)\",\n\t\t},\n\t\t{\n\t\t\tname:     \"named return values\",\n\t\t\tinput:    \"func test() (result string, err error) {}\",\n\t\t\texpected: \"(result string, err error)\",\n\t\t},\n\t\t{\n\t\t\tname:     \"complex return type\",\n\t\t\tinput:    \"func test() unsafe.Pointer {}\",\n\t\t\texpected: \"unsafe.Pointer\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := extractGoFunctionSignatureReturn(tt.input)\n\t\t\tassert.Equal(t, tt.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestExtractGoFunctionCallParams(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"no parameters\",\n\t\t\tinput:    \"func test() {}\",\n\t\t\texpected: \"\",\n\t\t},\n\t\t{\n\t\t\tname:     \"single parameter\",\n\t\t\tinput:    \"func test(name string) {}\",\n\t\t\texpected: \"name\",\n\t\t},\n\t\t{\n\t\t\tname:     \"multiple parameters\",\n\t\t\tinput:    \"func test(a int, b string, c bool) {}\",\n\t\t\texpected: \"a, b, c\",\n\t\t},\n\t\t{\n\t\t\tname:     \"pointer parameters\",\n\t\t\tinput:    \"func test(data *go_string, opts *go_nullable) {}\",\n\t\t\texpected: \"data, opts\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := extractGoFunctionCallParams(tt.input)\n\t\t\tassert.Equal(t, tt.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestGoFileGenerator_SourceFilePreservation(t *testing.T) {\n\ttmpDir := t.TempDir()\n\n\tsourceContent := `package main\n\nimport \"fmt\"\n\n//export_php: greet(name string): string\nfunc greet(name *go_string) *go_value {\n\treturn String(fmt.Sprintf(\"Hello, %s!\", CStringToGoString(name)))\n}\n\nfunc internalHelper() {\n\tfmt.Println(\"internal\")\n}`\n\n\tsourceFile := filepath.Join(tmpDir, \"test.go\")\n\trequire.NoError(t, os.WriteFile(sourceFile, []byte(sourceContent), 0644))\n\n\thashBefore := computeFileHash(t, sourceFile)\n\n\tgenerator := &Generator{\n\t\tBaseName:   \"test\",\n\t\tSourceFile: sourceFile,\n\t\tBuildDir:   tmpDir,\n\t\tFunctions: []phpFunction{\n\t\t\t{\n\t\t\t\tName:       \"greet\",\n\t\t\t\tReturnType: phpString,\n\t\t\t\tGoFunction: `func greet(name *go_string) *go_value {\n\treturn String(fmt.Sprintf(\"Hello, %s!\", CStringToGoString(name)))\n}`,\n\t\t\t},\n\t\t},\n\t}\n\n\tgoGen := GoFileGenerator{generator}\n\trequire.NoError(t, goGen.generate())\n\n\thashAfter := computeFileHash(t, sourceFile)\n\n\tassert.Equal(t, hashBefore, hashAfter, \"Source file hash should remain unchanged after generation\")\n\n\tcontentAfter, err := os.ReadFile(sourceFile)\n\trequire.NoError(t, err)\n\tassert.Equal(t, sourceContent, string(contentAfter), \"Source file content should be byte-for-byte identical\")\n}\n\nfunc TestGoFileGenerator_WrapperParameterForwarding(t *testing.T) {\n\ttmpDir := t.TempDir()\n\n\tsourceContent := `package main\n\nimport \"fmt\"\n\n//export_php: process(name string, count int): string\nfunc process(name *go_string, count long) *go_value {\n\tn := CStringToGoString(name)\n\treturn String(fmt.Sprintf(\"%s: %d\", n, count))\n}\n\n//export_php: simple(): void\nfunc simple() {}`\n\n\tsourceFile := filepath.Join(tmpDir, \"test.go\")\n\trequire.NoError(t, os.WriteFile(sourceFile, []byte(sourceContent), 0644))\n\n\tfunctions := []phpFunction{\n\t\t{\n\t\t\tName:       \"process\",\n\t\t\tReturnType: phpString,\n\t\t\tGoFunction: `func process(name *go_string, count long) *go_value {\n\tn := CStringToGoString(name)\n\treturn String(fmt.Sprintf(\"%s: %d\", n, count))\n}`,\n\t\t},\n\t\t{\n\t\t\tName:       \"simple\",\n\t\t\tReturnType: phpVoid,\n\t\t\tGoFunction: \"func simple() {}\",\n\t\t},\n\t}\n\n\tgenerator := &Generator{\n\t\tBaseName:   \"wrapper_test\",\n\t\tSourceFile: sourceFile,\n\t\tBuildDir:   tmpDir,\n\t\tFunctions:  functions,\n\t}\n\n\tgoGen := GoFileGenerator{generator}\n\tcontent, err := goGen.buildContent()\n\trequire.NoError(t, err)\n\n\tassert.Contains(t, content, \"//export go_process\", \"Should have wrapper export directive\")\n\tassert.Contains(t, content, \"func go_process(\", \"Should have wrapper function\")\n\tassert.Contains(t, content, \"process(\", \"Wrapper should call original function\")\n\n\tassert.Contains(t, content, \"//export go_simple\", \"Should have simple wrapper export directive\")\n\tassert.Contains(t, content, \"func go_simple()\", \"Should have simple wrapper function\")\n\tassert.Contains(t, content, \"simple()\", \"Simple wrapper should call original function\")\n}\n\nfunc TestGoFileGenerator_MalformedSource(t *testing.T) {\n\ttests := []struct {\n\t\tname          string\n\t\tsourceContent string\n\t\texpectError   bool\n\t}{\n\t\t{\n\t\t\tname:          \"missing package declaration\",\n\t\t\tsourceContent: \"func test() {}\",\n\t\t\texpectError:   true,\n\t\t},\n\t\t{\n\t\t\tname:          \"syntax error\",\n\t\t\tsourceContent: \"package main\\nfunc test( {}\",\n\t\t\texpectError:   true,\n\t\t},\n\t\t{\n\t\t\tname:          \"incomplete function\",\n\t\t\tsourceContent: \"package main\\nfunc test() {\",\n\t\t\texpectError:   true,\n\t\t},\n\t\t{\n\t\t\tname:          \"valid minimal source\",\n\t\t\tsourceContent: \"package main\\nfunc test() {}\",\n\t\t\texpectError:   false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ttmpDir := t.TempDir()\n\t\t\tsourceFile := filepath.Join(tmpDir, \"test.go\")\n\t\t\trequire.NoError(t, os.WriteFile(sourceFile, []byte(tt.sourceContent), 0644))\n\n\t\t\tgenerator := &Generator{\n\t\t\t\tBaseName:   \"test\",\n\t\t\t\tSourceFile: sourceFile,\n\t\t\t\tBuildDir:   tmpDir,\n\t\t\t}\n\n\t\t\tgoGen := GoFileGenerator{generator}\n\t\t\t_, err := goGen.buildContent()\n\n\t\t\tif tt.expectError {\n\t\t\t\tassert.Error(t, err, \"Expected error for malformed source\")\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err, \"Should not error for valid source\")\n\t\t\t}\n\n\t\t\tcontentAfter, readErr := os.ReadFile(sourceFile)\n\t\t\trequire.NoError(t, readErr)\n\t\t\tassert.Equal(t, tt.sourceContent, string(contentAfter), \"Source file should remain unchanged even on error\")\n\t\t})\n\t}\n}\n\nfunc TestGoFileGenerator_MethodWrapperWithNullableArrayParams(t *testing.T) {\n\ttmpDir := t.TempDir()\n\n\tsourceContent := `package main\n\n//export_php:class NullableArrayClass\ntype NullableArrayStruct struct{}\n\n//export_php:method NullableArrayClass::processOptionalArray(?array $items, string $name): string\nfunc (nas *NullableArrayStruct) ProcessOptionalArray(items frankenphp.AssociativeArray, name string) string {\n\treturn fmt.Sprintf(\"Processing %d items for %s\", len(items.Map), name)\n}`\n\n\tsourceFile := filepath.Join(tmpDir, \"test.go\")\n\trequire.NoError(t, os.WriteFile(sourceFile, []byte(sourceContent), 0644))\n\n\tmethods := []phpClassMethod{\n\t\t{\n\t\t\tName:       \"ProcessOptionalArray\",\n\t\t\tPhpName:    \"processOptionalArray\",\n\t\t\tClassName:  \"NullableArrayClass\",\n\t\t\tSignature:  \"processOptionalArray(?array $items, string $name): string\",\n\t\t\tReturnType: phpString,\n\t\t\tParams: []phpParameter{\n\t\t\t\t{Name: \"items\", PhpType: phpArray, IsNullable: true},\n\t\t\t\t{Name: \"name\", PhpType: phpString, IsNullable: false},\n\t\t\t},\n\t\t\tGoFunction: `func (nas *NullableArrayStruct) ProcessOptionalArray(items frankenphp.AssociativeArray, name string) string {\n\treturn fmt.Sprintf(\"Processing %d items for %s\", len(items.Map), name)\n}`,\n\t\t},\n\t}\n\n\tclasses := []phpClass{\n\t\t{\n\t\t\tName:     \"NullableArrayClass\",\n\t\t\tGoStruct: \"NullableArrayStruct\",\n\t\t\tMethods:  methods,\n\t\t},\n\t}\n\n\tgenerator := &Generator{\n\t\tBaseName:   \"nullable_array_test\",\n\t\tSourceFile: sourceFile,\n\t\tClasses:    classes,\n\t\tBuildDir:   tmpDir,\n\t}\n\n\tgoGen := GoFileGenerator{generator}\n\tcontent, err := goGen.buildContent()\n\trequire.NoError(t, err)\n\n\texpectedWrapperSignature := \"func ProcessOptionalArray_wrapper(handle C.uintptr_t, items *C.zval, name *C.zend_string) unsafe.Pointer\"\n\tassert.Contains(t, content, expectedWrapperSignature, \"Generated content should contain nullable array wrapper signature: %s\", expectedWrapperSignature)\n\n\texpectedCall := \"structObj.ProcessOptionalArray(items, name)\"\n\tassert.Contains(t, content, expectedCall, \"Generated content should contain method call: %s\", expectedCall)\n\n\tassert.Contains(t, content, \"//export ProcessOptionalArray_wrapper\", \"Generated content should contain export directive\")\n}\n\nfunc createTempSourceFile(t *testing.T, content string) string {\n\ttmpDir := t.TempDir()\n\ttmpFile := filepath.Join(tmpDir, \"source.go\")\n\n\trequire.NoError(t, os.WriteFile(tmpFile, []byte(content), 0644))\n\n\treturn tmpFile\n}\n\nfunc TestGoFileGenerator_MethodWrapperWithCallableParams(t *testing.T) {\n\ttmpDir := t.TempDir()\n\n\tsourceContent := `package main\n\nimport \"C\"\n\n//export_php:class CallableClass\ntype CallableStruct struct{}\n\n//export_php:method CallableClass::processCallback(callable $callback): string\nfunc (cs *CallableStruct) ProcessCallback(callback *C.zval) string {\n\treturn \"processed\"\n}\n\n//export_php:method CallableClass::processOptionalCallback(?callable $callback): string\nfunc (cs *CallableStruct) ProcessOptionalCallback(callback *C.zval) string {\n\treturn \"processed_optional\"\n}`\n\n\tsourceFile := filepath.Join(tmpDir, \"test.go\")\n\trequire.NoError(t, os.WriteFile(sourceFile, []byte(sourceContent), 0644))\n\n\tmethods := []phpClassMethod{\n\t\t{\n\t\t\tName:       \"ProcessCallback\",\n\t\t\tPhpName:    \"processCallback\",\n\t\t\tClassName:  \"CallableClass\",\n\t\t\tSignature:  \"processCallback(callable $callback): string\",\n\t\t\tReturnType: phpString,\n\t\t\tParams: []phpParameter{\n\t\t\t\t{Name: \"callback\", PhpType: phpCallable, IsNullable: false},\n\t\t\t},\n\t\t\tGoFunction: `func (cs *CallableStruct) ProcessCallback(callback *C.zval) string {\n\treturn \"processed\"\n}`,\n\t\t},\n\t\t{\n\t\t\tName:       \"ProcessOptionalCallback\",\n\t\t\tPhpName:    \"processOptionalCallback\",\n\t\t\tClassName:  \"CallableClass\",\n\t\t\tSignature:  \"processOptionalCallback(?callable $callback): string\",\n\t\t\tReturnType: phpString,\n\t\t\tParams: []phpParameter{\n\t\t\t\t{Name: \"callback\", PhpType: phpCallable, IsNullable: true},\n\t\t\t},\n\t\t\tGoFunction: `func (cs *CallableStruct) ProcessOptionalCallback(callback *C.zval) string {\n\treturn \"processed_optional\"\n}`,\n\t\t},\n\t}\n\n\tclasses := []phpClass{\n\t\t{\n\t\t\tName:     \"CallableClass\",\n\t\t\tGoStruct: \"CallableStruct\",\n\t\t\tMethods:  methods,\n\t\t},\n\t}\n\n\tgenerator := &Generator{\n\t\tBaseName:   \"callable_test\",\n\t\tSourceFile: sourceFile,\n\t\tClasses:    classes,\n\t\tBuildDir:   tmpDir,\n\t}\n\n\tgoGen := GoFileGenerator{generator}\n\tcontent, err := goGen.buildContent()\n\trequire.NoError(t, err)\n\n\texpectedCallableWrapperSignature := \"func ProcessCallback_wrapper(handle C.uintptr_t, callback *C.zval) unsafe.Pointer\"\n\tassert.Contains(t, content, expectedCallableWrapperSignature, \"Generated content should contain callable wrapper signature: %s\", expectedCallableWrapperSignature)\n\n\texpectedOptionalCallableWrapperSignature := \"func ProcessOptionalCallback_wrapper(handle C.uintptr_t, callback *C.zval) unsafe.Pointer\"\n\tassert.Contains(t, content, expectedOptionalCallableWrapperSignature, \"Generated content should contain optional callable wrapper signature: %s\", expectedOptionalCallableWrapperSignature)\n\n\texpectedCallableCall := \"structObj.ProcessCallback(callback)\"\n\tassert.Contains(t, content, expectedCallableCall, \"Generated content should contain callable method call: %s\", expectedCallableCall)\n\n\texpectedOptionalCallableCall := \"structObj.ProcessOptionalCallback(callback)\"\n\tassert.Contains(t, content, expectedOptionalCallableCall, \"Generated content should contain optional callable method call: %s\", expectedOptionalCallableCall)\n\n\tassert.Contains(t, content, \"//export ProcessCallback_wrapper\", \"Generated content should contain ProcessCallback export directive\")\n\tassert.Contains(t, content, \"//export ProcessOptionalCallback_wrapper\", \"Generated content should contain ProcessOptionalCallback export directive\")\n}\n\nfunc TestGoFileGenerator_phpTypeToGoType(t *testing.T) {\n\tgenerator := &Generator{}\n\tgoGen := GoFileGenerator{generator}\n\n\ttests := []struct {\n\t\tphpType  phpType\n\t\texpected string\n\t}{\n\t\t{phpString, \"string\"},\n\t\t{phpInt, \"int64\"},\n\t\t{phpFloat, \"float64\"},\n\t\t{phpBool, \"bool\"},\n\t\t{phpArray, \"*frankenphp.Array\"},\n\t\t{phpMixed, \"any\"},\n\t\t{phpVoid, \"\"},\n\t\t{phpCallable, \"*C.zval\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(string(tt.phpType), func(t *testing.T) {\n\t\t\tresult := goGen.phpTypeToGoType(tt.phpType)\n\t\t\tassert.Equal(t, tt.expected, result, \"phpTypeToGoType(%s) should return %s\", tt.phpType, tt.expected)\n\t\t})\n\t}\n\n\tt.Run(\"unknown_type\", func(t *testing.T) {\n\t\tunknownType := phpType(\"unknown\")\n\t\tresult := goGen.phpTypeToGoType(unknownType)\n\t\tassert.Equal(t, \"any\", result, \"phpTypeToGoType should fallback to interface{} for unknown types\")\n\t})\n}\n\nfunc testGeneratedFileBasicStructure(t *testing.T, content, expectedPackage, baseName string) {\n\trequiredElements := []string{\n\t\t\"package \" + expectedPackage,\n\t\t\"// #include <stdlib.h>\",\n\t\t`// #include \"` + baseName + `.h\"`,\n\t\t`import \"C\"`,\n\t\t\"func init() {\",\n\t\t\"frankenphp.RegisterExtension(\",\n\t\t\"}\",\n\t}\n\n\tfor _, element := range requiredElements {\n\t\tassert.Contains(t, content, element, \"Generated file should contain: %s\", element)\n\t}\n\n\tassert.NotContains(t, content, \"func internalHelper\", \"Generated file should not contain internal functions from source\")\n\tassert.NotContains(t, content, \"func anotherHelper\", \"Generated file should not contain internal functions from source\")\n}\n\nfunc testGeneratedFileWrappers(t *testing.T, content string, functions []phpFunction) {\n\tfor _, fn := range functions {\n\t\texportDirective := \"//export go_\" + fn.Name\n\t\tassert.Contains(t, content, exportDirective, \"Generated file should contain export directive: %s\", exportDirective)\n\n\t\twrapperFunc := \"func go_\" + fn.Name + \"(\"\n\t\tassert.Contains(t, content, wrapperFunc, \"Generated file should contain wrapper function: %s\", wrapperFunc)\n\n\t\tfuncName := extractGoFunctionName(fn.GoFunction)\n\t\tif funcName != \"\" {\n\t\t\tassert.Contains(t, content, funcName+\"(\", \"Generated wrapper should call original function: %s\", funcName)\n\t\t}\n\t}\n}\n\n// computeFileHash returns SHA256 hash of file\nfunc computeFileHash(t *testing.T, filename string) string {\n\tcontent, err := os.ReadFile(filename)\n\trequire.NoError(t, err)\n\thash := sha256.Sum256(content)\n\treturn hex.EncodeToString(hash[:])\n}\n\n// assertContainsHeaderComment verifies file has autogenerated header\nfunc assertContainsHeaderComment(t *testing.T, filename string) {\n\tcontent, err := os.ReadFile(filename)\n\trequire.NoError(t, err)\n\n\theaderSection := string(content[:min(len(content), 500)])\n\tassert.Contains(t, headerSection, \"AUTOGENERATED FILE - DO NOT EDIT\", \"File should contain autogenerated header comment\")\n\tassert.Contains(t, headerSection, \"FrankenPHP extension generator\", \"File should mention FrankenPHP extension generator\")\n}\n"
  },
  {
    "path": "internal/extgen/hfile.go",
    "content": "// header.go\npackage extgen\n\nimport (\n\t\"bytes\"\n\t_ \"embed\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"text/template\"\n)\n\n//go:embed templates/extension.h.tpl\nvar hFileContent string\n\ntype HeaderGenerator struct {\n\tgenerator *Generator\n}\n\ntype TemplateData struct {\n\tBaseName    string\n\tHeaderGuard string\n\tConstants   []phpConstant\n\tClasses     []phpClass\n}\n\nfunc (hg *HeaderGenerator) generate() error {\n\tfilename := filepath.Join(hg.generator.BuildDir, hg.generator.BaseName+\".h\")\n\tcontent, err := hg.buildContent()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn writeFile(filename, content)\n}\n\nfunc (hg *HeaderGenerator) buildContent() (string, error) {\n\theaderGuard := strings.Map(func(r rune) rune {\n\t\tif r >= 'A' && r <= 'Z' || r >= 'a' && r <= 'z' || r >= '0' && r <= '9' {\n\t\t\treturn r\n\t\t}\n\n\t\treturn '_'\n\t}, hg.generator.BaseName)\n\n\theaderGuard = strings.ToUpper(headerGuard) + \"_H\"\n\n\ttmpl, err := template.New(\"header\").Parse(hFileContent)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tvar buf bytes.Buffer\n\terr = tmpl.Execute(&buf, TemplateData{\n\t\tBaseName:    hg.generator.BaseName,\n\t\tHeaderGuard: headerGuard,\n\t\tConstants:   hg.generator.Constants,\n\t\tClasses:     hg.generator.Classes,\n\t})\n\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn buf.String(), nil\n}\n"
  },
  {
    "path": "internal/extgen/hfile_test.go",
    "content": "package extgen\n\nimport (\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestHeaderGenerator_Generate(t *testing.T) {\n\ttmpDir := t.TempDir()\n\n\tgenerator := &Generator{\n\t\tBaseName: \"test_extension\",\n\t\tBuildDir: tmpDir,\n\t}\n\n\theaderGen := HeaderGenerator{generator}\n\trequire.NoError(t, headerGen.generate())\n\n\texpectedFile := filepath.Join(tmpDir, \"test_extension.h\")\n\trequire.FileExists(t, expectedFile)\n\n\tcontent, err := readFile(expectedFile)\n\trequire.NoError(t, err)\n\n\ttestHeaderBasicStructure(t, content, \"test_extension\")\n\ttestHeaderIncludeGuards(t, content, \"TEST_EXTENSION_H\")\n}\n\nfunc TestHeaderGenerator_BuildContent(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tbaseName string\n\t\tcontains []string\n\t}{\n\t\t{\n\t\t\tname:     \"simple extension\",\n\t\t\tbaseName: \"simple\",\n\t\t\tcontains: []string{\n\t\t\t\t\"#ifndef _SIMPLE_H\",\n\t\t\t\t\"#define _SIMPLE_H\",\n\t\t\t\t\"#include <php.h>\",\n\t\t\t\t\"extern zend_module_entry simple_module_entry;\",\n\t\t\t\t\"#endif\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"extension with hyphens\",\n\t\t\tbaseName: \"my-extension\",\n\t\t\tcontains: []string{\n\t\t\t\t\"#ifndef _MY_EXTENSION_H\",\n\t\t\t\t\"#define _MY_EXTENSION_H\",\n\t\t\t\t\"#endif\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"extension with underscores\",\n\t\t\tbaseName: \"my_extension_name\",\n\t\t\tcontains: []string{\n\t\t\t\t\"#ifndef _MY_EXTENSION_NAME_H\",\n\t\t\t\t\"#define _MY_EXTENSION_NAME_H\",\n\t\t\t\t\"#endif\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"complex extension name\",\n\t\t\tbaseName: \"complex.name-with_symbols\",\n\t\t\tcontains: []string{\n\t\t\t\t\"#ifndef _COMPLEX_NAME_WITH_SYMBOLS_H\",\n\t\t\t\t\"#define _COMPLEX_NAME_WITH_SYMBOLS_H\",\n\t\t\t\t\"#endif\",\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgenerator := &Generator{BaseName: tt.baseName}\n\t\t\theaderGen := HeaderGenerator{generator}\n\t\t\tcontent, err := headerGen.buildContent()\n\t\t\trequire.NoError(t, err)\n\n\t\t\tfor _, expected := range tt.contains {\n\t\t\t\tassert.Contains(t, content, expected, \"Generated header content should contain '%s'\", expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestHeaderGenerator_HeaderGuardGeneration(t *testing.T) {\n\ttests := []struct {\n\t\tbaseName      string\n\t\texpectedGuard string\n\t}{\n\t\t{\"simple\", \"_SIMPLE_H\"},\n\t\t{\"my-extension\", \"_MY_EXTENSION_H\"},\n\t\t{\"complex.name\", \"_COMPLEX_NAME_H\"},\n\t\t{\"under_score\", \"_UNDER_SCORE_H\"},\n\t\t{\"MixedCase\", \"_MIXEDCASE_H\"},\n\t\t{\"123numeric\", \"_123NUMERIC_H\"},\n\t\t{\"special!@#chars\", \"_SPECIAL___CHARS_H\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.baseName, func(t *testing.T) {\n\t\t\tgenerator := &Generator{BaseName: tt.baseName}\n\t\t\theaderGen := HeaderGenerator{generator}\n\t\t\tcontent, err := headerGen.buildContent()\n\t\t\trequire.NoError(t, err)\n\n\t\t\texpectedIfndef := \"#ifndef \" + tt.expectedGuard\n\t\t\texpectedDefine := \"#define \" + tt.expectedGuard\n\n\t\t\tassert.Contains(t, content, expectedIfndef, \"Expected #ifndef %s, but not found in content\", tt.expectedGuard)\n\t\t\tassert.Contains(t, content, expectedDefine, \"Expected #define %s, but not found in content\", tt.expectedGuard)\n\t\t})\n\t}\n}\n\nfunc TestHeaderGenerator_BasicStructure(t *testing.T) {\n\tgenerator := &Generator{BaseName: \"structtest\"}\n\theaderGen := HeaderGenerator{generator}\n\tcontent, err := headerGen.buildContent()\n\trequire.NoError(t, err)\n\n\texpectedElements := []string{\n\t\t\"#include <php.h>\",\n\t\t\"extern zend_module_entry structtest_module_entry;\",\n\t}\n\n\tfor _, element := range expectedElements {\n\t\tassert.Contains(t, content, element, \"Header should contain: %s\", element)\n\t}\n}\n\nfunc TestHeaderGenerator_CompleteStructure(t *testing.T) {\n\tgenerator := &Generator{BaseName: \"complete_test\"}\n\theaderGen := HeaderGenerator{generator}\n\tcontent, err := headerGen.buildContent()\n\trequire.NoError(t, err)\n\n\tlines := strings.Split(content, \"\\n\")\n\n\tassert.GreaterOrEqual(t, len(lines), 5, \"Header file should have multiple lines\")\n\n\tvar foundIfndef, foundDefine, foundEndif bool\n\n\tfor _, line := range lines {\n\t\tline = strings.TrimSpace(line)\n\t\tif line == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tif strings.HasPrefix(line, \"#ifndef\") && !foundIfndef {\n\t\t\tfoundIfndef = true\n\t\t} else if strings.HasPrefix(line, \"#define\") && foundIfndef && !foundDefine {\n\t\t\tfoundDefine = true\n\t\t} else if line == \"#endif\" {\n\t\t\tfoundEndif = true\n\t\t}\n\t}\n\n\tassert.True(t, foundIfndef, \"Header should start with #ifndef guard\")\n\tassert.True(t, foundDefine, \"Header should have #define after #ifndef\")\n\tassert.True(t, foundEndif, \"Header should end with #endif\")\n}\n\nfunc TestHeaderGenerator_ErrorHandling(t *testing.T) {\n\tgenerator := &Generator{\n\t\tBaseName: \"test\",\n\t\tBuildDir: \"/invalid/readonly/path\",\n\t}\n\n\theaderGen := HeaderGenerator{generator}\n\terr := headerGen.generate()\n\tassert.Error(t, err, \"Expected error when writing to invalid directory\")\n}\n\nfunc TestHeaderGenerator_EmptyBaseName(t *testing.T) {\n\tgenerator := &Generator{BaseName: \"\"}\n\theaderGen := HeaderGenerator{generator}\n\tcontent, err := headerGen.buildContent()\n\trequire.NoError(t, err)\n\n\tassert.Contains(t, content, \"#ifndef __H\", \"Header with empty basename should have __H guard\")\n\tassert.Contains(t, content, \"#define __H\", \"Header with empty basename should have __H define\")\n}\n\nfunc TestHeaderGenerator_ContentValidation(t *testing.T) {\n\tgenerator := &Generator{BaseName: \"validation_test\"}\n\theaderGen := HeaderGenerator{generator}\n\tcontent, err := headerGen.buildContent()\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, 1, strings.Count(content, \"#ifndef\"), \"Header should have exactly one #ifndef\")\n\tassert.Equal(t, 1, strings.Count(content, \"#define\"), \"Header should have exactly one #define\")\n\tassert.Equal(t, 1, strings.Count(content, \"#endif\"), \"Header should have exactly one #endif\")\n\tassert.False(t, strings.Contains(content, \"{{\") || strings.Contains(content, \"}}\"), \"Generated header contains unresolved template syntax\")\n}\n\nfunc TestHeaderGenerator_SpecialCharacterHandling(t *testing.T) {\n\ttests := []struct {\n\t\tinput    string\n\t\texpected string\n\t}{\n\t\t{\"normal\", \"NORMAL\"},\n\t\t{\"with-hyphens\", \"WITH_HYPHENS\"},\n\t\t{\"with.dots\", \"WITH_DOTS\"},\n\t\t{\"with_underscores\", \"WITH_UNDERSCORES\"},\n\t\t{\"MixedCASE\", \"MIXEDCASE\"},\n\t\t{\"123numbers\", \"123NUMBERS\"},\n\t\t{\"special!@#$%\", \"SPECIAL_____\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.input, func(t *testing.T) {\n\t\t\tgenerator := &Generator{BaseName: tt.input}\n\t\t\theaderGen := HeaderGenerator{generator}\n\t\t\tcontent, err := headerGen.buildContent()\n\t\t\trequire.NoError(t, err)\n\n\t\t\texpectedGuard := \"_\" + tt.expected + \"_H\"\n\t\t\texpectedIfndef := \"#ifndef \" + expectedGuard\n\t\t\texpectedDefine := \"#define \" + expectedGuard\n\n\t\t\tassert.Contains(t, content, expectedIfndef, \"Expected #ifndef %s for input %s\", expectedGuard, tt.input)\n\t\t\tassert.Contains(t, content, expectedDefine, \"Expected #define %s for input %s\", expectedGuard, tt.input)\n\t\t})\n\t}\n}\n\nfunc TestHeaderGenerator_TemplateErrorHandling(t *testing.T) {\n\tgenerator := &Generator{BaseName: \"error_test\"}\n\theaderGen := HeaderGenerator{generator}\n\n\t_, err := headerGen.buildContent()\n\tassert.NoError(t, err, \"buildContent() should not fail with valid template\")\n}\n\nfunc TestHeaderGenerator_GuardConsistency(t *testing.T) {\n\tbaseName := \"test_consistency\"\n\tgenerator := &Generator{BaseName: baseName}\n\theaderGen := HeaderGenerator{generator}\n\n\tcontent1, err := headerGen.buildContent()\n\trequire.NoError(t, err, \"First buildContent() failed: %v\", err)\n\n\tcontent2, err := headerGen.buildContent()\n\trequire.NoError(t, err, \"Second buildContent() failed: %v\", err)\n\n\tassert.Equal(t, content1, content2, \"Multiple calls to buildContent() should produce identical results\")\n}\n\nfunc TestHeaderGenerator_MinimalContent(t *testing.T) {\n\tgenerator := &Generator{BaseName: \"minimal\"}\n\theaderGen := HeaderGenerator{generator}\n\tcontent, err := headerGen.buildContent()\n\trequire.NoError(t, err)\n\n\tessentialElements := []string{\n\t\t\"#ifndef _MINIMAL_H\",\n\t\t\"#define _MINIMAL_H\",\n\t\t\"#include <php.h>\",\n\t\t\"extern zend_module_entry minimal_module_entry;\",\n\t\t\"#endif\",\n\t}\n\n\tfor _, element := range essentialElements {\n\t\tassert.Contains(t, content, element, \"Minimal header should contain: %s\", element)\n\t}\n}\n\nfunc testHeaderBasicStructure(t *testing.T, content, baseName string) {\n\theaderGuard := strings.Map(func(r rune) rune {\n\t\tif r >= 'A' && r <= 'Z' || r >= 'a' && r <= 'z' || r >= '0' && r <= '9' {\n\t\t\treturn r\n\t\t}\n\n\t\treturn '_'\n\t}, baseName)\n\theaderGuard = strings.ToUpper(headerGuard) + \"_H\"\n\n\trequiredElements := []string{\n\t\t\"#ifndef _\" + headerGuard,\n\t\t\"#define _\" + headerGuard,\n\t\t\"#include <php.h>\",\n\t\t\"extern zend_module_entry test_extension_module_entry;\",\n\t\t\"#endif\",\n\t}\n\n\tfor _, element := range requiredElements {\n\t\tassert.Contains(t, content, element, \"Header file should contain: %s\", element)\n\t}\n}\n\nfunc testHeaderIncludeGuards(t *testing.T, content, expectedGuard string) {\n\texpectedIfndef := \"#ifndef _\" + expectedGuard\n\texpectedDefine := \"#define _\" + expectedGuard\n\n\tassert.Contains(t, content, expectedIfndef, \"Header should contain: %s\", expectedIfndef)\n\tassert.Contains(t, content, expectedDefine, \"Header should contain: %s\", expectedDefine)\n\tassert.Contains(t, content, \"#endif\", \"Header should end with #endif\")\n\n\tifndefPos := strings.Index(content, expectedIfndef)\n\tdefinePos := strings.Index(content, expectedDefine)\n\n\tassert.Less(t, ifndefPos, definePos, \"#ifndef should come before #define\")\n\n\tendifPos := strings.LastIndex(content, \"#endif\")\n\tassert.NotEqual(t, -1, endifPos, \"Header should end with #endif\")\n\tassert.Greater(t, endifPos, definePos, \"#endif should come after #define\")\n}\n"
  },
  {
    "path": "internal/extgen/integration_test.go",
    "content": "//go:build integration\n\npackage extgen\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst (\n\ttestModuleName = \"github.com/frankenphp/test-extension\"\n)\n\ntype IntegrationTestSuite struct {\n\ttempDir        string\n\tgenStubScript  string\n\txcaddyPath     string\n\tfrankenphpPath string\n\tphpConfigPath  string\n\tt              *testing.T\n}\n\nfunc setupTest(t *testing.T) *IntegrationTestSuite {\n\tt.Helper()\n\n\tsuite := &IntegrationTestSuite{t: t}\n\n\tsuite.genStubScript = os.Getenv(\"GEN_STUB_SCRIPT\")\n\tif suite.genStubScript == \"\" {\n\t\tsuite.genStubScript = \"/usr/local/src/php/build/gen_stub.php\"\n\t}\n\n\tif _, err := os.Stat(suite.genStubScript); os.IsNotExist(err) {\n\t\tt.Error(\"GEN_STUB_SCRIPT not found. Integration tests require PHP sources. Set GEN_STUB_SCRIPT environment variable.\")\n\t}\n\n\txcaddyPath, err := exec.LookPath(\"xcaddy\")\n\tif err != nil {\n\t\tt.Error(\"xcaddy not found in PATH. Integration tests require xcaddy to build FrankenPHP.\")\n\t}\n\tsuite.xcaddyPath = xcaddyPath\n\n\tphpConfigPath, err := exec.LookPath(\"php-config\")\n\tif err != nil {\n\t\tt.Error(\"php-config not found in PATH. Integration tests require PHP development headers.\")\n\t}\n\tsuite.phpConfigPath = phpConfigPath\n\n\ttempDir := t.TempDir()\n\tsuite.tempDir = tempDir\n\n\treturn suite\n}\n\nfunc (s *IntegrationTestSuite) createGoModule(sourceFile string) (string, error) {\n\ts.t.Helper()\n\n\tmoduleDir := filepath.Join(s.tempDir, \"module\")\n\tif err := os.MkdirAll(moduleDir, 0o755); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create module directory: %w\", err)\n\t}\n\n\t// Get project root for replace directive\n\tprojectRoot, err := filepath.Abs(filepath.Join(\"..\", \"..\"))\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get project root: %w\", err)\n\t}\n\n\tgoModContent := fmt.Sprintf(`module %s\n\ngo 1.26\n\nrequire github.com/dunglas/frankenphp v0.0.0\n\nreplace github.com/dunglas/frankenphp => %s\n`, testModuleName, projectRoot)\n\n\tif err := os.WriteFile(filepath.Join(moduleDir, \"go.mod\"), []byte(goModContent), 0o644); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create go.mod: %w\", err)\n\t}\n\n\tsourceContent, err := os.ReadFile(sourceFile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read source file: %w\", err)\n\t}\n\n\ttargetFile := filepath.Join(moduleDir, filepath.Base(sourceFile))\n\tif err := os.WriteFile(targetFile, sourceContent, 0o644); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to write source file: %w\", err)\n\t}\n\n\treturn targetFile, nil\n}\n\nfunc (s *IntegrationTestSuite) runExtensionInit(sourceFile string) error {\n\ts.t.Helper()\n\n\tos.Setenv(\"GEN_STUB_SCRIPT\", s.genStubScript)\n\n\tbaseName := SanitizePackageName(strings.TrimSuffix(filepath.Base(sourceFile), \".go\"))\n\tgenerator := Generator{\n\t\tBaseName:   baseName,\n\t\tSourceFile: sourceFile,\n\t\tBuildDir:   filepath.Dir(sourceFile),\n\t}\n\n\tif err := generator.Generate(); err != nil {\n\t\treturn fmt.Errorf(\"generation failed: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// cleanupGeneratedFiles removes generated files from the original source directory\nfunc (s *IntegrationTestSuite) cleanupGeneratedFiles(originalSourceFile string) {\n\ts.t.Helper()\n\n\tsourceDir := filepath.Dir(originalSourceFile)\n\tbaseName := SanitizePackageName(strings.TrimSuffix(filepath.Base(originalSourceFile), \".go\"))\n\n\tgeneratedFiles := []string{\n\t\tbaseName + \".stub.php\",\n\t\tbaseName + \"_arginfo.h\",\n\t\tbaseName + \".h\",\n\t\tbaseName + \".c\",\n\t\tbaseName + \".go\",\n\t\t\"README.md\",\n\t}\n\n\tfor _, file := range generatedFiles {\n\t\tfullPath := filepath.Join(sourceDir, file)\n\t\tif _, err := os.Stat(fullPath); err == nil {\n\t\t\tos.Remove(fullPath)\n\t\t}\n\t}\n}\n\n// compileFrankenPHP compiles FrankenPHP with the generated extension\nfunc (s *IntegrationTestSuite) compileFrankenPHP(moduleDir string) (string, error) {\n\ts.t.Helper()\n\n\tprojectRoot, err := filepath.Abs(filepath.Join(\"..\", \"..\"))\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get project root: %w\", err)\n\t}\n\n\tcflags, err := exec.Command(s.phpConfigPath, \"--includes\").Output()\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get PHP includes: %w\", err)\n\t}\n\n\tldflags, err := exec.Command(s.phpConfigPath, \"--ldflags\").Output()\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get PHP ldflags: %w\", err)\n\t}\n\n\tlibs, err := exec.Command(s.phpConfigPath, \"--libs\").Output()\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get PHP libs: %w\", err)\n\t}\n\n\tcgoCflags := strings.TrimSpace(string(cflags))\n\tcgoLdflags := strings.TrimSpace(string(ldflags)) + \" \" + strings.TrimSpace(string(libs))\n\n\toutputBinary := filepath.Join(s.tempDir, \"frankenphp\")\n\n\tcmd := exec.Command(\n\t\ts.xcaddyPath,\n\t\t\"build\",\n\t\t\"--output\", outputBinary,\n\t\t\"--with\", \"github.com/dunglas/frankenphp=\"+projectRoot,\n\t\t\"--with\", \"github.com/dunglas/frankenphp/caddy=\"+projectRoot+\"/caddy\",\n\t\t\"--with\", testModuleName+\"=\"+moduleDir,\n\t)\n\n\tcmd.Env = append(os.Environ(),\n\t\t\"CGO_ENABLED=1\",\n\t\t\"CGO_CFLAGS=\"+cgoCflags,\n\t\t\"CGO_LDFLAGS=\"+cgoLdflags,\n\t\tfmt.Sprintf(\"XCADDY_GO_BUILD_FLAGS=-ldflags='-w -s' -tags=nobadger,nomysql,nopgx,nowatcher\"),\n\t)\n\n\tcmd.Dir = s.tempDir\n\n\toutput, err := cmd.CombinedOutput()\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"xcaddy build failed: %w\\nOutput: %s\", err, string(output))\n\t}\n\n\ts.frankenphpPath = outputBinary\n\treturn outputBinary, nil\n}\n\nfunc (s *IntegrationTestSuite) runPHPCode(phpCode string) (string, error) {\n\ts.t.Helper()\n\n\tif s.frankenphpPath == \"\" {\n\t\treturn \"\", fmt.Errorf(\"FrankenPHP not compiled yet\")\n\t}\n\n\tphpFile := filepath.Join(s.tempDir, \"test.php\")\n\tif err := os.WriteFile(phpFile, []byte(phpCode), 0o644); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create PHP file: %w\", err)\n\t}\n\n\tcmd := exec.Command(s.frankenphpPath, \"php-cli\", phpFile)\n\toutput, err := cmd.CombinedOutput()\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"PHP execution failed: %w\\nOutput: %s\", err, string(output))\n\t}\n\n\treturn string(output), nil\n}\n\n// verifyPHPSymbols checks if PHP can find the exposed functions, classes, and constants\nfunc (s *IntegrationTestSuite) verifyPHPSymbols(functions []string, classes []string, constants []string) error {\n\ts.t.Helper()\n\n\tvar checks []string\n\n\tfor _, fn := range functions {\n\t\tchecks = append(checks, fmt.Sprintf(\"if (!function_exists('%s')) { echo 'MISSING_FUNCTION: %s'; exit(1); }\", fn, fn))\n\t}\n\n\tfor _, cls := range classes {\n\t\tchecks = append(checks, fmt.Sprintf(\"if (!class_exists('%s')) { echo 'MISSING_CLASS: %s'; exit(1); }\", cls, cls))\n\t}\n\n\tfor _, cnst := range constants {\n\t\tchecks = append(checks, fmt.Sprintf(\"if (!defined('%s')) { echo 'MISSING_CONSTANT: %s'; exit(1); }\", cnst, cnst))\n\t}\n\n\tchecks = append(checks, \"echo 'OK';\")\n\n\tphpCode := \"<?php\\n\" + strings.Join(checks, \"\\n\")\n\n\toutput, err := s.runPHPCode(phpCode)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif !strings.Contains(output, \"OK\") {\n\t\treturn fmt.Errorf(\"symbol verification failed: %s\", output)\n\t}\n\n\treturn nil\n}\n\nfunc (s *IntegrationTestSuite) verifyFunctionBehavior(phpCode string, expectedOutput string) error {\n\ts.t.Helper()\n\n\toutput, err := s.runPHPCode(phpCode)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif !strings.Contains(output, expectedOutput) {\n\t\treturn fmt.Errorf(\"unexpected output.\\nExpected to contain: %q\\nGot: %q\", expectedOutput, output)\n\t}\n\n\treturn nil\n}\n\nfunc TestBasicFunction(t *testing.T) {\n\tsuite := setupTest(t)\n\n\tsourceFile := filepath.Join(\"..\", \"..\", \"testdata\", \"integration\", \"basic_function.go\")\n\tsourceFile, err := filepath.Abs(sourceFile)\n\trequire.NoError(t, err)\n\tdefer suite.cleanupGeneratedFiles(sourceFile)\n\n\ttargetFile, err := suite.createGoModule(sourceFile)\n\trequire.NoError(t, err)\n\n\terr = suite.runExtensionInit(targetFile)\n\trequire.NoError(t, err, \"extension-init should succeed\")\n\n\tbaseDir := filepath.Dir(targetFile)\n\tbaseName := strings.TrimSuffix(filepath.Base(targetFile), \".go\")\n\n\texpectedFiles := []string{\n\t\tbaseName + \".stub.php\",\n\t\tbaseName + \"_arginfo.h\",\n\t\tbaseName + \".h\",\n\t\tbaseName + \".c\",\n\t\tbaseName + \".go\",\n\t\t\"README.md\",\n\t}\n\n\tfor _, file := range expectedFiles {\n\t\tfullPath := filepath.Join(baseDir, file)\n\t\tassert.FileExists(t, fullPath, \"Generated file should exist: %s\", file)\n\t}\n\n\t_, err = suite.compileFrankenPHP(filepath.Dir(targetFile))\n\trequire.NoError(t, err, \"FrankenPHP compilation should succeed\")\n\n\terr = suite.verifyPHPSymbols(\n\t\t[]string{\"test_uppercase\", \"test_add_numbers\", \"test_multiply\", \"test_is_enabled\"},\n\t\t[]string{},\n\t\t[]string{},\n\t)\n\trequire.NoError(t, err, \"all functions should be accessible from PHP\")\n\n\terr = suite.verifyFunctionBehavior(`<?php\n$result = test_uppercase(\"hello world\");\nif ($result !== \"HELLO WORLD\") {\n\techo \"FAIL: test_uppercase expected 'HELLO WORLD', got '$result'\";\n\texit(1);\n}\n\n$result = test_uppercase(\"\");\nif ($result !== \"\") {\n\techo \"FAIL: test_uppercase with empty string expected '', got '$result'\";\n\texit(1);\n}\n\n$sum = test_add_numbers(5, 7);\nif ($sum !== 12) {\n\techo \"FAIL: test_add_numbers(5, 7) expected 12, got $sum\";\n\texit(1);\n}\n\n$result = test_is_enabled(true);\nif ($result !== false) {\n\techo \"FAIL: test_is_enabled(true) expected false, got \" . ($result ? \"true\" : \"false\");\n\texit(1);\n}\n\n$result = test_is_enabled(false);\nif ($result !== true) {\n\techo \"FAIL: test_is_enabled(false) expected true, got \" . ($result ? \"true\" : \"false\");\n\texit(1);\n}\n\necho \"OK\";\n`, \"OK\")\n\trequire.NoError(t, err, \"all function calls should work correctly\")\n}\n\nfunc TestClassMethodsIntegration(t *testing.T) {\n\tsuite := setupTest(t)\n\n\tsourceFile := filepath.Join(\"..\", \"..\", \"testdata\", \"integration\", \"class_methods.go\")\n\tsourceFile, err := filepath.Abs(sourceFile)\n\trequire.NoError(t, err)\n\tdefer suite.cleanupGeneratedFiles(sourceFile)\n\n\ttargetFile, err := suite.createGoModule(sourceFile)\n\trequire.NoError(t, err)\n\n\terr = suite.runExtensionInit(targetFile)\n\trequire.NoError(t, err)\n\n\t_, err = suite.compileFrankenPHP(filepath.Dir(targetFile))\n\trequire.NoError(t, err)\n\n\terr = suite.verifyPHPSymbols(\n\t\t[]string{},\n\t\t[]string{\"Counter\", \"StringHolder\"},\n\t\t[]string{},\n\t)\n\trequire.NoError(t, err, \"all classes should be accessible from PHP\")\n\n\terr = suite.verifyFunctionBehavior(`<?php\n$counter = new Counter();\nif ($counter->getValue() !== 0) {\n\techo \"FAIL: Counter initial value expected 0, got \" . $counter->getValue();\n\texit(1);\n}\n\n$counter->increment();\nif ($counter->getValue() !== 1) {\n\techo \"FAIL: Counter after increment expected 1, got \" . $counter->getValue();\n\texit(1);\n}\n\n$counter->decrement();\nif ($counter->getValue() !== 0) {\n\techo \"FAIL: Counter after decrement expected 0, got \" . $counter->getValue();\n\texit(1);\n}\n\n$counter->setValue(10);\nif ($counter->getValue() !== 10) {\n\techo \"FAIL: Counter after setValue(10) expected 10, got \" . $counter->getValue();\n\texit(1);\n}\n\n$newValue = $counter->addValue(5);\nif ($newValue !== 15) {\n\techo \"FAIL: Counter addValue(5) expected to return 15, got $newValue\";\n\texit(1);\n}\nif ($counter->getValue() !== 15) {\n\techo \"FAIL: Counter value after addValue(5) expected 15, got \" . $counter->getValue();\n\texit(1);\n}\n\n$counter->updateWithNullable(50);\nif ($counter->getValue() !== 50) {\n\techo \"FAIL: Counter after updateWithNullable(50) expected 50, got \" . $counter->getValue();\n\texit(1);\n}\n\n$counter->updateWithNullable(null);\nif ($counter->getValue() !== 50) {\n\techo \"FAIL: Counter after updateWithNullable(null) expected 50 (unchanged), got \" . $counter->getValue();\n\texit(1);\n}\n\n$counter->reset();\nif ($counter->getValue() !== 0) {\n\techo \"FAIL: Counter after reset expected 0, got \" . $counter->getValue();\n\texit(1);\n}\n\n$counter1 = new Counter();\n$counter2 = new Counter();\n$counter1->setValue(100);\n$counter2->setValue(200);\nif ($counter1->getValue() !== 100 || $counter2->getValue() !== 200) {\n\techo \"FAIL: Multiple Counter instances should be independent\";\n\texit(1);\n}\n\n$holder = new StringHolder();\n$holder->setData(\"test string\");\nif ($holder->getData() !== \"test string\") {\n\techo \"FAIL: StringHolder getData expected 'test string', got '\" . $holder->getData() . \"'\";\n\texit(1);\n}\nif ($holder->getLength() !== 11) {\n\techo \"FAIL: StringHolder getLength expected 11, got \" . $holder->getLength();\n\texit(1);\n}\n\n$holder->setData(\"\");\nif ($holder->getData() !== \"\") {\n\techo \"FAIL: StringHolder empty string expected '', got '\" . $holder->getData() . \"'\";\n\texit(1);\n}\nif ($holder->getLength() !== 0) {\n\techo \"FAIL: StringHolder empty string length expected 0, got \" . $holder->getLength();\n\texit(1);\n}\n\necho \"OK\";\n`, \"OK\")\n\trequire.NoError(t, err, \"all class methods should work correctly\")\n}\n\nfunc TestConstants(t *testing.T) {\n\tsuite := setupTest(t)\n\n\tsourceFile := filepath.Join(\"..\", \"..\", \"testdata\", \"integration\", \"constants.go\")\n\tsourceFile, err := filepath.Abs(sourceFile)\n\trequire.NoError(t, err)\n\tdefer suite.cleanupGeneratedFiles(sourceFile)\n\n\ttargetFile, err := suite.createGoModule(sourceFile)\n\trequire.NoError(t, err)\n\n\terr = suite.runExtensionInit(targetFile)\n\trequire.NoError(t, err)\n\n\t_, err = suite.compileFrankenPHP(filepath.Dir(targetFile))\n\trequire.NoError(t, err)\n\n\terr = suite.verifyPHPSymbols(\n\t\t[]string{\"test_with_constants\"},\n\t\t[]string{\"Config\"},\n\t\t[]string{\n\t\t\t\"TEST_MAX_RETRIES\", \"TEST_API_VERSION\", \"TEST_ENABLED\", \"TEST_PI\",\n\t\t\t\"STATUS_PENDING\", \"STATUS_PROCESSING\", \"STATUS_COMPLETED\",\n\t\t\t\"ONE\", \"TWO\",\n\t\t},\n\t)\n\trequire.NoError(t, err, \"all constants, functions, and classes should be accessible from PHP\")\n\n\terr = suite.verifyFunctionBehavior(`<?php\nif (TEST_MAX_RETRIES !== 100) {\n\techo \"FAIL: TEST_MAX_RETRIES expected 100, got \" . TEST_MAX_RETRIES;\n\texit(1);\n}\n\nif (TEST_API_VERSION !== \"2.0.0\") {\n\techo \"FAIL: TEST_API_VERSION expected '2.0.0', got '\" . TEST_API_VERSION . \"'\";\n\texit(1);\n}\n\nif (TEST_ENABLED !== true) {\nvar_dump(TEST_ENABLED);\n\techo \"FAIL: TEST_ENABLED expected true, got \" . (TEST_ENABLED ? \"true\" : \"false\");\n\texit(1);\n}\n\nif (abs(TEST_PI - 3.14159) > 0.00001) {\n\techo \"FAIL: TEST_PI expected 3.14159, got \" . TEST_PI;\n\texit(1);\n}\n\nif (Config::MODE_DEBUG !== 1) {\n\techo \"FAIL: Config::MODE_DEBUG expected 1, got \" . Config::MODE_DEBUG;\n\texit(1);\n}\n\nif (Config::MODE_PRODUCTION !== 2) {\n\techo \"FAIL: Config::MODE_PRODUCTION expected 2, got \" . Config::MODE_PRODUCTION;\n\texit(1);\n}\n\nif (Config::DEFAULT_TIMEOUT !== 30) {\n\techo \"FAIL: Config::DEFAULT_TIMEOUT expected 30, got \" . Config::DEFAULT_TIMEOUT;\n\texit(1);\n}\n\n$config = new Config();\n$config->setMode(Config::MODE_DEBUG);\nif ($config->getMode() !== Config::MODE_DEBUG) {\n\techo \"FAIL: Config getMode expected MODE_DEBUG, got \" . $config->getMode();\n\texit(1);\n}\n\n$result = test_with_constants(STATUS_PENDING);\nif ($result !== \"pending\") {\n\techo \"FAIL: test_with_constants(STATUS_PENDING) expected 'pending', got '$result'\";\n\texit(1);\n}\n\n$result = test_with_constants(STATUS_PROCESSING);\nif ($result !== \"processing\") {\n\techo \"FAIL: test_with_constants(STATUS_PROCESSING) expected 'processing', got '$result'\";\n\texit(1);\n}\n\n$result = test_with_constants(STATUS_COMPLETED);\nif ($result !== \"completed\") {\n\techo \"FAIL: test_with_constants(STATUS_COMPLETED) expected 'completed', got '$result'\";\n\texit(1);\n}\n\n$result = test_with_constants(999);\nif ($result !== \"unknown\") {\n\techo \"FAIL: test_with_constants(999) expected 'unknown', got '$result'\";\n\texit(1);\n}\n\necho \"OK\";\n`, \"OK\")\n\trequire.NoError(t, err, \"all constants should have correct values and functions should work\")\n}\n\nfunc TestNamespace(t *testing.T) {\n\tsuite := setupTest(t)\n\n\tsourceFile := filepath.Join(\"..\", \"..\", \"testdata\", \"integration\", \"namespace.go\")\n\tsourceFile, err := filepath.Abs(sourceFile)\n\trequire.NoError(t, err)\n\tdefer suite.cleanupGeneratedFiles(sourceFile)\n\n\ttargetFile, err := suite.createGoModule(sourceFile)\n\trequire.NoError(t, err)\n\n\terr = suite.runExtensionInit(targetFile)\n\trequire.NoError(t, err)\n\n\t_, err = suite.compileFrankenPHP(filepath.Dir(targetFile))\n\trequire.NoError(t, err)\n\n\terr = suite.verifyPHPSymbols(\n\t\t[]string{`\\\\TestIntegration\\\\Extension\\\\greet`},\n\t\t[]string{`\\\\TestIntegration\\\\Extension\\\\Person`},\n\t\t[]string{`\\\\TestIntegration\\\\Extension\\\\NAMESPACE_VERSION`},\n\t)\n\trequire.NoError(t, err, \"all namespaced symbols should be accessible from PHP\")\n\n\terr = suite.verifyFunctionBehavior(`<?php\nuse TestIntegration\\Extension;\n\nif (Extension\\NAMESPACE_VERSION !== \"1.0.0\") {\n\techo \"FAIL: NAMESPACE_VERSION expected '1.0.0', got '\" . Extension\\NAMESPACE_VERSION . \"'\";\n\texit(1);\n}\n\n$greeting = Extension\\greet(\"Alice\");\nif ($greeting !== \"Hello, Alice!\") {\n\techo \"FAIL: greet('Alice') expected 'Hello, Alice!', got '$greeting'\";\n\texit(1);\n}\n\n$greeting = Extension\\greet(\"\");\nif ($greeting !== \"Hello, !\") {\n\techo \"FAIL: greet('') expected 'Hello, !', got '$greeting'\";\n\texit(1);\n}\n\nif (Extension\\Person::DEFAULT_AGE !== 18) {\n\techo \"FAIL: Person::DEFAULT_AGE expected 18, got \" . Extension\\Person::DEFAULT_AGE;\n\texit(1);\n}\n\n$person = new Extension\\Person();\n$person->setName(\"Bob\");\n$person->setAge(25);\n\nif ($person->getName() !== \"Bob\") {\n\techo \"FAIL: Person getName expected 'Bob', got '\" . $person->getName() . \"'\";\n\texit(1);\n}\n\nif ($person->getAge() !== 25) {\n\techo \"FAIL: Person getAge expected 25, got \" . $person->getAge();\n\texit(1);\n}\n\n$person->setAge(Extension\\Person::DEFAULT_AGE);\nif ($person->getAge() !== 18) {\n\techo \"FAIL: Person setAge(DEFAULT_AGE) expected 18, got \" . $person->getAge();\n\texit(1);\n}\n\n$person1 = new Extension\\Person();\n$person2 = new Extension\\Person();\n$person1->setName(\"Alice\");\n$person1->setAge(30);\n$person2->setName(\"Charlie\");\n$person2->setAge(40);\n\nif ($person1->getName() !== \"Alice\" || $person1->getAge() !== 30) {\n\techo \"FAIL: person1 should have independent state\";\n\texit(1);\n}\nif ($person2->getName() !== \"Charlie\" || $person2->getAge() !== 40) {\n\techo \"FAIL: person2 should have independent state\";\n\texit(1);\n}\n\necho \"OK\";\n`, \"OK\")\n\trequire.NoError(t, err, \"all namespaced symbols should work correctly\")\n}\n\nfunc TestInvalidSignature(t *testing.T) {\n\tsuite := setupTest(t)\n\n\tsourceFile := filepath.Join(\"..\", \"..\", \"testdata\", \"integration\", \"invalid_signature.go\")\n\tsourceFile, err := filepath.Abs(sourceFile)\n\trequire.NoError(t, err)\n\tdefer suite.cleanupGeneratedFiles(sourceFile)\n\n\ttargetFile, err := suite.createGoModule(sourceFile)\n\trequire.NoError(t, err)\n\n\terr = suite.runExtensionInit(targetFile)\n\tassert.Error(t, err, \"extension-init should fail for invalid return type\")\n\tassert.Contains(t, err.Error(), \"no PHP functions, classes, or constants found\", \"invalid functions should be ignored, resulting in no valid exports\")\n}\n\nfunc TestTypeMismatch(t *testing.T) {\n\tsuite := setupTest(t)\n\n\tsourceFile := filepath.Join(\"..\", \"..\", \"testdata\", \"integration\", \"type_mismatch.go\")\n\tsourceFile, err := filepath.Abs(sourceFile)\n\trequire.NoError(t, err)\n\tdefer suite.cleanupGeneratedFiles(sourceFile)\n\n\ttargetFile, err := suite.createGoModule(sourceFile)\n\trequire.NoError(t, err)\n\n\terr = suite.runExtensionInit(targetFile)\n\tassert.NoError(t, err, \"generation should succeed - class is valid even though function/method have type mismatches\")\n\n\tbaseDir := filepath.Dir(targetFile)\n\tbaseName := strings.TrimSuffix(filepath.Base(targetFile), \".go\")\n\tstubFile := filepath.Join(baseDir, baseName+\".stub.php\")\n\tassert.FileExists(t, stubFile, \"stub file should be generated for valid class\")\n}\n\nfunc TestMissingGenStub(t *testing.T) {\n\t// temp override of GEN_STUB_SCRIPT\n\toriginalValue := os.Getenv(\"GEN_STUB_SCRIPT\")\n\tdefer os.Setenv(\"GEN_STUB_SCRIPT\", originalValue)\n\n\tos.Setenv(\"GEN_STUB_SCRIPT\", \"/nonexistent/gen_stub.php\")\n\n\ttempDir := t.TempDir()\n\tsourceFile := filepath.Join(tempDir, \"test.go\")\n\n\terr := os.WriteFile(sourceFile, []byte(`package test\n\n//export_php:function dummy(): void\nfunc dummy() {}\n`), 0o644)\n\trequire.NoError(t, err)\n\n\tbaseName := SanitizePackageName(strings.TrimSuffix(filepath.Base(sourceFile), \".go\"))\n\tgen := Generator{\n\t\tBaseName:   baseName,\n\t\tSourceFile: sourceFile,\n\t\tBuildDir:   filepath.Dir(sourceFile),\n\t}\n\n\terr = gen.Generate()\n\tassert.Error(t, err, \"should fail when gen_stub.php is missing\")\n\tassert.Contains(t, err.Error(), \"gen_stub.php\", \"error should mention missing script\")\n}\n\nfunc TestCallable(t *testing.T) {\n\tsuite := setupTest(t)\n\n\tsourceFile := filepath.Join(\"..\", \"..\", \"testdata\", \"integration\", \"callable.go\")\n\tsourceFile, err := filepath.Abs(sourceFile)\n\trequire.NoError(t, err)\n\tdefer suite.cleanupGeneratedFiles(sourceFile)\n\n\ttargetFile, err := suite.createGoModule(sourceFile)\n\trequire.NoError(t, err)\n\n\terr = suite.runExtensionInit(targetFile)\n\trequire.NoError(t, err)\n\n\t_, err = suite.compileFrankenPHP(filepath.Dir(targetFile))\n\trequire.NoError(t, err)\n\n\terr = suite.verifyPHPSymbols(\n\t\t[]string{\"my_array_map\", \"my_filter\"},\n\t\t[]string{\"Processor\"},\n\t\t[]string{},\n\t)\n\trequire.NoError(t, err, \"all functions and classes should be accessible from PHP\")\n\n\terr = suite.verifyFunctionBehavior(`<?php\n\n$result = my_array_map([1, 2, 3], function($x) { return $x * 2; });\nif ($result !== [2, 4, 6]) {\n\techo \"FAIL: my_array_map with closure expected [2, 4, 6], got \" . json_encode($result);\n\texit(1);\n}\n\n$result = my_array_map(['hello', 'world'], 'strtoupper');\nif ($result !== ['HELLO', 'WORLD']) {\n\techo \"FAIL: my_array_map with function name expected ['HELLO', 'WORLD'], got \" . json_encode($result);\n\texit(1);\n}\n\n$result = my_array_map([], function($x) { return $x; });\nif ($result !== []) {\n\techo \"FAIL: my_array_map with empty array expected [], got \" . json_encode($result);\n\texit(1);\n}\n\n$result = my_filter([1, 2, 3, 4, 5, 6], function($x) { return $x % 2 === 0; });\nif ($result !== [2, 4, 6]) {\n\techo \"FAIL: my_filter expected [2, 4, 6], got \" . json_encode($result);\n\texit(1);\n}\n\n$result = my_filter([1, 2, 3, 4], null);\nif ($result !== [1, 2, 3, 4]) {\n\techo \"FAIL: my_filter with null callback expected [1, 2, 3, 4], got \" . json_encode($result);\n\texit(1);\n}\n\n$processor = new Processor();\n$result = $processor->transform('hello', function($s) { return strtoupper($s); });\nif ($result !== 'HELLO') {\n\techo \"FAIL: Processor::transform with closure expected 'HELLO', got '$result'\";\n\texit(1);\n}\n\n$result = $processor->transform('world', 'strtoupper');\nif ($result !== 'WORLD') {\n\techo \"FAIL: Processor::transform with function name expected 'WORLD', got '$result'\";\n\texit(1);\n}\n\n$result = $processor->transform('  test  ', 'trim');\nif ($result !== 'test') {\n\techo \"FAIL: Processor::transform with trim expected 'test', got '$result'\";\n\texit(1);\n}\n\necho \"OK\";\n`, \"OK\")\n\trequire.NoError(t, err, \"all callable tests should pass\")\n}\n"
  },
  {
    "path": "internal/extgen/namespace_test.go",
    "content": "package extgen\n\nimport (\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestNamespaceParser(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\tcontent     string\n\t\texpected    string\n\t\tshouldError bool\n\t}{\n\t\t{\n\t\t\tname: \"basic namespace\",\n\t\t\tcontent: `package main\n\n//export_php:namespace My\\Test\\Namespace\n\nfunc main() {}`,\n\t\t\texpected: `My\\Test\\Namespace`,\n\t\t},\n\t\t{\n\t\t\tname: \"namespace with spaces\",\n\t\t\tcontent: `package main\n\n//export_php:namespace   My\\Test\\Namespace   \n\nfunc main() {}`,\n\t\t\texpected: `My\\Test\\Namespace`,\n\t\t},\n\t\t{\n\t\t\tname: \"no namespace\",\n\t\t\tcontent: `package main\n\nfunc main() {}`,\n\t\t\texpected: \"\",\n\t\t},\n\t\t{\n\t\t\tname: \"multiple namespaces should error\",\n\t\t\tcontent: `package main\n\n//export_php:namespace First\\Namespace\n//export_php:namespace Second\\Namespace\n\nfunc main() {}`,\n\t\t\texpected:    \"\",\n\t\t\tshouldError: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ttmpfile, err := os.CreateTemp(\"\", \"test_namespace_*.go\")\n\t\t\trequire.NoError(t, err, \"Failed to create temp file\")\n\t\t\tdefer func() {\n\t\t\t\terr := os.Remove(tmpfile.Name())\n\t\t\t\tassert.NoError(t, err, \"Failed to remove temp file: %v\", err)\n\t\t\t}()\n\n\t\t\t_, err = tmpfile.Write([]byte(tt.content))\n\t\t\trequire.NoError(t, err, \"Failed to write to temp file\")\n\n\t\t\terr = tmpfile.Close()\n\t\t\trequire.NoError(t, err, \"Failed to close temp file\")\n\n\t\t\tparser := NamespaceParser{}\n\t\t\tresult, err := parser.parse(tmpfile.Name())\n\n\t\t\tif tt.shouldError {\n\t\t\t\trequire.Error(t, err, \"expected error but got none\")\n\t\t\t\treturn\n\t\t\t}\n\t\t\trequire.NoError(t, err, \"unexpected error\")\n\t\t\trequire.Equal(t, tt.expected, result, \"expected %q, got %q\", tt.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestGeneratorWithNamespace(t *testing.T) {\n\tcontent := `package main\n\n//export_php:namespace My\\Test\\Namespace\n\n//export_php:function hello(): string\nfunc hello() string {\n\treturn \"Hello from namespace!\"\n}\n\n//export_php:constant TEST_CONSTANT = \"test_value\"\nconst TEST_CONSTANT = \"test_value\"\n`\n\n\ttmpfile, err := os.CreateTemp(\"\", \"test_generator_namespace_*.go\")\n\trequire.NoError(t, err, \"Failed to create temp file\")\n\tdefer func() {\n\t\tif err := os.Remove(tmpfile.Name()); err != nil {\n\t\t\tt.Logf(\"Failed to remove temp file: %v\", err)\n\t\t}\n\t}()\n\n\t_, err = tmpfile.Write([]byte(content))\n\trequire.NoError(t, err, \"Failed to write to temp file\")\n\n\terr = tmpfile.Close()\n\trequire.NoError(t, err, \"Failed to close temp file\")\n\n\tparser := SourceParser{}\n\tnamespace, err := parser.ParseNamespace(tmpfile.Name())\n\trequire.NoErrorf(t, err, \"Failed to parse namespace from %s: %v\", tmpfile.Name(), err)\n\n\trequire.Equal(t, `My\\Test\\Namespace`, namespace, \"Namespace should match the parsed namespace\")\n\n\tgenerator := &Generator{\n\t\tSourceFile: tmpfile.Name(),\n\t\tNamespace:  namespace,\n\t}\n\n\trequire.Equal(t, `My\\Test\\Namespace`, generator.Namespace, \"Namespace should match the parsed namespace\")\n}\n"
  },
  {
    "path": "internal/extgen/nodes.go",
    "content": "package extgen\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n)\n\n// phpType represents a PHP type\ntype phpType string\n\nconst (\n\tphpString   phpType = \"string\"\n\tphpInt      phpType = \"int\"\n\tphpFloat    phpType = \"float\"\n\tphpBool     phpType = \"bool\"\n\tphpArray    phpType = \"array\"\n\tphpObject   phpType = \"object\"\n\tphpMixed    phpType = \"mixed\"\n\tphpVoid     phpType = \"void\"\n\tphpNull     phpType = \"null\"\n\tphpTrue     phpType = \"true\"\n\tphpFalse    phpType = \"false\"\n\tphpCallable phpType = \"callable\"\n)\n\ntype phpFunction struct {\n\tName             string\n\tSignature        string\n\tGoFunction       string\n\tParams           []phpParameter\n\tReturnType       phpType\n\tIsReturnNullable bool\n\tlineNumber       int\n}\n\ntype phpParameter struct {\n\tName         string\n\tPhpType      phpType\n\tIsNullable   bool\n\tDefaultValue string\n\tHasDefault   bool\n}\n\ntype phpClass struct {\n\tName       string\n\tGoStruct   string\n\tProperties []phpClassProperty\n\tMethods    []phpClassMethod\n}\n\ntype phpClassMethod struct {\n\tName             string\n\tPhpName          string\n\tSignature        string\n\tGoFunction       string\n\tWrapper          string\n\tParams           []phpParameter\n\tReturnType       phpType\n\tisReturnNullable bool\n\tlineNumber       int\n\tClassName        string // used by the \"//export_php:method\" directive\n}\n\ntype phpClassProperty struct {\n\tName       string\n\tPhpType    phpType\n\tGoType     string\n\tIsNullable bool\n}\n\ntype phpConstant struct {\n\tName       string\n\tValue      string\n\tPhpType    phpType\n\tIsIota     bool\n\tlineNumber int\n\tClassName  string // empty for global constants, set for class constants\n}\n\n// CValue returns the constant value in C-compatible format\nfunc (c phpConstant) CValue() string {\n\tif c.PhpType != phpInt {\n\t\treturn c.Value\n\t}\n\n\tif strings.HasPrefix(c.Value, \"0o\") {\n\t\tif val, err := strconv.ParseInt(c.Value, 0, 64); err == nil {\n\t\t\treturn strconv.FormatInt(val, 10)\n\t\t}\n\t}\n\n\treturn c.Value\n}\n"
  },
  {
    "path": "internal/extgen/nsparser.go",
    "content": "package extgen\n\nimport (\n\t\"bufio\"\n\t\"fmt\"\n\t\"os\"\n\t\"regexp\"\n\t\"strings\"\n)\n\ntype NamespaceParser struct{}\n\nvar namespaceRegex = regexp.MustCompile(`//\\s*export_php:namespace\\s+(.+)`)\n\nfunc (np *NamespaceParser) parse(filename string) (string, error) {\n\tfile, err := os.Open(filename)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdefer func() {\n\t\tif err := file.Close(); err != nil {\n\t\t\tfmt.Printf(\"Error closing file %s: %v\\n\", filename, err)\n\t\t}\n\t}()\n\n\tvar foundNamespace string\n\tvar lineNumber int\n\tvar foundLineNumber int\n\n\tscanner := bufio.NewScanner(file)\n\tfor scanner.Scan() {\n\t\tlineNumber++\n\t\tline := strings.TrimSpace(scanner.Text())\n\t\tif matches := namespaceRegex.FindStringSubmatch(line); matches != nil {\n\t\t\tnamespace := strings.TrimSpace(matches[1])\n\t\t\tif foundNamespace != \"\" {\n\t\t\t\treturn \"\", fmt.Errorf(\"multiple namespace declarations found: first at line %d, second at line %d\", foundLineNumber, lineNumber)\n\t\t\t}\n\t\t\tfoundNamespace = namespace\n\t\t\tfoundLineNumber = lineNumber\n\t\t}\n\t}\n\n\treturn foundNamespace, scanner.Err()\n}\n"
  },
  {
    "path": "internal/extgen/paramparser.go",
    "content": "package extgen\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n)\n\ntype ParameterParser struct{}\n\ntype ParameterInfo struct {\n\tRequiredCount int\n\tTotalCount    int\n}\n\nfunc (pp *ParameterParser) analyzeParameters(params []phpParameter) ParameterInfo {\n\tinfo := ParameterInfo{TotalCount: len(params)}\n\n\tfor _, param := range params {\n\t\tif !param.HasDefault {\n\t\t\tinfo.RequiredCount++\n\t\t}\n\t}\n\n\treturn info\n}\n\nfunc (pp *ParameterParser) generateParamDeclarations(params []phpParameter) string {\n\tif len(params) == 0 {\n\t\treturn \"\"\n\t}\n\n\tvar declarations []string\n\n\tfor _, param := range params {\n\t\tdeclarations = append(declarations, pp.generateSingleParamDeclaration(param)...)\n\t}\n\n\treturn \"    \" + strings.Join(declarations, \"\\n    \")\n}\n\nfunc (pp *ParameterParser) generateSingleParamDeclaration(param phpParameter) []string {\n\tvar decls []string\n\n\tswitch param.PhpType {\n\tcase phpString:\n\t\tdecls = append(decls, fmt.Sprintf(\"zend_string *%s = NULL;\", param.Name))\n\t\tif param.IsNullable {\n\t\t\tdecls = append(decls, fmt.Sprintf(\"zend_bool %s_is_null = 0;\", param.Name))\n\t\t}\n\tcase phpInt:\n\t\tdefaultVal := pp.getDefaultValue(param, \"0\")\n\t\tdecls = append(decls, fmt.Sprintf(\"zend_long %s = %s;\", param.Name, defaultVal))\n\t\tif param.IsNullable {\n\t\t\tdecls = append(decls, fmt.Sprintf(\"zend_bool %s_is_null = 0;\", param.Name))\n\t\t}\n\tcase phpFloat:\n\t\tdefaultVal := pp.getDefaultValue(param, \"0.0\")\n\t\tdecls = append(decls, fmt.Sprintf(\"double %s = %s;\", param.Name, defaultVal))\n\t\tif param.IsNullable {\n\t\t\tdecls = append(decls, fmt.Sprintf(\"zend_bool %s_is_null = 0;\", param.Name))\n\t\t}\n\tcase phpBool:\n\t\tdefaultVal := pp.getDefaultValue(param, \"0\")\n\t\tif param.HasDefault && param.DefaultValue == \"true\" {\n\t\t\tdefaultVal = \"1\"\n\t\t}\n\t\tdecls = append(decls, fmt.Sprintf(\"zend_bool %s = %s;\", param.Name, defaultVal))\n\t\tif param.IsNullable {\n\t\t\tdecls = append(decls, fmt.Sprintf(\"zend_bool %s_is_null = 0;\", param.Name))\n\t\t}\n\tcase phpArray:\n\t\tdecls = append(decls, fmt.Sprintf(\"zend_array *%s = NULL;\", param.Name))\n\tcase phpMixed:\n\t\tdecls = append(decls, fmt.Sprintf(\"zval *%s = NULL;\", param.Name))\n\tcase \"callable\":\n\t\tdecls = append(decls, fmt.Sprintf(\"zval *%s_callback;\", param.Name))\n\t}\n\n\treturn decls\n}\n\nfunc (pp *ParameterParser) getDefaultValue(param phpParameter, fallback string) string {\n\tif !param.HasDefault || param.DefaultValue == \"\" {\n\t\treturn fallback\n\t}\n\treturn param.DefaultValue\n}\n\nfunc (pp *ParameterParser) generateParamParsing(params []phpParameter, requiredCount int) string {\n\tif len(params) == 0 {\n\t\treturn `    ZEND_PARSE_PARAMETERS_NONE();`\n\t}\n\n\tvar builder strings.Builder\n\t_, _ = fmt.Fprintf(&builder, \"    ZEND_PARSE_PARAMETERS_START(%d, %d)\", requiredCount, len(params))\n\n\toptionalStarted := false\n\tfor _, param := range params {\n\t\tif param.HasDefault && !optionalStarted {\n\t\t\tbuilder.WriteString(\"\\n        Z_PARAM_OPTIONAL\")\n\t\t\toptionalStarted = true\n\t\t}\n\n\t\tbuilder.WriteString(pp.generateParamParsingMacro(param))\n\t}\n\n\tbuilder.WriteString(\"\\n    ZEND_PARSE_PARAMETERS_END();\")\n\treturn builder.String()\n}\n\nfunc (pp *ParameterParser) generateParamParsingMacro(param phpParameter) string {\n\tif param.IsNullable {\n\t\tswitch param.PhpType {\n\t\tcase phpString:\n\t\t\treturn fmt.Sprintf(\"\\n        Z_PARAM_STR_OR_NULL(%s, %s_is_null)\", param.Name, param.Name)\n\t\tcase phpInt:\n\t\t\treturn fmt.Sprintf(\"\\n        Z_PARAM_LONG_OR_NULL(%s, %s_is_null)\", param.Name, param.Name)\n\t\tcase phpFloat:\n\t\t\treturn fmt.Sprintf(\"\\n        Z_PARAM_DOUBLE_OR_NULL(%s, %s_is_null)\", param.Name, param.Name)\n\t\tcase phpBool:\n\t\t\treturn fmt.Sprintf(\"\\n        Z_PARAM_BOOL_OR_NULL(%s, %s_is_null)\", param.Name, param.Name)\n\t\tcase phpArray:\n\t\t\treturn fmt.Sprintf(\"\\n        Z_PARAM_ARRAY_HT_OR_NULL(%s)\", param.Name)\n\t\tcase phpMixed:\n\t\t\treturn fmt.Sprintf(\"\\n        Z_PARAM_ZVAL_OR_NULL(%s)\", param.Name)\n\t\tcase phpCallable:\n\t\t\treturn fmt.Sprintf(\"\\n        Z_PARAM_ZVAL_OR_NULL(%s_callback)\", param.Name)\n\t\tdefault:\n\t\t\treturn \"\"\n\t\t}\n\t} else {\n\t\tswitch param.PhpType {\n\t\tcase phpString:\n\t\t\treturn fmt.Sprintf(\"\\n        Z_PARAM_STR(%s)\", param.Name)\n\t\tcase phpInt:\n\t\t\treturn fmt.Sprintf(\"\\n        Z_PARAM_LONG(%s)\", param.Name)\n\t\tcase phpFloat:\n\t\t\treturn fmt.Sprintf(\"\\n        Z_PARAM_DOUBLE(%s)\", param.Name)\n\t\tcase phpBool:\n\t\t\treturn fmt.Sprintf(\"\\n        Z_PARAM_BOOL(%s)\", param.Name)\n\t\tcase phpArray:\n\t\t\treturn fmt.Sprintf(\"\\n        Z_PARAM_ARRAY_HT(%s)\", param.Name)\n\t\tcase phpMixed:\n\t\t\treturn fmt.Sprintf(\"\\n        Z_PARAM_ZVAL(%s)\", param.Name)\n\t\tcase phpCallable:\n\t\t\treturn fmt.Sprintf(\"\\n        Z_PARAM_ZVAL(%s_callback)\", param.Name)\n\t\tdefault:\n\t\t\treturn \"\"\n\t\t}\n\t}\n}\n\nfunc (pp *ParameterParser) generateGoCallParams(params []phpParameter) string {\n\tif len(params) == 0 {\n\t\treturn \"\"\n\t}\n\n\tvar goParams []string\n\tfor _, param := range params {\n\t\tgoParams = append(goParams, pp.generateSingleGoCallParam(param))\n\t}\n\n\treturn strings.Join(goParams, \", \")\n}\n\nfunc (pp *ParameterParser) generateSingleGoCallParam(param phpParameter) string {\n\tif param.IsNullable {\n\t\tswitch param.PhpType {\n\t\tcase phpString:\n\t\t\treturn fmt.Sprintf(\"%s_is_null ? NULL : %s\", param.Name, param.Name)\n\t\tcase phpInt:\n\t\t\treturn fmt.Sprintf(\"%s_is_null ? NULL : &%s\", param.Name, param.Name)\n\t\tcase phpFloat:\n\t\t\treturn fmt.Sprintf(\"%s_is_null ? NULL : &%s\", param.Name, param.Name)\n\t\tcase phpBool:\n\t\t\treturn fmt.Sprintf(\"%s_is_null ? NULL : &%s\", param.Name, param.Name)\n\t\tcase phpCallable:\n\t\t\treturn fmt.Sprintf(\"%s_callback\", param.Name)\n\t\tdefault:\n\t\t\treturn param.Name\n\t\t}\n\t}\n\n\tswitch param.PhpType {\n\tcase phpInt:\n\t\treturn fmt.Sprintf(\"(long) %s\", param.Name)\n\tcase phpFloat:\n\t\treturn fmt.Sprintf(\"(double) %s\", param.Name)\n\tcase phpBool:\n\t\treturn fmt.Sprintf(\"(int) %s\", param.Name)\n\tcase phpCallable:\n\t\treturn fmt.Sprintf(\"%s_callback\", param.Name)\n\tdefault:\n\t\treturn param.Name\n\t}\n}\n"
  },
  {
    "path": "internal/extgen/paramparser_test.go",
    "content": "package extgen\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestParameterParser_AnalyzeParameters(t *testing.T) {\n\tpp := &ParameterParser{}\n\n\ttests := []struct {\n\t\tname     string\n\t\tparams   []phpParameter\n\t\texpected ParameterInfo\n\t}{\n\t\t{\n\t\t\tname:   \"no parameters\",\n\t\t\tparams: []phpParameter{},\n\t\t\texpected: ParameterInfo{\n\t\t\t\tRequiredCount: 0,\n\t\t\t\tTotalCount:    0,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"all required parameters\",\n\t\t\tparams: []phpParameter{\n\t\t\t\t{Name: \"name\", PhpType: phpString, HasDefault: false},\n\t\t\t\t{Name: \"count\", PhpType: phpInt, HasDefault: false},\n\t\t\t},\n\t\t\texpected: ParameterInfo{\n\t\t\t\tRequiredCount: 2,\n\t\t\t\tTotalCount:    2,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"mixed required and optional parameters\",\n\t\t\tparams: []phpParameter{\n\t\t\t\t{Name: \"name\", PhpType: phpString, HasDefault: false},\n\t\t\t\t{Name: \"count\", PhpType: phpInt, HasDefault: true, DefaultValue: \"10\"},\n\t\t\t\t{Name: \"enabled\", PhpType: phpBool, HasDefault: true, DefaultValue: \"true\"},\n\t\t\t},\n\t\t\texpected: ParameterInfo{\n\t\t\t\tRequiredCount: 1,\n\t\t\t\tTotalCount:    3,\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := pp.analyzeParameters(tt.params)\n\t\t\tassert.Equal(t, tt.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestParameterParser_GenerateParamDeclarations(t *testing.T) {\n\tpp := &ParameterParser{}\n\n\ttests := []struct {\n\t\tname     string\n\t\tparams   []phpParameter\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"no parameters\",\n\t\t\tparams:   []phpParameter{},\n\t\t\texpected: \"\",\n\t\t},\n\t\t{\n\t\t\tname: \"string parameter\",\n\t\t\tparams: []phpParameter{\n\t\t\t\t{Name: \"message\", PhpType: phpString, HasDefault: false},\n\t\t\t},\n\t\t\texpected: \"    zend_string *message = NULL;\",\n\t\t},\n\t\t{\n\t\t\tname: \"nullable string parameter\",\n\t\t\tparams: []phpParameter{\n\t\t\t\t{Name: \"message\", PhpType: phpString, HasDefault: false, IsNullable: true},\n\t\t\t},\n\t\t\texpected: \"    zend_string *message = NULL;\\n    zend_bool message_is_null = 0;\",\n\t\t},\n\t\t{\n\t\t\tname: \"int parameter with default\",\n\t\t\tparams: []phpParameter{\n\t\t\t\t{Name: \"count\", PhpType: phpInt, HasDefault: true, DefaultValue: \"42\"},\n\t\t\t},\n\t\t\texpected: \"    zend_long count = 42;\",\n\t\t},\n\t\t{\n\t\t\tname: \"nullable int parameter\",\n\t\t\tparams: []phpParameter{\n\t\t\t\t{Name: \"count\", PhpType: phpInt, HasDefault: false, IsNullable: true},\n\t\t\t},\n\t\t\texpected: \"    zend_long count = 0;\\n    zend_bool count_is_null = 0;\",\n\t\t},\n\t\t{\n\t\t\tname: \"bool parameter with true default\",\n\t\t\tparams: []phpParameter{\n\t\t\t\t{Name: \"enabled\", PhpType: phpBool, HasDefault: true, DefaultValue: \"true\"},\n\t\t\t},\n\t\t\texpected: \"    zend_bool enabled = 1;\",\n\t\t},\n\t\t{\n\t\t\tname: \"nullable bool parameter\",\n\t\t\tparams: []phpParameter{\n\t\t\t\t{Name: \"enabled\", PhpType: phpBool, HasDefault: false, IsNullable: true},\n\t\t\t},\n\t\t\texpected: \"    zend_bool enabled = 0;\\n    zend_bool enabled_is_null = 0;\",\n\t\t},\n\t\t{\n\t\t\tname: \"float parameter\",\n\t\t\tparams: []phpParameter{\n\t\t\t\t{Name: \"ratio\", PhpType: phpFloat, HasDefault: false},\n\t\t\t},\n\t\t\texpected: \"    double ratio = 0.0;\",\n\t\t},\n\t\t{\n\t\t\tname: \"nullable float parameter\",\n\t\t\tparams: []phpParameter{\n\t\t\t\t{Name: \"ratio\", PhpType: phpFloat, HasDefault: false, IsNullable: true},\n\t\t\t},\n\t\t\texpected: \"    double ratio = 0.0;\\n    zend_bool ratio_is_null = 0;\",\n\t\t},\n\t\t{\n\t\t\tname: \"multiple parameters\",\n\t\t\tparams: []phpParameter{\n\t\t\t\t{Name: \"name\", PhpType: phpString, HasDefault: false},\n\t\t\t\t{Name: \"count\", PhpType: phpInt, HasDefault: true, DefaultValue: \"10\"},\n\t\t\t},\n\t\t\texpected: \"    zend_string *name = NULL;\\n    zend_long count = 10;\",\n\t\t},\n\t\t{\n\t\t\tname: \"mixed nullable and non-nullable parameters\",\n\t\t\tparams: []phpParameter{\n\t\t\t\t{Name: \"name\", PhpType: phpString, HasDefault: false, IsNullable: false},\n\t\t\t\t{Name: \"count\", PhpType: phpInt, HasDefault: false, IsNullable: true},\n\t\t\t},\n\t\t\texpected: \"    zend_string *name = NULL;\\n    zend_long count = 0;\\n    zend_bool count_is_null = 0;\",\n\t\t},\n\t\t{\n\t\t\tname: \"array parameter\",\n\t\t\tparams: []phpParameter{\n\t\t\t\t{Name: \"items\", PhpType: phpArray, HasDefault: false},\n\t\t\t},\n\t\t\texpected: \"    zend_array *items = NULL;\",\n\t\t},\n\t\t{\n\t\t\tname: \"nullable array parameter\",\n\t\t\tparams: []phpParameter{\n\t\t\t\t{Name: \"items\", PhpType: phpArray, HasDefault: false, IsNullable: true},\n\t\t\t},\n\t\t\texpected: \"    zend_array *items = NULL;\",\n\t\t},\n\t\t{\n\t\t\tname: \"mixed types with array\",\n\t\t\tparams: []phpParameter{\n\t\t\t\t{Name: \"name\", PhpType: phpString, HasDefault: false},\n\t\t\t\t{Name: \"items\", PhpType: phpArray, HasDefault: false},\n\t\t\t\t{Name: \"count\", PhpType: phpInt, HasDefault: true, DefaultValue: \"5\"},\n\t\t\t},\n\t\t\texpected: \"    zend_string *name = NULL;\\n    zend_array *items = NULL;\\n    zend_long count = 5;\",\n\t\t},\n\t\t{\n\t\t\tname: \"mixed parameter\",\n\t\t\tparams: []phpParameter{\n\t\t\t\t{Name: \"m\", PhpType: phpMixed, HasDefault: false},\n\t\t\t},\n\t\t\texpected: \"    zval *m = NULL;\",\n\t\t},\n\t\t{\n\t\t\tname: \"nullable mixed parameter\",\n\t\t\tparams: []phpParameter{\n\t\t\t\t{Name: \"m\", PhpType: phpMixed, HasDefault: false, IsNullable: true},\n\t\t\t},\n\t\t\texpected: \"    zval *m = NULL;\",\n\t\t},\n\t\t{\n\t\t\tname: \"callable parameter\",\n\t\t\tparams: []phpParameter{\n\t\t\t\t{Name: \"callback\", PhpType: phpCallable, HasDefault: false},\n\t\t\t},\n\t\t\texpected: \"    zval *callback_callback;\",\n\t\t},\n\t\t{\n\t\t\tname: \"nullable callable parameter\",\n\t\t\tparams: []phpParameter{\n\t\t\t\t{Name: \"callback\", PhpType: phpCallable, HasDefault: false, IsNullable: true},\n\t\t\t},\n\t\t\texpected: \"    zval *callback_callback;\",\n\t\t},\n\t\t{\n\t\t\tname: \"mixed types with callable\",\n\t\t\tparams: []phpParameter{\n\t\t\t\t{Name: \"data\", PhpType: phpArray, HasDefault: false},\n\t\t\t\t{Name: \"callback\", PhpType: phpCallable, HasDefault: false},\n\t\t\t\t{Name: \"options\", PhpType: phpInt, HasDefault: true, DefaultValue: \"0\"},\n\t\t\t},\n\t\t\texpected: \"    zend_array *data = NULL;\\n    zval *callback_callback;\\n    zend_long options = 0;\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := pp.generateParamDeclarations(tt.params)\n\t\t\tassert.Equal(t, tt.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestParameterParser_GenerateParamParsing(t *testing.T) {\n\tpp := &ParameterParser{}\n\n\ttests := []struct {\n\t\tname          string\n\t\tparams        []phpParameter\n\t\trequiredCount int\n\t\texpected      string\n\t}{\n\t\t{\n\t\t\tname:          \"no parameters\",\n\t\t\tparams:        []phpParameter{},\n\t\t\trequiredCount: 0,\n\t\t\texpected:      `    ZEND_PARSE_PARAMETERS_NONE();`,\n\t\t},\n\t\t{\n\t\t\tname: \"single required string parameter\",\n\t\t\tparams: []phpParameter{\n\t\t\t\t{Name: \"message\", PhpType: phpString, HasDefault: false},\n\t\t\t},\n\t\t\trequiredCount: 1,\n\t\t\texpected: `    ZEND_PARSE_PARAMETERS_START(1, 1)\n        Z_PARAM_STR(message)\n    ZEND_PARSE_PARAMETERS_END();`,\n\t\t},\n\t\t{\n\t\t\tname: \"mixed required and optional parameters\",\n\t\t\tparams: []phpParameter{\n\t\t\t\t{Name: \"name\", PhpType: phpString, HasDefault: false},\n\t\t\t\t{Name: \"count\", PhpType: phpInt, HasDefault: true, DefaultValue: \"10\"},\n\t\t\t\t{Name: \"enabled\", PhpType: phpBool, HasDefault: true, DefaultValue: \"true\"},\n\t\t\t},\n\t\t\trequiredCount: 1,\n\t\t\texpected: `    ZEND_PARSE_PARAMETERS_START(1, 3)\n        Z_PARAM_STR(name)\n        Z_PARAM_OPTIONAL\n        Z_PARAM_LONG(count)\n        Z_PARAM_BOOL(enabled)\n    ZEND_PARSE_PARAMETERS_END();`,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := pp.generateParamParsing(tt.params, tt.requiredCount)\n\t\t\tassert.Equal(t, tt.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestParameterParser_GenerateGoCallParams(t *testing.T) {\n\tpp := &ParameterParser{}\n\n\ttests := []struct {\n\t\tname     string\n\t\tparams   []phpParameter\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"no parameters\",\n\t\t\tparams:   []phpParameter{},\n\t\t\texpected: \"\",\n\t\t},\n\t\t{\n\t\t\tname: \"single string parameter\",\n\t\t\tparams: []phpParameter{\n\t\t\t\t{Name: \"message\", PhpType: phpString},\n\t\t\t},\n\t\t\texpected: \"message\",\n\t\t},\n\t\t{\n\t\t\tname: \"multiple parameters of different types\",\n\t\t\tparams: []phpParameter{\n\t\t\t\t{Name: \"name\", PhpType: phpString},\n\t\t\t\t{Name: \"count\", PhpType: phpInt},\n\t\t\t\t{Name: \"ratio\", PhpType: phpFloat},\n\t\t\t\t{Name: \"enabled\", PhpType: phpBool},\n\t\t\t},\n\t\t\texpected: \"name, (long) count, (double) ratio, (int) enabled\",\n\t\t},\n\t\t{\n\t\t\tname: \"array parameter\",\n\t\t\tparams: []phpParameter{\n\t\t\t\t{Name: \"items\", PhpType: phpArray},\n\t\t\t},\n\t\t\texpected: \"items\",\n\t\t},\n\t\t{\n\t\t\tname: \"nullable array parameter\",\n\t\t\tparams: []phpParameter{\n\t\t\t\t{Name: \"items\", PhpType: phpArray, IsNullable: true},\n\t\t\t},\n\t\t\texpected: \"items\",\n\t\t},\n\t\t{\n\t\t\tname: \"mixed parameters with array\",\n\t\t\tparams: []phpParameter{\n\t\t\t\t{Name: \"name\", PhpType: phpString},\n\t\t\t\t{Name: \"items\", PhpType: phpArray},\n\t\t\t\t{Name: \"count\", PhpType: phpInt},\n\t\t\t},\n\t\t\texpected: \"name, items, (long) count\",\n\t\t},\n\t\t{\n\t\t\tname: \"callable parameter\",\n\t\t\tparams: []phpParameter{\n\t\t\t\t{Name: \"callback\", PhpType: \"callable\"},\n\t\t\t},\n\t\t\texpected: \"callback_callback\",\n\t\t},\n\t\t{\n\t\t\tname: \"nullable callable parameter\",\n\t\t\tparams: []phpParameter{\n\t\t\t\t{Name: \"callback\", PhpType: \"callable\", IsNullable: true},\n\t\t\t},\n\t\t\texpected: \"callback_callback\",\n\t\t},\n\t\t{\n\t\t\tname: \"mixed parameters with callable\",\n\t\t\tparams: []phpParameter{\n\t\t\t\t{Name: \"data\", PhpType: \"array\"},\n\t\t\t\t{Name: \"callback\", PhpType: \"callable\"},\n\t\t\t\t{Name: \"limit\", PhpType: \"int\"},\n\t\t\t},\n\t\t\texpected: \"data, callback_callback, (long) limit\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := pp.generateGoCallParams(tt.params)\n\t\t\tassert.Equal(t, tt.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestParameterParser_GenerateParamParsingMacro(t *testing.T) {\n\tpp := &ParameterParser{}\n\n\ttests := []struct {\n\t\tname     string\n\t\tparam    phpParameter\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"string parameter\",\n\t\t\tparam:    phpParameter{Name: \"message\", PhpType: phpString},\n\t\t\texpected: \"\\n        Z_PARAM_STR(message)\",\n\t\t},\n\t\t{\n\t\t\tname:     \"nullable string parameter\",\n\t\t\tparam:    phpParameter{Name: \"message\", PhpType: phpString, IsNullable: true},\n\t\t\texpected: \"\\n        Z_PARAM_STR_OR_NULL(message, message_is_null)\",\n\t\t},\n\t\t{\n\t\t\tname:     \"int parameter\",\n\t\t\tparam:    phpParameter{Name: \"count\", PhpType: phpInt},\n\t\t\texpected: \"\\n        Z_PARAM_LONG(count)\",\n\t\t},\n\t\t{\n\t\t\tname:     \"nullable int parameter\",\n\t\t\tparam:    phpParameter{Name: \"count\", PhpType: phpInt, IsNullable: true},\n\t\t\texpected: \"\\n        Z_PARAM_LONG_OR_NULL(count, count_is_null)\",\n\t\t},\n\t\t{\n\t\t\tname:     \"float parameter\",\n\t\t\tparam:    phpParameter{Name: \"ratio\", PhpType: phpFloat},\n\t\t\texpected: \"\\n        Z_PARAM_DOUBLE(ratio)\",\n\t\t},\n\t\t{\n\t\t\tname:     \"nullable float parameter\",\n\t\t\tparam:    phpParameter{Name: \"ratio\", PhpType: phpFloat, IsNullable: true},\n\t\t\texpected: \"\\n        Z_PARAM_DOUBLE_OR_NULL(ratio, ratio_is_null)\",\n\t\t},\n\t\t{\n\t\t\tname:     \"bool parameter\",\n\t\t\tparam:    phpParameter{Name: \"enabled\", PhpType: phpBool},\n\t\t\texpected: \"\\n        Z_PARAM_BOOL(enabled)\",\n\t\t},\n\t\t{\n\t\t\tname:     \"nullable bool parameter\",\n\t\t\tparam:    phpParameter{Name: \"enabled\", PhpType: phpBool, IsNullable: true},\n\t\t\texpected: \"\\n        Z_PARAM_BOOL_OR_NULL(enabled, enabled_is_null)\",\n\t\t},\n\t\t{\n\t\t\tname:     \"array parameter\",\n\t\t\tparam:    phpParameter{Name: \"items\", PhpType: phpArray},\n\t\t\texpected: \"\\n        Z_PARAM_ARRAY_HT(items)\",\n\t\t},\n\t\t{\n\t\t\tname:     \"nullable array parameter\",\n\t\t\tparam:    phpParameter{Name: \"items\", PhpType: phpArray, IsNullable: true},\n\t\t\texpected: \"\\n        Z_PARAM_ARRAY_HT_OR_NULL(items)\",\n\t\t},\n\t\t{\n\t\t\tname:     \"mixed parameter\",\n\t\t\tparam:    phpParameter{Name: \"m\", PhpType: phpMixed},\n\t\t\texpected: \"\\n        Z_PARAM_ZVAL(m)\",\n\t\t},\n\t\t{\n\t\t\tname:     \"nullable mixed parameter\",\n\t\t\tparam:    phpParameter{Name: \"m\", PhpType: phpMixed, IsNullable: true},\n\t\t\texpected: \"\\n        Z_PARAM_ZVAL_OR_NULL(m)\",\n\t\t},\n\t\t{\n\t\t\tname:     \"callable parameter\",\n\t\t\tparam:    phpParameter{Name: \"callback\", PhpType: phpCallable},\n\t\t\texpected: \"\\n        Z_PARAM_ZVAL(callback_callback)\",\n\t\t},\n\t\t{\n\t\t\tname:     \"nullable callable parameter\",\n\t\t\tparam:    phpParameter{Name: \"callback\", PhpType: phpCallable, IsNullable: true},\n\t\t\texpected: \"\\n        Z_PARAM_ZVAL_OR_NULL(callback_callback)\",\n\t\t},\n\t\t{\n\t\t\tname:     \"unknown type\",\n\t\t\tparam:    phpParameter{Name: \"unknown\", PhpType: phpType(\"unknown\")},\n\t\t\texpected: \"\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := pp.generateParamParsingMacro(tt.param)\n\t\t\tassert.Equal(t, tt.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestParameterParser_GetDefaultValue(t *testing.T) {\n\tpp := &ParameterParser{}\n\n\ttests := []struct {\n\t\tname     string\n\t\tparam    phpParameter\n\t\tfallback string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"parameter without default\",\n\t\t\tparam:    phpParameter{Name: \"count\", PhpType: phpInt, HasDefault: false},\n\t\t\tfallback: \"0\",\n\t\t\texpected: \"0\",\n\t\t},\n\t\t{\n\t\t\tname:     \"parameter with default value\",\n\t\t\tparam:    phpParameter{Name: \"count\", PhpType: phpInt, HasDefault: true, DefaultValue: \"42\"},\n\t\t\tfallback: \"0\",\n\t\t\texpected: \"42\",\n\t\t},\n\t\t{\n\t\t\tname:     \"parameter with empty default value\",\n\t\t\tparam:    phpParameter{Name: \"count\", PhpType: phpInt, HasDefault: true, DefaultValue: \"\"},\n\t\t\tfallback: \"0\",\n\t\t\texpected: \"0\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := pp.getDefaultValue(tt.param, tt.fallback)\n\t\t\tassert.Equal(t, tt.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestParameterParser_GenerateSingleGoCallParam(t *testing.T) {\n\tpp := &ParameterParser{}\n\n\ttests := []struct {\n\t\tname     string\n\t\tparam    phpParameter\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"string parameter\",\n\t\t\tparam:    phpParameter{Name: \"message\", PhpType: phpString},\n\t\t\texpected: \"message\",\n\t\t},\n\t\t{\n\t\t\tname:     \"nullable string parameter\",\n\t\t\tparam:    phpParameter{Name: \"message\", PhpType: phpString, IsNullable: true},\n\t\t\texpected: \"message_is_null ? NULL : message\",\n\t\t},\n\t\t{\n\t\t\tname:     \"int parameter\",\n\t\t\tparam:    phpParameter{Name: \"count\", PhpType: phpInt},\n\t\t\texpected: \"(long) count\",\n\t\t},\n\t\t{\n\t\t\tname:     \"nullable int parameter\",\n\t\t\tparam:    phpParameter{Name: \"count\", PhpType: phpInt, IsNullable: true},\n\t\t\texpected: \"count_is_null ? NULL : &count\",\n\t\t},\n\t\t{\n\t\t\tname:     \"float parameter\",\n\t\t\tparam:    phpParameter{Name: \"ratio\", PhpType: phpFloat},\n\t\t\texpected: \"(double) ratio\",\n\t\t},\n\t\t{\n\t\t\tname:     \"nullable float parameter\",\n\t\t\tparam:    phpParameter{Name: \"ratio\", PhpType: phpFloat, IsNullable: true},\n\t\t\texpected: \"ratio_is_null ? NULL : &ratio\",\n\t\t},\n\t\t{\n\t\t\tname:     \"bool parameter\",\n\t\t\tparam:    phpParameter{Name: \"enabled\", PhpType: phpBool},\n\t\t\texpected: \"(int) enabled\",\n\t\t},\n\t\t{\n\t\t\tname:     \"nullable bool parameter\",\n\t\t\tparam:    phpParameter{Name: \"enabled\", PhpType: phpBool, IsNullable: true},\n\t\t\texpected: \"enabled_is_null ? NULL : &enabled\",\n\t\t},\n\t\t{\n\t\t\tname:     \"array parameter\",\n\t\t\tparam:    phpParameter{Name: \"items\", PhpType: phpArray},\n\t\t\texpected: \"items\",\n\t\t},\n\t\t{\n\t\t\tname:     \"nullable array parameter\",\n\t\t\tparam:    phpParameter{Name: \"items\", PhpType: phpArray, IsNullable: true},\n\t\t\texpected: \"items\",\n\t\t},\n\t\t{\n\t\t\tname:     \"callable parameter\",\n\t\t\tparam:    phpParameter{Name: \"callback\", PhpType: \"callable\"},\n\t\t\texpected: \"callback_callback\",\n\t\t},\n\t\t{\n\t\t\tname:     \"nullable callable parameter\",\n\t\t\tparam:    phpParameter{Name: \"callback\", PhpType: \"callable\", IsNullable: true},\n\t\t\texpected: \"callback_callback\",\n\t\t},\n\t\t{\n\t\t\tname:     \"unknown type\",\n\t\t\tparam:    phpParameter{Name: \"unknown\", PhpType: phpType(\"unknown\")},\n\t\t\texpected: \"unknown\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := pp.generateSingleGoCallParam(tt.param)\n\t\t\tassert.Equal(t, tt.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestParameterParser_GenerateSingleParamDeclaration(t *testing.T) {\n\tpp := &ParameterParser{}\n\n\ttests := []struct {\n\t\tname     string\n\t\tparam    phpParameter\n\t\texpected []string\n\t}{\n\t\t{\n\t\t\tname:     \"string parameter\",\n\t\t\tparam:    phpParameter{Name: \"message\", PhpType: phpString, HasDefault: false},\n\t\t\texpected: []string{\"zend_string *message = NULL;\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"nullable string parameter\",\n\t\t\tparam:    phpParameter{Name: \"message\", PhpType: phpString, HasDefault: false, IsNullable: true},\n\t\t\texpected: []string{\"zend_string *message = NULL;\", \"zend_bool message_is_null = 0;\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"int parameter with default\",\n\t\t\tparam:    phpParameter{Name: \"count\", PhpType: phpInt, HasDefault: true, DefaultValue: \"42\"},\n\t\t\texpected: []string{\"zend_long count = 42;\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"nullable int parameter\",\n\t\t\tparam:    phpParameter{Name: \"count\", PhpType: phpInt, HasDefault: false, IsNullable: true},\n\t\t\texpected: []string{\"zend_long count = 0;\", \"zend_bool count_is_null = 0;\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"bool parameter with true default\",\n\t\t\tparam:    phpParameter{Name: \"enabled\", PhpType: phpBool, HasDefault: true, DefaultValue: \"true\"},\n\t\t\texpected: []string{\"zend_bool enabled = 1;\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"nullable bool parameter\",\n\t\t\tparam:    phpParameter{Name: \"enabled\", PhpType: phpBool, HasDefault: false, IsNullable: true},\n\t\t\texpected: []string{\"zend_bool enabled = 0;\", \"zend_bool enabled_is_null = 0;\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"bool parameter with false default\",\n\t\t\tparam:    phpParameter{Name: \"disabled\", PhpType: phpBool, HasDefault: true, DefaultValue: \"false\"},\n\t\t\texpected: []string{\"zend_bool disabled = false;\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"float parameter\",\n\t\t\tparam:    phpParameter{Name: \"ratio\", PhpType: phpFloat, HasDefault: false},\n\t\t\texpected: []string{\"double ratio = 0.0;\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"nullable float parameter\",\n\t\t\tparam:    phpParameter{Name: \"ratio\", PhpType: phpFloat, HasDefault: false, IsNullable: true},\n\t\t\texpected: []string{\"double ratio = 0.0;\", \"zend_bool ratio_is_null = 0;\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"array parameter\",\n\t\t\tparam:    phpParameter{Name: \"items\", PhpType: phpArray, HasDefault: false},\n\t\t\texpected: []string{\"zend_array *items = NULL;\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"nullable array parameter\",\n\t\t\tparam:    phpParameter{Name: \"items\", PhpType: phpArray, HasDefault: false, IsNullable: true},\n\t\t\texpected: []string{\"zend_array *items = NULL;\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"callable parameter\",\n\t\t\tparam:    phpParameter{Name: \"callback\", PhpType: \"callable\", HasDefault: false},\n\t\t\texpected: []string{\"zval *callback_callback;\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"nullable callable parameter\",\n\t\t\tparam:    phpParameter{Name: \"callback\", PhpType: \"callable\", HasDefault: false, IsNullable: true},\n\t\t\texpected: []string{\"zval *callback_callback;\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := pp.generateSingleParamDeclaration(tt.param)\n\t\t\tassert.Equal(t, tt.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestParameterParser_Integration(t *testing.T) {\n\tpp := &ParameterParser{}\n\n\tparams := []phpParameter{\n\t\t{Name: \"name\", PhpType: phpString, HasDefault: false},\n\t\t{Name: \"count\", PhpType: phpInt, HasDefault: true, DefaultValue: \"10\"},\n\t\t{Name: \"enabled\", PhpType: phpBool, HasDefault: true, DefaultValue: \"true\"},\n\t}\n\n\tinfo := pp.analyzeParameters(params)\n\tassert.Equal(t, 1, info.RequiredCount)\n\tassert.Equal(t, 3, info.TotalCount)\n\n\tdeclarations := pp.generateParamDeclarations(params)\n\texpectedDeclarations := []string{\n\t\t\"zend_string *name = NULL;\",\n\t\t\"zend_long count = 10;\",\n\t\t\"zend_bool enabled = 1;\",\n\t}\n\tfor _, expected := range expectedDeclarations {\n\t\tassert.Contains(t, declarations, expected)\n\t}\n\n\tparsing := pp.generateParamParsing(params, info.RequiredCount)\n\tassert.Contains(t, parsing, \"ZEND_PARSE_PARAMETERS_START(1, 3)\")\n\tassert.Contains(t, parsing, \"Z_PARAM_OPTIONAL\")\n\n\tgoCallParams := pp.generateGoCallParams(params)\n\tassert.Equal(t, \"name, (long) count, (int) enabled\", goCallParams)\n}\n"
  },
  {
    "path": "internal/extgen/parser.go",
    "content": "package extgen\n\ntype SourceParser struct{}\n\n// EXPERIMENTAL\nfunc (p *SourceParser) ParseFunctions(filename string) ([]phpFunction, error) {\n\tfunctionParser := &FuncParser{}\n\treturn functionParser.parse(filename)\n}\n\n// EXPERIMENTAL\nfunc (p *SourceParser) ParseClasses(filename string) ([]phpClass, error) {\n\tclassParser := classParser{}\n\treturn classParser.parse(filename)\n}\n\n// EXPERIMENTAL\nfunc (p *SourceParser) ParseConstants(filename string) ([]phpConstant, error) {\n\tconstantParser := &ConstantParser{}\n\treturn constantParser.parse(filename)\n}\n\n// EXPERIMENTAL\nfunc (p *SourceParser) ParseNamespace(filename string) (string, error) {\n\tnamespaceParser := NamespaceParser{}\n\treturn namespaceParser.parse(filename)\n}\n"
  },
  {
    "path": "internal/extgen/phpfunc.go",
    "content": "package extgen\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n)\n\ntype PHPFuncGenerator struct {\n\tparamParser *ParameterParser\n\tnamespace   string\n}\n\nfunc (pfg *PHPFuncGenerator) generate(fn phpFunction) string {\n\tvar builder strings.Builder\n\n\tparamInfo := pfg.paramParser.analyzeParameters(fn.Params)\n\n\tfuncName := NamespacedName(pfg.namespace, fn.Name)\n\t_, _ = fmt.Fprintf(&builder, \"PHP_FUNCTION(%s)\\n{\\n\", funcName)\n\n\tif decl := pfg.paramParser.generateParamDeclarations(fn.Params); decl != \"\" {\n\t\tbuilder.WriteString(decl + \"\\n\")\n\t}\n\n\tbuilder.WriteString(pfg.paramParser.generateParamParsing(fn.Params, paramInfo.RequiredCount) + \"\\n\")\n\n\tbuilder.WriteString(pfg.generateGoCall(fn) + \"\\n\")\n\n\tif returnCode := pfg.generateReturnCode(fn.ReturnType); returnCode != \"\" {\n\t\tbuilder.WriteString(returnCode + \"\\n\")\n\t}\n\n\tbuilder.WriteString(\"}\\n\\n\")\n\n\treturn builder.String()\n}\n\nfunc (pfg *PHPFuncGenerator) generateGoCall(fn phpFunction) string {\n\tcallParams := pfg.paramParser.generateGoCallParams(fn.Params)\n\tgoFuncName := \"go_\" + fn.Name\n\n\tif fn.ReturnType == phpVoid {\n\t\treturn fmt.Sprintf(\"    %s(%s);\", goFuncName, callParams)\n\t}\n\n\tif fn.ReturnType == phpString {\n\t\treturn fmt.Sprintf(\"    zend_string *result = %s(%s);\", goFuncName, callParams)\n\t}\n\n\tif fn.ReturnType == phpArray {\n\t\treturn fmt.Sprintf(\"    zend_array *result = %s(%s);\", goFuncName, callParams)\n\t}\n\n\tif fn.ReturnType == phpMixed {\n\t\treturn fmt.Sprintf(\"    zval *result = %s(%s);\", goFuncName, callParams)\n\t}\n\n\treturn fmt.Sprintf(\"    %s result = %s(%s);\", pfg.getCReturnType(fn.ReturnType), goFuncName, callParams)\n}\n\nfunc (pfg *PHPFuncGenerator) getCReturnType(returnType phpType) string {\n\tswitch returnType {\n\tcase phpInt:\n\t\treturn \"long\"\n\tcase phpFloat:\n\t\treturn \"double\"\n\tcase phpBool:\n\t\treturn \"int\"\n\tdefault:\n\t\treturn \"void\"\n\t}\n}\n\nfunc (pfg *PHPFuncGenerator) generateReturnCode(returnType phpType) string {\n\tswitch returnType {\n\tcase phpString:\n\t\treturn `    if (result) {\n        RETURN_STR(result);\n    }\n\n\tRETURN_EMPTY_STRING();`\n\tcase phpInt:\n\t\treturn `    RETURN_LONG(result);`\n\tcase phpFloat:\n\t\treturn `    RETURN_DOUBLE(result);`\n\tcase phpBool:\n\t\treturn `    RETURN_BOOL(result);`\n\tcase phpArray:\n\t\treturn `    if (result) {\n        RETURN_ARR(result);\n    }\n\n\tRETURN_EMPTY_ARRAY();`\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n"
  },
  {
    "path": "internal/extgen/phpfunc_namespace_test.go",
    "content": "package extgen\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestPHPFuncGenerator_NamespacedFunctions(t *testing.T) {\n\ttests := []struct {\n\t\tname      string\n\t\tnamespace string\n\t\tfunction  phpFunction\n\t\texpected  string\n\t}{\n\t\t{\n\t\t\tname:      \"no namespace\",\n\t\t\tnamespace: \"\",\n\t\t\tfunction:  phpFunction{Name: \"test_func\", ReturnType: \"int\"},\n\t\t\texpected:  \"PHP_FUNCTION(test_func)\",\n\t\t},\n\t\t{\n\t\t\tname:      \"single level namespace\",\n\t\t\tnamespace: \"MyNamespace\",\n\t\t\tfunction:  phpFunction{Name: \"test_func\", ReturnType: \"int\"},\n\t\t\texpected:  \"PHP_FUNCTION(MyNamespace_test_func)\",\n\t\t},\n\t\t{\n\t\t\tname:      \"multi level namespace\",\n\t\t\tnamespace: `Go\\Extension`,\n\t\t\tfunction:  phpFunction{Name: \"multiply\", ReturnType: \"int\"},\n\t\t\texpected:  \"PHP_FUNCTION(Go_Extension_multiply)\",\n\t\t},\n\t\t{\n\t\t\tname:      \"deep namespace\",\n\t\t\tnamespace: `My\\Deep\\Nested\\Namespace`,\n\t\t\tfunction:  phpFunction{Name: \"is_even\", ReturnType: \"bool\"},\n\t\t\texpected:  \"PHP_FUNCTION(My_Deep_Nested_Namespace_is_even)\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgenerator := PHPFuncGenerator{\n\t\t\t\tparamParser: &ParameterParser{},\n\t\t\t\tnamespace:   tt.namespace,\n\t\t\t}\n\n\t\t\tresult := generator.generate(tt.function)\n\n\t\t\trequire.Contains(t, result, tt.expected, \"Expected to find %q in generated PHP code, but didn't.\\nGenerated:\\n%s\", tt.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestGetNamespacedFunctionName(t *testing.T) {\n\ttests := []struct {\n\t\tname         string\n\t\tnamespace    string\n\t\tfunctionName string\n\t\texpected     string\n\t}{\n\t\t{\n\t\t\tname:         \"no namespace\",\n\t\t\tnamespace:    \"\",\n\t\t\tfunctionName: \"test_func\",\n\t\t\texpected:     \"test_func\",\n\t\t},\n\t\t{\n\t\t\tname:         \"single level namespace\",\n\t\t\tnamespace:    \"MyNamespace\",\n\t\t\tfunctionName: \"test_func\",\n\t\t\texpected:     \"MyNamespace_test_func\",\n\t\t},\n\t\t{\n\t\t\tname:         \"multi level namespace\",\n\t\t\tnamespace:    `Go\\Extension`,\n\t\t\tfunctionName: \"multiply\",\n\t\t\texpected:     \"Go_Extension_multiply\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := NamespacedName(tt.namespace, tt.functionName)\n\n\t\t\trequire.Equal(t, tt.expected, result, \"Expected %q, got %q\", tt.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestCFileWithNamespacedPHPFunctions(t *testing.T) {\n\tgenerator := &Generator{\n\t\tBaseName:  \"test_extension\",\n\t\tNamespace: `Go\\Extension`,\n\t\tFunctions: []phpFunction{\n\t\t\t{\n\t\t\t\tName:       \"multiply\",\n\t\t\t\tReturnType: \"int\",\n\t\t\t\tParams: []phpParameter{\n\t\t\t\t\t{Name: \"a\", PhpType: \"int\"},\n\t\t\t\t\t{Name: \"b\", PhpType: \"int\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tName:       \"is_even\",\n\t\t\t\tReturnType: \"bool\",\n\t\t\t\tParams: []phpParameter{\n\t\t\t\t\t{Name: \"num\", PhpType: \"int\"},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tClasses: []phpClass{\n\t\t\t{\n\t\t\t\tName:     \"MySuperClass\",\n\t\t\t\tGoStruct: \"MySuperClass\",\n\t\t\t\tMethods: []phpClassMethod{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:       \"getName\",\n\t\t\t\t\t\tPhpName:    \"getName\",\n\t\t\t\t\t\tReturnType: \"string\",\n\t\t\t\t\t\tClassName:  \"MySuperClass\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tBuildDir: t.TempDir(),\n\t}\n\n\tcFileGen := cFileGenerator{generator: generator}\n\tcontent, err := cFileGen.buildContent()\n\trequire.NoError(t, err, \"error generating C file\")\n\n\texpectedFunctions := []string{\n\t\t\"PHP_FUNCTION(Go_Extension_multiply)\",\n\t\t\"PHP_FUNCTION(Go_Extension_is_even)\",\n\t}\n\n\tfor _, expected := range expectedFunctions {\n\t\trequire.Contains(t, content, expected, \"Expected to find %q in C file content\", expected)\n\t}\n\n\texpectedMethods := []string{\n\t\t\"PHP_METHOD(Go_Extension_MySuperClass, __construct)\",\n\t\t\"PHP_METHOD(Go_Extension_MySuperClass, getName)\",\n\t}\n\n\tfor _, expected := range expectedMethods {\n\t\trequire.Contains(t, content, expected, \"Expected to find %q in C file content\", expected)\n\t}\n\n\toldDeclarations := []string{\n\t\t\"PHP_FUNCTION(multiply)\",\n\t\t\"PHP_FUNCTION(is_even)\",\n\t\t\"PHP_METHOD(MySuperClass, __construct)\",\n\t\t\"PHP_METHOD(MySuperClass, getName)\",\n\t}\n\n\tfor _, old := range oldDeclarations {\n\t\trequire.NotContains(t, content, old, \"Did not expect to find old declaration %q in C file content\", old)\n\t}\n}\n"
  },
  {
    "path": "internal/extgen/phpfunc_test.go",
    "content": "package extgen\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestPHPFunctionGenerator_Generate(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tfunction phpFunction\n\t\tcontains []string // Strings that should be present in the output\n\t}{\n\t\t{\n\t\t\tname: \"simple string function\",\n\t\t\tfunction: phpFunction{\n\t\t\t\tName:       \"greet\",\n\t\t\t\tReturnType: phpString,\n\t\t\t\tParams: []phpParameter{\n\t\t\t\t\t{Name: \"name\", PhpType: phpString},\n\t\t\t\t},\n\t\t\t},\n\t\t\tcontains: []string{\n\t\t\t\t\"PHP_FUNCTION(greet)\",\n\t\t\t\t\"zend_string *name = NULL;\",\n\t\t\t\t\"Z_PARAM_STR(name)\",\n\t\t\t\t\"zend_string *result = go_greet(name);\",\n\t\t\t\t\"RETURN_STR(result)\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"function with default parameter\",\n\t\t\tfunction: phpFunction{\n\t\t\t\tName:       \"calculate\",\n\t\t\t\tReturnType: phpInt,\n\t\t\t\tParams: []phpParameter{\n\t\t\t\t\t{Name: \"base\", PhpType: phpInt},\n\t\t\t\t\t{Name: \"multiplier\", PhpType: phpInt, HasDefault: true, DefaultValue: \"2\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tcontains: []string{\n\t\t\t\t\"PHP_FUNCTION(calculate)\",\n\t\t\t\t\"zend_long base = 0;\",\n\t\t\t\t\"zend_long multiplier = 2;\",\n\t\t\t\t\"ZEND_PARSE_PARAMETERS_START(1, 2)\",\n\t\t\t\t\"Z_PARAM_OPTIONAL\",\n\t\t\t\t\"Z_PARAM_LONG(base)\",\n\t\t\t\t\"Z_PARAM_LONG(multiplier)\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"void function\",\n\t\t\tfunction: phpFunction{\n\t\t\t\tName:       \"doSomething\",\n\t\t\t\tReturnType: phpVoid,\n\t\t\t\tParams: []phpParameter{\n\t\t\t\t\t{Name: \"action\", PhpType: phpString},\n\t\t\t\t},\n\t\t\t},\n\t\t\tcontains: []string{\n\t\t\t\t\"PHP_FUNCTION(doSomething)\",\n\t\t\t\t\"go_doSomething(action);\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"bool function with default\",\n\t\t\tfunction: phpFunction{\n\t\t\t\tName:       \"isEnabled\",\n\t\t\t\tReturnType: phpBool,\n\t\t\t\tParams: []phpParameter{\n\t\t\t\t\t{Name: \"flag\", PhpType: phpBool, HasDefault: true, DefaultValue: \"true\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tcontains: []string{\n\t\t\t\t\"PHP_FUNCTION(isEnabled)\",\n\t\t\t\t\"zend_bool flag = 1;\",\n\t\t\t\t\"Z_PARAM_BOOL(flag)\",\n\t\t\t\t\"RETURN_BOOL(result)\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"float function\",\n\t\t\tfunction: phpFunction{\n\t\t\t\tName:       \"calculate\",\n\t\t\t\tReturnType: phpFloat,\n\t\t\t\tParams: []phpParameter{\n\t\t\t\t\t{Name: \"value\", PhpType: phpFloat},\n\t\t\t\t},\n\t\t\t},\n\t\t\tcontains: []string{\n\t\t\t\t\"PHP_FUNCTION(calculate)\",\n\t\t\t\t\"double value = 0.0;\",\n\t\t\t\t\"Z_PARAM_DOUBLE(value)\",\n\t\t\t\t\"RETURN_DOUBLE(result)\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"array function with array parameter\",\n\t\t\tfunction: phpFunction{\n\t\t\t\tName:       \"process_array\",\n\t\t\t\tReturnType: phpArray,\n\t\t\t\tParams: []phpParameter{\n\t\t\t\t\t{Name: \"input\", PhpType: phpArray},\n\t\t\t\t},\n\t\t\t},\n\t\t\tcontains: []string{\n\t\t\t\t\"PHP_FUNCTION(process_array)\",\n\t\t\t\t\"zend_array *input = NULL;\",\n\t\t\t\t\"Z_PARAM_ARRAY_HT(input)\",\n\t\t\t\t\"zend_array *result = go_process_array(input);\",\n\t\t\t\t\"RETURN_ARR(result)\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"array function with mixed parameters\",\n\t\t\tfunction: phpFunction{\n\t\t\t\tName:       \"filter_array\",\n\t\t\t\tReturnType: phpArray,\n\t\t\t\tParams: []phpParameter{\n\t\t\t\t\t{Name: \"data\", PhpType: phpArray},\n\t\t\t\t\t{Name: \"key\", PhpType: phpString},\n\t\t\t\t\t{Name: \"limit\", PhpType: phpInt, HasDefault: true, DefaultValue: \"10\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tcontains: []string{\n\t\t\t\t\"PHP_FUNCTION(filter_array)\",\n\t\t\t\t\"zend_array *data = NULL;\",\n\t\t\t\t\"zend_string *key = NULL;\",\n\t\t\t\t\"zend_long limit = 10;\",\n\t\t\t\t\"Z_PARAM_ARRAY_HT(data)\",\n\t\t\t\t\"Z_PARAM_STR(key)\",\n\t\t\t\t\"Z_PARAM_LONG(limit)\",\n\t\t\t\t\"ZEND_PARSE_PARAMETERS_START(2, 3)\",\n\t\t\t\t\"Z_PARAM_OPTIONAL\",\n\t\t\t},\n\t\t},\n\t}\n\n\tgenerator := PHPFuncGenerator{}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := generator.generate(tt.function)\n\n\t\t\tfor _, expected := range tt.contains {\n\t\t\t\tassert.Contains(t, result, expected, \"Generated code should contain '%s'\", expected)\n\t\t\t}\n\n\t\t\tassert.True(t, strings.HasPrefix(result, \"PHP_FUNCTION(\"), \"Generated code should start with PHP_FUNCTION\")\n\t\t\tassert.True(t, strings.HasSuffix(strings.TrimSpace(result), \"}\"), \"Generated code should end with closing brace\")\n\t\t})\n\t}\n}\n\nfunc TestPHPFunctionGenerator_GenerateParamDeclarations(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tparams   []phpParameter\n\t\tcontains []string\n\t}{\n\t\t{\n\t\t\tname: \"string parameter\",\n\t\t\tparams: []phpParameter{\n\t\t\t\t{Name: \"message\", PhpType: phpString},\n\t\t\t},\n\t\t\tcontains: []string{\n\t\t\t\t\"zend_string *message = NULL;\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"int parameter\",\n\t\t\tparams: []phpParameter{\n\t\t\t\t{Name: \"count\", PhpType: phpInt},\n\t\t\t},\n\t\t\tcontains: []string{\n\t\t\t\t\"zend_long count = 0;\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"bool with default\",\n\t\t\tparams: []phpParameter{\n\t\t\t\t{Name: \"enabled\", PhpType: phpBool, HasDefault: true, DefaultValue: \"true\"},\n\t\t\t},\n\t\t\tcontains: []string{\n\t\t\t\t\"zend_bool enabled = 1;\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"float parameter with default\",\n\t\t\tparams: []phpParameter{\n\t\t\t\t{Name: \"rate\", PhpType: phpFloat, HasDefault: true, DefaultValue: \"1.5\"},\n\t\t\t},\n\t\t\tcontains: []string{\n\t\t\t\t\"double rate = 1.5;\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"array parameter\",\n\t\t\tparams: []phpParameter{\n\t\t\t\t{Name: \"items\", PhpType: phpArray},\n\t\t\t},\n\t\t\tcontains: []string{\n\t\t\t\t\"zend_array *items = NULL;\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"mixed types with array\",\n\t\t\tparams: []phpParameter{\n\t\t\t\t{Name: \"name\", PhpType: phpString},\n\t\t\t\t{Name: \"data\", PhpType: phpArray},\n\t\t\t\t{Name: \"count\", PhpType: phpInt},\n\t\t\t},\n\t\t\tcontains: []string{\n\t\t\t\t\"zend_string *name = NULL;\",\n\t\t\t\t\"zend_array *data = NULL;\",\n\t\t\t\t\"zend_long count = 0;\",\n\t\t\t},\n\t\t},\n\t}\n\n\tparser := ParameterParser{}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := parser.generateParamDeclarations(tt.params)\n\n\t\t\tfor _, expected := range tt.contains {\n\t\t\t\tassert.Contains(t, result, expected, \"phpParameter declarations should contain '%s'\", expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestPHPFunctionGenerator_GenerateReturnCode(t *testing.T) {\n\ttests := []struct {\n\t\tname       string\n\t\treturnType phpType\n\t\tcontains   []string\n\t}{\n\t\t{\n\t\t\tname:       \"string return\",\n\t\t\treturnType: phpString,\n\t\t\tcontains: []string{\n\t\t\t\t\"RETURN_STR(result)\",\n\t\t\t\t\"RETURN_EMPTY_STRING()\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:       \"int return\",\n\t\t\treturnType: phpInt,\n\t\t\tcontains: []string{\n\t\t\t\t\"RETURN_LONG(result)\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:       \"bool return\",\n\t\t\treturnType: phpBool,\n\t\t\tcontains: []string{\n\t\t\t\t\"RETURN_BOOL(result)\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:       \"float return\",\n\t\t\treturnType: phpFloat,\n\t\t\tcontains: []string{\n\t\t\t\t\"RETURN_DOUBLE(result)\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:       \"array return\",\n\t\t\treturnType: phpArray,\n\t\t\tcontains: []string{\n\t\t\t\t\"RETURN_ARR(result)\",\n\t\t\t\t\"RETURN_EMPTY_ARRAY()\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:       \"void return\",\n\t\t\treturnType: phpVoid,\n\t\t\tcontains:   []string{},\n\t\t},\n\t}\n\n\tgenerator := PHPFuncGenerator{}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := generator.generateReturnCode(phpType(tt.returnType))\n\n\t\t\tif len(tt.contains) == 0 {\n\t\t\t\tassert.Empty(t, result, \"Return code should be empty for void\")\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tfor _, expected := range tt.contains {\n\t\t\t\tassert.Contains(t, result, expected, \"Return code should contain '%s'\", expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestPHPFunctionGenerator_GenerateGoCallParams(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tparams   []phpParameter\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"no parameters\",\n\t\t\tparams:   []phpParameter{},\n\t\t\texpected: \"\",\n\t\t},\n\t\t{\n\t\t\tname: \"simple string parameter\",\n\t\t\tparams: []phpParameter{\n\t\t\t\t{Name: \"message\", PhpType: phpString},\n\t\t\t},\n\t\t\texpected: \"message\",\n\t\t},\n\t\t{\n\t\t\tname: \"int parameter\",\n\t\t\tparams: []phpParameter{\n\t\t\t\t{Name: \"count\", PhpType: phpInt},\n\t\t\t},\n\t\t\texpected: \"(long) count\",\n\t\t},\n\t\t{\n\t\t\tname: \"multiple parameters\",\n\t\t\tparams: []phpParameter{\n\t\t\t\t{Name: \"name\", PhpType: phpString},\n\t\t\t\t{Name: \"age\", PhpType: phpInt},\n\t\t\t},\n\t\t\texpected: \"name, (long) age\",\n\t\t},\n\t\t{\n\t\t\tname: \"bool and float parameters\",\n\t\t\tparams: []phpParameter{\n\t\t\t\t{Name: \"enabled\", PhpType: phpBool},\n\t\t\t\t{Name: \"rate\", PhpType: phpFloat},\n\t\t\t},\n\t\t\texpected: \"(int) enabled, (double) rate\",\n\t\t},\n\t\t{\n\t\t\tname: \"array parameter\",\n\t\t\tparams: []phpParameter{\n\t\t\t\t{Name: \"data\", PhpType: phpArray},\n\t\t\t},\n\t\t\texpected: \"data\",\n\t\t},\n\t\t{\n\t\t\tname: \"mixed parameters with array\",\n\t\t\tparams: []phpParameter{\n\t\t\t\t{Name: \"name\", PhpType: phpString},\n\t\t\t\t{Name: \"items\", PhpType: phpArray},\n\t\t\t\t{Name: \"count\", PhpType: phpInt},\n\t\t\t},\n\t\t\texpected: \"name, items, (long) count\",\n\t\t},\n\t}\n\n\tparser := ParameterParser{}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := parser.generateGoCallParams(tt.params)\n\n\t\t\tassert.Equal(t, tt.expected, result, \"generateGoCallParams() mismatch\")\n\t\t})\n\t}\n}\n\nfunc TestPHPFunctionGenerator_AnalyzeParameters(t *testing.T) {\n\ttests := []struct {\n\t\tname          string\n\t\tparams        []phpParameter\n\t\texpectedReq   int\n\t\texpectedTotal int\n\t}{\n\t\t{\n\t\t\tname:          \"no parameters\",\n\t\t\tparams:        []phpParameter{},\n\t\t\texpectedReq:   0,\n\t\t\texpectedTotal: 0,\n\t\t},\n\t\t{\n\t\t\tname: \"all required\",\n\t\t\tparams: []phpParameter{\n\t\t\t\t{Name: \"a\", PhpType: phpString},\n\t\t\t\t{Name: \"b\", PhpType: phpInt},\n\t\t\t},\n\t\t\texpectedReq:   2,\n\t\t\texpectedTotal: 2,\n\t\t},\n\t\t{\n\t\t\tname: \"mixed required and optional\",\n\t\t\tparams: []phpParameter{\n\t\t\t\t{Name: \"required\", PhpType: phpString},\n\t\t\t\t{Name: \"optional\", PhpType: phpInt, HasDefault: true, DefaultValue: \"10\"},\n\t\t\t},\n\t\t\texpectedReq:   1,\n\t\t\texpectedTotal: 2,\n\t\t},\n\t\t{\n\t\t\tname: \"all optional\",\n\t\t\tparams: []phpParameter{\n\t\t\t\t{Name: \"opt1\", PhpType: phpString, HasDefault: true, DefaultValue: \"hello\"},\n\t\t\t\t{Name: \"opt2\", PhpType: phpInt, HasDefault: true, DefaultValue: \"0\"},\n\t\t\t},\n\t\t\texpectedReq:   0,\n\t\t\texpectedTotal: 2,\n\t\t},\n\t}\n\n\tparser := ParameterParser{}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tinfo := parser.analyzeParameters(tt.params)\n\n\t\t\tassert.Equal(t, tt.expectedReq, info.RequiredCount, \"analyzeParameters() RequiredCount mismatch\")\n\t\t\tassert.Equal(t, tt.expectedTotal, info.TotalCount, \"analyzeParameters() TotalCount mismatch\")\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/extgen/srcanalyzer.go",
    "content": "package extgen\n\nimport (\n\t\"fmt\"\n\t\"go/parser\"\n\t\"go/token\"\n\t\"os\"\n\t\"strings\"\n)\n\ntype SourceAnalyzer struct{}\n\nfunc (sa *SourceAnalyzer) analyze(filename string) (packageName string, variables []string, internalFunctions []string, err error) {\n\tfset := token.NewFileSet()\n\tnode, err := parser.ParseFile(fset, filename, nil, parser.ParseComments)\n\tif err != nil {\n\t\treturn \"\", nil, nil, fmt.Errorf(\"parsing file: %w\", err)\n\t}\n\n\tpackageName = node.Name.Name\n\n\tsourceContent, err := os.ReadFile(filename)\n\tif err != nil {\n\t\treturn \"\", nil, nil, fmt.Errorf(\"reading source file: %w\", err)\n\t}\n\n\tvariables = sa.extractVariables(string(sourceContent))\n\tinternalFunctions = sa.extractInternalFunctions(string(sourceContent))\n\n\treturn packageName, variables, internalFunctions, nil\n}\n\nfunc (sa *SourceAnalyzer) extractVariables(content string) []string {\n\tlines := strings.Split(content, \"\\n\")\n\tvar (\n\t\tvariables  []string\n\t\tcurrentVar strings.Builder\n\t\tinVarBlock bool\n\t\tparenCount int\n\t)\n\n\tfor _, line := range lines {\n\t\ttrimmedLine := strings.TrimSpace(line)\n\n\t\tif strings.HasPrefix(trimmedLine, \"var \") && !inVarBlock {\n\t\t\tif strings.Contains(trimmedLine, \"(\") {\n\t\t\t\tinVarBlock = true\n\t\t\t\tparenCount = 1\n\t\t\t\tcurrentVar.Reset()\n\t\t\t\tcurrentVar.WriteString(line + \"\\n\")\n\t\t\t} else {\n\t\t\t\tvariables = append(variables, strings.TrimSpace(line))\n\t\t\t}\n\t\t} else if inVarBlock {\n\t\t\tcurrentVar.WriteString(line + \"\\n\")\n\n\t\t\tfor _, char := range line {\n\t\t\t\tswitch char {\n\t\t\t\tcase '(':\n\t\t\t\t\tparenCount++\n\t\t\t\tcase ')':\n\t\t\t\t\tparenCount--\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif parenCount == 0 {\n\t\t\t\tvarContent := currentVar.String()\n\t\t\t\tvariables = append(variables, strings.TrimSpace(varContent))\n\t\t\t\tinVarBlock = false\n\t\t\t\tcurrentVar.Reset()\n\t\t\t}\n\t\t}\n\t}\n\n\treturn variables\n}\n\nfunc (sa *SourceAnalyzer) extractInternalFunctions(content string) []string {\n\tlines := strings.Split(content, \"\\n\")\n\tvar (\n\t\tfunctions              []string\n\t\tcurrentFunc            strings.Builder\n\t\tinFunction, hasPHPFunc bool\n\t\tbraceCount             int\n\t)\n\n\tfor i, line := range lines {\n\t\ttrimmedLine := strings.TrimSpace(line)\n\n\t\tif strings.HasPrefix(trimmedLine, \"func \") && !inFunction {\n\t\t\tinFunction = true\n\t\t\tbraceCount = 0\n\t\t\thasPHPFunc = false\n\t\t\tcurrentFunc.Reset()\n\n\t\t\t// look backwards for export_php comment\n\t\t\tfor j := i - 1; j >= 0 && j >= i-5; j-- {\n\t\t\t\tprevLine := strings.TrimSpace(lines[j])\n\t\t\t\tif prevLine == \"\" {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tif strings.Contains(prevLine, \"export_php:\") {\n\t\t\t\t\thasPHPFunc = true\n\n\t\t\t\t\tbreak\n\t\t\t\t}\n\n\t\t\t\tif !strings.HasPrefix(prevLine, \"//\") {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif inFunction {\n\t\t\tcurrentFunc.WriteString(line + \"\\n\")\n\n\t\t\tfor _, char := range line {\n\t\t\t\tswitch char {\n\t\t\t\tcase '{':\n\t\t\t\t\tbraceCount++\n\t\t\t\tcase '}':\n\t\t\t\t\tbraceCount--\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif braceCount == 0 && strings.Contains(line, \"}\") {\n\t\t\t\tfuncContent := currentFunc.String()\n\n\t\t\t\tif !hasPHPFunc {\n\t\t\t\t\tfunctions = append(functions, strings.TrimSpace(funcContent))\n\t\t\t\t}\n\n\t\t\t\tinFunction = false\n\t\t\t\tcurrentFunc.Reset()\n\t\t\t}\n\t\t}\n\t}\n\n\treturn functions\n}\n"
  },
  {
    "path": "internal/extgen/srcanalyzer_test.go",
    "content": "package extgen\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestSourceAnalyzer_Analyze(t *testing.T) {\n\ttests := []struct {\n\t\tname              string\n\t\tsourceContent     string\n\t\texpectedImports   []string\n\t\texpectedVariables []string\n\t\texpectedFunctions []string\n\t\texpectError       bool\n\t}{\n\t\t{\n\t\t\tname: \"simple file with imports and functions\",\n\t\t\tsourceContent: `package main\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n)\n\nfunc regularFunction() {\n\tfmt.Println(\"hello\")\n}\n\n//export_php:function\nfunc exportedFunction() string {\n\treturn \"exported\"\n}`,\n\t\t\texpectedImports:   []string{`\"fmt\"`, `\"strings\"`},\n\t\t\texpectedVariables: nil,\n\t\t\texpectedFunctions: []string{\n\t\t\t\t`func regularFunction() {\n\tfmt.Println(\"hello\")\n}`,\n\t\t\t},\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname: \"file with named imports\",\n\t\t\tsourceContent: `package main\n\nimport (\n\tcustom \"fmt\"\n\t. \"strings\"\n\t_ \"os\"\n)\n\nfunc test() {}`,\n\t\t\texpectedImports:   []string{`custom \"fmt\"`, `. \"strings\"`, `_ \"os\"`},\n\t\t\texpectedVariables: nil,\n\t\t\texpectedFunctions: []string{\n\t\t\t\t`func test() {}`,\n\t\t\t},\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname: \"file with multiple functions and export comments\",\n\t\t\tsourceContent: `package main\n\nfunc internalOne() {\n\t// some code\n}\n\n// This function is exported to PHP\n//export_php:function\nfunc exportedOne() int {\n\treturn 42\n}\n\nfunc internalTwo() string {\n\treturn \"internal\"\n}\n\n// Another exported function\n//export_php:function  \nfunc exportedTwo() bool {\n\treturn true\n}`,\n\t\t\texpectedImports:   []string{},\n\t\t\texpectedVariables: nil,\n\t\t\texpectedFunctions: []string{\n\t\t\t\t`func internalOne() {\n\t// some code\n}`,\n\t\t\t\t`func internalTwo() string {\n\treturn \"internal\"\n}`,\n\t\t\t},\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname: \"file with nested braces\",\n\t\t\tsourceContent: `package main\n\nfunc complexFunction() {\n\tif true {\n\t\tfor i := 0; i < 10; i++ {\n\t\t\tif i%2 == 0 {\n\t\t\t\tfmt.Println(i)\n\t\t\t}\n\t\t}\n\t}\n}\n\n//export_php:function\nfunc exportedComplex() {\n\tobj := struct{\n\t\tfield string\n\t}{\n\t\tfield: \"value\",\n\t}\n\tfmt.Println(obj)\n}`,\n\t\t\texpectedImports:   []string{},\n\t\t\texpectedVariables: nil,\n\t\t\texpectedFunctions: []string{\n\t\t\t\t`func complexFunction() {\n\tif true {\n\t\tfor i := 0; i < 10; i++ {\n\t\t\tif i%2 == 0 {\n\t\t\t\tfmt.Println(i)\n\t\t\t}\n\t\t}\n\t}\n}`,\n\t\t\t},\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname:              \"empty file\",\n\t\t\tsourceContent:     `package main`,\n\t\t\texpectedImports:   []string{},\n\t\t\texpectedFunctions: []string{},\n\t\t\texpectError:       false,\n\t\t},\n\t\t{\n\t\t\tname: \"file with only exported functions\",\n\t\t\tsourceContent: `package main\n\n//export_php:function\nfunc onlyExported() {}\n\n//export_php:function\nfunc anotherExported() string {\n\treturn \"test\"\n}`,\n\t\t\texpectedImports:   []string{},\n\t\t\texpectedFunctions: []string{},\n\t\t\texpectError:       false,\n\t\t},\n\t\t{\n\t\t\tname: \"file with export comment not immediately before function\",\n\t\t\tsourceContent: `package main\n\n//export_php:function\n// Some other comment\nfunc shouldNotBeExported() {}\n\nfunc normalFunction() {\n\t//export_php:function inside function should not count\n}`,\n\t\t\texpectedImports:   []string{},\n\t\t\texpectedVariables: nil,\n\t\t\texpectedFunctions: []string{\n\t\t\t\t`func normalFunction() {\n\t//export_php:function inside function should not count\n}`,\n\t\t\t},\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname: \"file with variable blocks\",\n\t\t\tsourceContent: `package main\n\nimport (\n\t\"sync\"\n)\n\nvar (\n\tmu    sync.RWMutex\n\tstore = map[string]struct {\n\t\tval     string\n\t\texpires int64\n\t}{}\n)\n\nvar singleVar = \"test\"\n\nfunc testFunction() {\n\t// test function\n}`,\n\t\t\texpectedImports: []string{`\"sync\"`},\n\t\t\texpectedVariables: []string{\n\t\t\t\t`var (\n\tmu    sync.RWMutex\n\tstore = map[string]struct {\n\t\tval     string\n\t\texpires int64\n\t}{}\n)`,\n\t\t\t\t`var singleVar = \"test\"`,\n\t\t\t},\n\t\t\texpectedFunctions: []string{\n\t\t\t\t`func testFunction() {\n\t// test function\n}`,\n\t\t\t},\n\t\t\texpectError: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ttempDir := t.TempDir()\n\t\t\tfilename := filepath.Join(tempDir, \"test.go\")\n\n\t\t\trequire.NoError(t, os.WriteFile(filename, []byte(tt.sourceContent), 0644))\n\n\t\t\tanalyzer := &SourceAnalyzer{}\n\t\t\t_, variables, functions, err := analyzer.analyze(filename)\n\n\t\t\tif tt.expectError {\n\t\t\t\tassert.Error(t, err, \"expected error\")\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.NoError(t, err, \"unexpected error\")\n\n\t\t\tassert.Equal(t, tt.expectedVariables, variables, \"variables mismatch\")\n\t\t\tassert.Len(t, functions, len(tt.expectedFunctions), \"function count mismatch\")\n\n\t\t\tfor i, expected := range tt.expectedFunctions {\n\t\t\t\tassert.Equal(t, expected, functions[i], \"function %d mismatch\", i)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSourceAnalyzer_Analyze_InvalidFile(t *testing.T) {\n\tanalyzer := &SourceAnalyzer{}\n\n\tt.Run(\"nonexistent file\", func(t *testing.T) {\n\t\t_, _, _, err := analyzer.analyze(\"/nonexistent/file.go\")\n\t\tassert.Error(t, err, \"expected error for nonexistent file\")\n\t})\n\n\tt.Run(\"invalid Go syntax\", func(t *testing.T) {\n\t\ttempDir := t.TempDir()\n\t\tfilename := filepath.Join(tempDir, \"invalid.go\")\n\n\t\tinvalidContent := `package main\n\t\tfunc incomplete( {\n\t\t\t// invalid syntax\n\t\t`\n\n\t\trequire.NoError(t, os.WriteFile(filename, []byte(invalidContent), 0644))\n\n\t\t_, _, _, err := analyzer.analyze(filename)\n\t\tassert.Error(t, err, \"expected error for invalid syntax\")\n\t})\n}\n\nfunc TestSourceAnalyzer_ExtractInternalFunctions(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tcontent  string\n\t\texpected []string\n\t}{\n\t\t{\n\t\t\tname: \"single function without export\",\n\t\t\tcontent: `func test() {\n\tfmt.Println(\"test\")\n}`,\n\t\t\texpected: []string{\n\t\t\t\t`func test() {\n\tfmt.Println(\"test\")\n}`,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"function with export comment\",\n\t\t\tcontent: `//export_php:function\nfunc exported() {}`,\n\t\t\texpected: []string{},\n\t\t},\n\t\t{\n\t\t\tname: \"mixed functions\",\n\t\t\tcontent: `func internal() {}\n\n//export_php:function\nfunc exported() {}\n\nfunc anotherInternal() {\n\treturn \"test\"\n}`,\n\t\t\texpected: []string{\n\t\t\t\t\"func internal() {}\",\n\t\t\t\t`func anotherInternal() {\n\treturn \"test\"\n}`,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"export comment with spacing\",\n\t\t\tcontent: `//export_php:function\nfunc exported1() {}\n\n//export_php:function\nfunc exported2() {}\n\n// export_php:function   \nfunc exported3() {}`,\n\t\t\texpected: []string{},\n\t\t},\n\t\t{\n\t\t\tname: \"complex function with nested braces\",\n\t\t\tcontent: `func complex() {\n\tif true {\n\t\tfor {\n\t\t\tswitch x {\n\t\t\tcase 1:\n\t\t\t\t{\n\t\t\t\t\t// nested block\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}`,\n\t\t\texpected: []string{\n\t\t\t\t`func complex() {\n\tif true {\n\t\tfor {\n\t\t\tswitch x {\n\t\t\tcase 1:\n\t\t\t\t{\n\t\t\t\t\t// nested block\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}`,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"empty content\",\n\t\t\tcontent:  \"\",\n\t\t\texpected: []string{},\n\t\t},\n\t\t{\n\t\t\tname: \"no functions\",\n\t\t\tcontent: `package main\n\nimport \"fmt\"\n\nvar x = 10`,\n\t\t\texpected: []string{},\n\t\t},\n\t}\n\n\tanalyzer := &SourceAnalyzer{}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := analyzer.extractInternalFunctions(tt.content)\n\n\t\t\tassert.Len(t, result, len(tt.expected), \"function count mismatch\")\n\n\t\t\tfor i, expected := range tt.expected {\n\t\t\t\tassert.Equal(t, expected, result[i], \"function %d mismatch\", i)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSourceAnalyzer_InternalFunctionPreservation(t *testing.T) {\n\ttmpDir := t.TempDir()\n\n\tsourceContent := `package main\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n)\n\n//export_php: exported1(): string\nfunc exported1() *go_value {\n\treturn String(internal1())\n}\n\nfunc internal1() string {\n\treturn \"helper1\"\n}\n\n//export_php: exported2(): void\nfunc exported2() {\n\tinternal2()\n}\n\nfunc internal2() {\n\tfmt.Println(\"helper2\")\n}\n\nfunc internal3(data string) string {\n\treturn strings.ToUpper(data)\n}`\n\n\tsourceFile := filepath.Join(tmpDir, \"test.go\")\n\trequire.NoError(t, os.WriteFile(sourceFile, []byte(sourceContent), 0644))\n\n\tanalyzer := &SourceAnalyzer{}\n\tpackageName, variables, internalFuncs, err := analyzer.analyze(sourceFile)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, \"main\", packageName)\n\n\tassert.Len(t, internalFuncs, 3, \"Should extract exactly 3 internal functions\")\n\n\texpectedInternalFuncs := []string{\n\t\t`func internal1() string {\n\treturn \"helper1\"\n}`,\n\t\t`func internal2() {\n\tfmt.Println(\"helper2\")\n}`,\n\t\t`func internal3(data string) string {\n\treturn strings.ToUpper(data)\n}`,\n\t}\n\n\tfor i, expected := range expectedInternalFuncs {\n\t\tassert.Equal(t, expected, internalFuncs[i], \"Internal function %d should match\", i)\n\t}\n\n\tassert.Empty(t, variables, \"Should not have variables\")\n}\n\nfunc TestSourceAnalyzer_VariableBlockPreservation(t *testing.T) {\n\ttmpDir := t.TempDir()\n\n\tsourceContent := `package main\n\nimport (\n\t\"sync\"\n)\n\nvar (\n\tmu    sync.RWMutex\n\tcache = make(map[string]string)\n)\n\nvar globalCounter int = 0\n\n//export_php: test(): void\nfunc test() {}`\n\n\tsourceFile := filepath.Join(tmpDir, \"test.go\")\n\trequire.NoError(t, os.WriteFile(sourceFile, []byte(sourceContent), 0644))\n\n\tanalyzer := &SourceAnalyzer{}\n\tpackageName, variables, internalFuncs, err := analyzer.analyze(sourceFile)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, \"main\", packageName)\n\n\tassert.Len(t, variables, 2, \"Should extract exactly 2 variable declarations\")\n\n\texpectedVar1 := `var (\n\tmu    sync.RWMutex\n\tcache = make(map[string]string)\n)`\n\texpectedVar2 := `var globalCounter int = 0`\n\n\tassert.Equal(t, expectedVar1, variables[0], \"First variable block should match\")\n\tassert.Equal(t, expectedVar2, variables[1], \"Second variable declaration should match\")\n\n\tassert.Empty(t, internalFuncs, \"Should not have internal functions (only exported function)\")\n}\n\nfunc BenchmarkSourceAnalyzer_Analyze(b *testing.B) {\n\tcontent := `package main\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"os\"\n)\n\nfunc internalOne() {\n\tfmt.Println(\"test\")\n}\n\n//export_php:function\nfunc exported() string {\n\treturn \"exported\"\n}\n\nfunc internalTwo() {\n\tfor i := 0; i < 100; i++ {\n\t\tif i%2 == 0 {\n\t\t\tfmt.Println(i)\n\t\t}\n\t}\n}`\n\n\ttempDir := b.TempDir()\n\tfilename := filepath.Join(tempDir, \"bench.go\")\n\n\trequire.NoError(b, os.WriteFile(filename, []byte(content), 0644))\n\n\tanalyzer := &SourceAnalyzer{}\n\n\tfor b.Loop() {\n\t\t_, _, _, err := analyzer.analyze(filename)\n\t\trequire.NoError(b, err)\n\t}\n}\n\nfunc BenchmarkSourceAnalyzer_ExtractInternalFunctions(b *testing.B) {\n\tcontent := `func test1() { fmt.Println(\"1\") }\nfunc test2() { fmt.Println(\"2\") }\n//export_php:function\nfunc exported() {}\nfunc test3() { \n\tfor i := 0; i < 10; i++ {\n\t\tfmt.Println(i)\n\t}\n}`\n\n\tanalyzer := &SourceAnalyzer{}\n\n\tfor b.Loop() {\n\t\tanalyzer.extractInternalFunctions(content)\n\t}\n}\n"
  },
  {
    "path": "internal/extgen/stub.go",
    "content": "package extgen\n\nimport (\n\t_ \"embed\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"text/template\"\n)\n\n//go:embed templates/stub.php.tpl\nvar templateContent string\n\ntype StubGenerator struct {\n\tGenerator *Generator\n}\n\nfunc (sg *StubGenerator) generate() error {\n\tfilename := filepath.Join(sg.Generator.BuildDir, sg.Generator.BaseName+\".stub.php\")\n\tcontent, err := sg.buildContent()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn writeFile(filename, content)\n}\n\nfunc (sg *StubGenerator) buildContent() (string, error) {\n\ttmpl, err := template.New(\"stub.php.tpl\").Funcs(template.FuncMap{\n\t\t\"phpType\": getPhpTypeAnnotation,\n\t}).Parse(templateContent)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tvar buf strings.Builder\n\tif err := tmpl.Execute(&buf, sg.Generator); err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn buf.String(), nil\n}\n\n// getPhpTypeAnnotation converts phpType to PHP type annotation\nfunc getPhpTypeAnnotation(t phpType) string {\n\tswitch t {\n\tcase phpString:\n\t\treturn \"string\"\n\tcase phpBool:\n\t\treturn \"bool\"\n\tcase phpFloat:\n\t\treturn \"float\"\n\tcase phpInt:\n\t\treturn \"int\"\n\tcase phpArray:\n\t\treturn \"array\"\n\tdefault:\n\t\treturn \"int\"\n\t}\n}\n"
  },
  {
    "path": "internal/extgen/stub_test.go",
    "content": "package extgen\n\nimport (\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestStubGenerator_Generate(t *testing.T) {\n\ttmpDir := t.TempDir()\n\n\tgenerator := &Generator{\n\t\tBaseName: \"test_extension\",\n\t\tBuildDir: tmpDir,\n\t\tFunctions: []phpFunction{\n\t\t\t{\n\t\t\t\tName:      \"greet\",\n\t\t\t\tSignature: \"greet(string $name): string\",\n\t\t\t\tParams: []phpParameter{\n\t\t\t\t\t{Name: \"name\", PhpType: phpString},\n\t\t\t\t},\n\t\t\t\tReturnType: phpString,\n\t\t\t},\n\t\t\t{\n\t\t\t\tName:      \"calculate\",\n\t\t\t\tSignature: \"calculate(int $a, int $b): int\",\n\t\t\t\tParams: []phpParameter{\n\t\t\t\t\t{Name: \"a\", PhpType: phpInt},\n\t\t\t\t\t{Name: \"b\", PhpType: phpInt},\n\t\t\t\t},\n\t\t\t\tReturnType: phpInt,\n\t\t\t},\n\t\t},\n\t\tClasses: []phpClass{\n\t\t\t{\n\t\t\t\tName:     \"User\",\n\t\t\t\tGoStruct: \"UserStruct\",\n\t\t\t},\n\t\t},\n\t\tConstants: []phpConstant{\n\t\t\t{\n\t\t\t\tName:    \"GLOBAL_CONST\",\n\t\t\t\tValue:   \"42\",\n\t\t\t\tPhpType: phpInt,\n\t\t\t},\n\t\t\t{\n\t\t\t\tName:      \"USER_STATUS_ACTIVE\",\n\t\t\t\tValue:     \"1\",\n\t\t\t\tPhpType:   phpInt,\n\t\t\t\tClassName: \"User\",\n\t\t\t},\n\t\t},\n\t}\n\n\tstubGen := StubGenerator{generator}\n\tassert.NoError(t, stubGen.generate(), \"generate() failed\")\n\n\texpectedFile := filepath.Join(tmpDir, \"test_extension.stub.php\")\n\tassert.FileExists(t, expectedFile, \"Expected stub file was not created: %s\", expectedFile)\n\n\tcontent, err := readFile(expectedFile)\n\tassert.NoError(t, err, \"Failed to read generated stub file\")\n\n\ttestStubBasicStructure(t, content)\n\ttestStubFunctions(t, content, generator.Functions)\n\ttestStubClasses(t, content, generator.Classes)\n\ttestStubConstants(t, content, generator.Constants)\n}\n\nfunc TestStubGenerator_BuildContent(t *testing.T) {\n\ttests := []struct {\n\t\tname      string\n\t\tfunctions []phpFunction\n\t\tclasses   []phpClass\n\t\tconstants []phpConstant\n\t\tcontains  []string\n\t}{\n\t\t{\n\t\t\tname:      \"empty extension\",\n\t\t\tfunctions: []phpFunction{},\n\t\t\tclasses:   []phpClass{},\n\t\t\tconstants: []phpConstant{},\n\t\t\tcontains: []string{\n\t\t\t\t\"<?php\",\n\t\t\t\t\"/** @generate-class-entries */\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"functions only\",\n\t\t\tfunctions: []phpFunction{\n\t\t\t\t{\n\t\t\t\t\tName:      \"testFunc\",\n\t\t\t\t\tSignature: \"testFunc(string $param): bool\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tclasses:   []phpClass{},\n\t\t\tconstants: []phpConstant{},\n\t\t\tcontains: []string{\n\t\t\t\t\"<?php\",\n\t\t\t\t\"/** @generate-class-entries */\",\n\t\t\t\t\"function testFunc(string $param): bool {}\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:      \"classes only\",\n\t\t\tfunctions: []phpFunction{},\n\t\t\tclasses: []phpClass{\n\t\t\t\t{\n\t\t\t\t\tName: \"TestClass\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tconstants: []phpConstant{},\n\t\t\tcontains: []string{\n\t\t\t\t\"<?php\",\n\t\t\t\t\"/** @generate-class-entries */\",\n\t\t\t\t\"class TestClass {\",\n\t\t\t\t\"public function __construct() {}\",\n\t\t\t\t\"}\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:      \"constants only\",\n\t\t\tfunctions: []phpFunction{},\n\t\t\tclasses:   []phpClass{},\n\t\t\tconstants: []phpConstant{\n\t\t\t\t{\n\t\t\t\t\tName:    \"GLOBAL_CONST\",\n\t\t\t\t\tValue:   `\"test\"`,\n\t\t\t\t\tPhpType: phpString,\n\t\t\t\t},\n\t\t\t},\n\t\t\tcontains: []string{\n\t\t\t\t\"<?php\",\n\t\t\t\t\"/** @generate-class-entries */\",\n\t\t\t\t`const GLOBAL_CONST = \"test\";`,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"functions and classes\",\n\t\t\tfunctions: []phpFunction{\n\t\t\t\t{\n\t\t\t\t\tName:      \"process\",\n\t\t\t\t\tSignature: \"process(array $data): array\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tclasses: []phpClass{\n\t\t\t\t{\n\t\t\t\t\tName: \"Result\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tconstants: []phpConstant{},\n\t\t\tcontains: []string{\n\t\t\t\t\"function process(array $data): array {}\",\n\t\t\t\t\"class Result {\",\n\t\t\t\t\"public function __construct() {}\",\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgenerator := &Generator{\n\t\t\t\tFunctions: tt.functions,\n\t\t\t\tClasses:   tt.classes,\n\t\t\t\tConstants: tt.constants,\n\t\t\t}\n\n\t\t\tstubGen := StubGenerator{generator}\n\t\t\tcontent, err := stubGen.buildContent()\n\t\t\tassert.NoError(t, err, \"buildContent() failed\")\n\n\t\t\tfor _, expected := range tt.contains {\n\t\t\t\tassert.Contains(t, content, expected, \"Generated stub content should contain '%s'\", expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestStubGenerator_FunctionSignatures(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tfunction phpFunction\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname: \"simple function\",\n\t\t\tfunction: phpFunction{\n\t\t\t\tName:      \"test\",\n\t\t\t\tSignature: \"test(): void\",\n\t\t\t},\n\t\t\texpected: \"function test(): void {}\",\n\t\t},\n\t\t{\n\t\t\tname: \"function with parameters\",\n\t\t\tfunction: phpFunction{\n\t\t\t\tName:      \"greet\",\n\t\t\t\tSignature: \"greet(string $name): string\",\n\t\t\t},\n\t\t\texpected: \"function greet(string $name): string {}\",\n\t\t},\n\t\t{\n\t\t\tname: \"function with nullable return\",\n\t\t\tfunction: phpFunction{\n\t\t\t\tName:      \"findUser\",\n\t\t\t\tSignature: \"findUser(int $id): ?object\",\n\t\t\t},\n\t\t\texpected: \"function findUser(int $id): ?object {}\",\n\t\t},\n\t\t{\n\t\t\tname: \"complex function signature\",\n\t\t\tfunction: phpFunction{\n\t\t\t\tName:      \"process\",\n\t\t\t\tSignature: \"process(array $data, ?string $prefix = null, bool $strict = false): ?array\",\n\t\t\t},\n\t\t\texpected: \"function process(array $data, ?string $prefix = null, bool $strict = false): ?array {}\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgenerator := &Generator{\n\t\t\t\tFunctions: []phpFunction{tt.function},\n\t\t\t}\n\n\t\t\tstubGen := StubGenerator{generator}\n\t\t\tcontent, err := stubGen.buildContent()\n\t\t\tassert.NoError(t, err, \"buildContent() failed\")\n\t\t\tassert.Contains(t, content, tt.expected, \"Generated content should contain function signature: %s\", tt.expected)\n\t\t})\n\t}\n}\n\nfunc TestStubGenerator_ClassGeneration(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tclass    phpClass\n\t\tcontains []string\n\t}{\n\t\t{\n\t\t\tname: \"simple class\",\n\t\t\tclass: phpClass{\n\t\t\t\tName: \"SimpleClass\",\n\t\t\t},\n\t\t\tcontains: []string{\n\t\t\t\t\"class SimpleClass {\",\n\t\t\t\t\"public function __construct() {}\",\n\t\t\t\t\"}\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"class with no properties\",\n\t\t\tclass: phpClass{\n\t\t\t\tName: \"EmptyClass\",\n\t\t\t},\n\t\t\tcontains: []string{\n\t\t\t\t\"class EmptyClass {\",\n\t\t\t\t\"public function __construct() {}\",\n\t\t\t\t\"}\",\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgenerator := &Generator{\n\t\t\t\tClasses: []phpClass{tt.class},\n\t\t\t}\n\n\t\t\tstubGen := StubGenerator{generator}\n\t\t\tcontent, err := stubGen.buildContent()\n\t\t\tassert.NoError(t, err, \"buildContent() failed\")\n\n\t\t\tfor _, expected := range tt.contains {\n\t\t\t\tassert.Contains(t, content, expected, \"Generated content should contain '%s'\", expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestStubGenerator_MultipleItems(t *testing.T) {\n\tfunctions := []phpFunction{\n\t\t{\n\t\t\tName:      \"func1\",\n\t\t\tSignature: \"func1(): void\",\n\t\t},\n\t\t{\n\t\t\tName:      \"func2\",\n\t\t\tSignature: \"func2(string $param): bool\",\n\t\t},\n\t\t{\n\t\t\tName:      \"func3\",\n\t\t\tSignature: \"func3(int $a, int $b): int\",\n\t\t},\n\t}\n\n\tclasses := []phpClass{\n\t\t{\n\t\t\tName: \"Class1\",\n\t\t},\n\t\t{\n\t\t\tName: \"Class2\",\n\t\t},\n\t}\n\n\tgenerator := &Generator{\n\t\tFunctions: functions,\n\t\tClasses:   classes,\n\t}\n\n\tstubGen := StubGenerator{generator}\n\tcontent, err := stubGen.buildContent()\n\tassert.NoError(t, err, \"buildContent() failed\")\n\n\tfor _, fn := range functions {\n\t\texpectedFunc := \"function \" + fn.Name\n\t\tassert.Contains(t, content, expectedFunc, \"Should contain function: %s\", expectedFunc)\n\t}\n\n\tfor _, class := range classes {\n\t\texpectedClass := \"class \" + class.Name\n\t\tassert.Contains(t, content, expectedClass, \"Should contain class: %s\", expectedClass)\n\t}\n\n\tfuncPos := strings.Index(content, \"function func1\")\n\tclassPos := strings.Index(content, \"class Class1\")\n\n\tassert.NotEqual(t, -1, funcPos, \"functions should be present\")\n\tassert.NotEqual(t, -1, classPos, \"classes should be present\")\n\tassert.Less(t, funcPos, classPos, \"functions should appear before classes in the stub file\")\n}\n\nfunc TestStubGenerator_ErrorHandling(t *testing.T) {\n\tgenerator := &Generator{\n\t\tBaseName: \"test\",\n\t\tBuildDir: \"/invalid/readonly/path\",\n\t\tFunctions: []phpFunction{\n\t\t\t{Name: \"test\", Signature: \"test(): void\"},\n\t\t},\n\t}\n\n\tstubGen := StubGenerator{generator}\n\terr := stubGen.generate()\n\tassert.Error(t, err, \"Expected error when writing to invalid directory\")\n}\n\nfunc TestStubGenerator_EmptyContent(t *testing.T) {\n\tgenerator := &Generator{\n\t\tFunctions: []phpFunction{},\n\t\tClasses:   []phpClass{},\n\t}\n\n\tstubGen := StubGenerator{generator}\n\tcontent, err := stubGen.buildContent()\n\tassert.NoError(t, err, \"buildContent() failed\")\n\n\texpectedMinimal := []string{\n\t\t\"<?php\",\n\t\t\"/** @generate-class-entries */\",\n\t}\n\n\tfor _, expected := range expectedMinimal {\n\t\tassert.Contains(t, content, expected, \"Even empty content should contain: %s\", expected)\n\t}\n\n\tassert.NotContains(t, content, \"function \", \"Empty stub should not contain function declarations\")\n\tassert.NotContains(t, content, \"class \", \"Empty stub should not contain class declarations\")\n}\n\nfunc TestStubGenerator_PHPSyntaxValidation(t *testing.T) {\n\tfunctions := []phpFunction{\n\t\t{\n\t\t\tName:      \"complexFunc\",\n\t\t\tSignature: \"complexFunc(?string $name = null, array $options = [], bool $strict = false): ?object\",\n\t\t},\n\t}\n\n\tclasses := []phpClass{\n\t\t{\n\t\t\tName: \"ComplexClass\",\n\t\t},\n\t}\n\n\tgenerator := &Generator{\n\t\tFunctions: functions,\n\t\tClasses:   classes,\n\t}\n\n\tstubGen := StubGenerator{generator}\n\tcontent, err := stubGen.buildContent()\n\tassert.NoError(t, err, \"buildContent() failed\")\n\n\tsyntaxChecks := []struct {\n\t\telement string\n\t\treason  string\n\t}{\n\t\t{\"<?php\", \"should start with PHP opening tag\"},\n\t\t{\"{\", \"should contain opening braces\"},\n\t\t{\"}\", \"should contain closing braces\"},\n\t\t{\"public\", \"should use proper visibility\"},\n\t\t{\"function\", \"should contain function keyword\"},\n\t\t{\"class\", \"should contain class keyword\"},\n\t}\n\n\tfor _, check := range syntaxChecks {\n\t\tassert.Contains(t, content, check.element, \"Generated PHP %s\", check.reason)\n\t}\n\n\topenBraces := strings.Count(content, \"{\")\n\tcloseBraces := strings.Count(content, \"}\")\n\tassert.Equal(t, openBraces, closeBraces, \"Unbalanced braces in PHP: %d open, %d close\", openBraces, closeBraces)\n\n\tassert.Contains(t, content, \"function complexFunc(?string $name = null, array $options = [], bool $strict = false): ?object {}\", \"Complex function signature should be preserved exactly\")\n}\n\nfunc TestStubGenerator_ClassConstants(t *testing.T) {\n\ttests := []struct {\n\t\tname      string\n\t\tclasses   []phpClass\n\t\tconstants []phpConstant\n\t\tcontains  []string\n\t}{\n\t\t{\n\t\t\tname: \"class with constants\",\n\t\t\tclasses: []phpClass{\n\t\t\t\t{Name: \"MyClass\"},\n\t\t\t},\n\t\t\tconstants: []phpConstant{\n\t\t\t\t{\n\t\t\t\t\tName:      \"STATUS_ACTIVE\",\n\t\t\t\t\tValue:     \"1\",\n\t\t\t\t\tPhpType:   phpInt,\n\t\t\t\t\tClassName: \"MyClass\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tName:      \"STATUS_INACTIVE\",\n\t\t\t\t\tValue:     \"0\",\n\t\t\t\t\tPhpType:   phpInt,\n\t\t\t\t\tClassName: \"MyClass\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tcontains: []string{\n\t\t\t\t\"class MyClass {\",\n\t\t\t\t\"public const STATUS_ACTIVE = 1;\",\n\t\t\t\t\"public const STATUS_INACTIVE = 0;\",\n\t\t\t\t\"public function __construct() {}\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"class with iota constants\",\n\t\t\tclasses: []phpClass{\n\t\t\t\t{Name: \"StatusClass\"},\n\t\t\t},\n\t\t\tconstants: []phpConstant{\n\t\t\t\t{\n\t\t\t\t\tName:      \"FIRST\",\n\t\t\t\t\tValue:     \"0\",\n\t\t\t\t\tPhpType:   phpInt,\n\t\t\t\t\tIsIota:    true,\n\t\t\t\t\tClassName: \"StatusClass\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tName:      \"SECOND\",\n\t\t\t\t\tValue:     \"1\",\n\t\t\t\t\tPhpType:   phpInt,\n\t\t\t\t\tIsIota:    true,\n\t\t\t\t\tClassName: \"StatusClass\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tcontains: []string{\n\t\t\t\t\"class StatusClass {\",\n\t\t\t\t\"public const FIRST = UNKNOWN;\",\n\t\t\t\t\"public const SECOND = UNKNOWN;\",\n\t\t\t\t\"@cvalue FIRST\",\n\t\t\t\t\"@cvalue SECOND\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"global and class constants\",\n\t\t\tclasses: []phpClass{\n\t\t\t\t{Name: \"TestClass\"},\n\t\t\t},\n\t\t\tconstants: []phpConstant{\n\t\t\t\t{\n\t\t\t\t\tName:    \"GLOBAL_CONST\",\n\t\t\t\t\tValue:   `\"global\"`,\n\t\t\t\t\tPhpType: phpString,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tName:      \"CLASS_CONST\",\n\t\t\t\t\tValue:     \"42\",\n\t\t\t\t\tPhpType:   phpInt,\n\t\t\t\t\tClassName: \"TestClass\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tcontains: []string{\n\t\t\t\t`const GLOBAL_CONST = \"global\";`,\n\t\t\t\t\"class TestClass {\",\n\t\t\t\t\"public const CLASS_CONST = 42;\",\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgenerator := &Generator{\n\t\t\t\tClasses:   tt.classes,\n\t\t\t\tConstants: tt.constants,\n\t\t\t}\n\n\t\t\tstubGen := StubGenerator{generator}\n\t\t\tcontent, err := stubGen.buildContent()\n\t\t\tassert.NoError(t, err, \"buildContent() failed\")\n\n\t\t\tfor _, expected := range tt.contains {\n\t\t\t\tassert.Contains(t, content, expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestStubGenerator_FileStructure(t *testing.T) {\n\tgenerator := &Generator{\n\t\tFunctions: []phpFunction{\n\t\t\t{Name: \"testFunc\", Signature: \"testFunc(): void\"},\n\t\t},\n\t\tClasses: []phpClass{\n\t\t\t{\n\t\t\t\tName: \"TestClass\",\n\t\t\t},\n\t\t},\n\t}\n\n\tstubGen := StubGenerator{generator}\n\tcontent, err := stubGen.buildContent()\n\tassert.NoError(t, err, \"buildContent() failed\")\n\n\tlines := strings.Split(content, \"\\n\")\n\n\tassert.GreaterOrEqual(t, len(lines), 3, \"Stub file should have multiple lines\")\n\tassert.Equal(t, \"<?php\", strings.TrimSpace(lines[0]), \"First line should be <?php opening tag\")\n\n\tfoundGenerateDirective := false\n\tfor _, line := range lines {\n\t\tif strings.Contains(line, \"@generate-class-entries\") {\n\t\t\tfoundGenerateDirective = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\tassert.True(t, foundGenerateDirective, \"Should contain @generate-class-entries directive\")\n\tassert.Contains(t, strings.Join(lines, \"\\n\"), \"\\n\\n\", \"Should have proper spacing between sections\")\n}\n\nfunc testStubBasicStructure(t *testing.T, content string) {\n\trequiredElements := []string{\n\t\t\"<?php\",\n\t\t\"/** @generate-class-entries */\",\n\t}\n\n\tfor _, element := range requiredElements {\n\t\tassert.Contains(t, content, element, \"Stub file should contain: %s\", element)\n\t}\n\n\tlines := strings.Split(content, \"\\n\")\n\tif len(lines) > 0 {\n\t\tassert.Equal(t, \"<?php\", strings.TrimSpace(lines[0]), \"Stub file should start with <?php\")\n\t}\n}\n\nfunc testStubFunctions(t *testing.T, content string, functions []phpFunction) {\n\tfor _, fn := range functions {\n\t\texpectedFunc := \"function \" + fn.Signature + \" {}\"\n\t\tassert.Contains(t, content, expectedFunc, \"Stub should contain function: %s\", expectedFunc)\n\t}\n}\n\nfunc testStubClasses(t *testing.T, content string, classes []phpClass) {\n\tfor _, class := range classes {\n\t\texpectedClass := \"class \" + class.Name + \" {\"\n\t\tassert.Contains(t, content, expectedClass, \"Stub should contain class: %s\", expectedClass)\n\n\t\texpectedConstructor := \"public function __construct() {}\"\n\t\tassert.Contains(t, content, expectedConstructor, \"Class %s should have constructor\", class.Name)\n\n\t\tassert.Contains(t, content, \"}\", \"Class %s should be properly closed\", class.Name)\n\t}\n}\n\nfunc testStubConstants(t *testing.T, content string, constants []phpConstant) {\n\tfor _, constant := range constants {\n\t\tif constant.ClassName == \"\" {\n\t\t\tif constant.IsIota {\n\t\t\t\texpectedConst := \"const \" + constant.Name + \" = UNKNOWN;\"\n\t\t\t\tassert.Contains(t, content, expectedConst, \"Stub should contain iota constant: %s\", expectedConst)\n\t\t\t} else {\n\t\t\t\texpectedConst := \"const \" + constant.Name + \" = \" + constant.Value + \";\"\n\t\t\t\tassert.Contains(t, content, expectedConst, \"Stub should contain constant: %s\", expectedConst)\n\t\t\t}\n\n\t\t\tcontinue\n\t\t}\n\t\tif constant.IsIota {\n\t\t\texpectedConst := \"public const \" + constant.Name + \" = UNKNOWN;\"\n\t\t\tassert.Contains(t, content, expectedConst, \"Stub should contain class iota constant: %s\", expectedConst)\n\t\t} else {\n\t\t\texpectedConst := \"public const \" + constant.Name + \" = \" + constant.Value + \";\"\n\t\t\tassert.Contains(t, content, expectedConst, \"Stub should contain class constant: %s\", expectedConst)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/extgen/templates/README.md.tpl",
    "content": "# {{.BaseName}} Extension\n\nAuto-generated PHP extension from Go code.\n\n{{if .Functions}}## Functions\n\n{{range .Functions}}### {{.Name}}\n\n```php\n{{.Signature}}\n```\n\n{{if .Params}}**Parameters:**\n\n{{range .Params}}- `{{.Name}}` ({{.PhpType}}){{if .IsNullable}} (nullable){{end}}{{if .HasDefault}} (default: {{.DefaultValue}}){{end}}\n{{end}}\n{{end}}**Returns:** {{.ReturnType}}{{if .IsReturnNullable}} (nullable){{end}}\n\n{{end}}{{end}}{{if .Classes}}## Classes\n\n{{range .Classes}}### {{.Name}}\n\n{{if .Properties}}**Properties:**\n\n{{range .Properties}}- `{{.Name}}`: {{.PhpType}}{{if .IsNullable}} (nullable){{end}}\n{{end}}\n{{end}}{{end}}{{end}}\n"
  },
  {
    "path": "internal/extgen/templates/extension.c.tpl",
    "content": "// AUTOGENERATED FILE - DO NOT EDIT.\n//\n// This file has been automatically generated by FrankenPHP extension generator\n// and should not be edited as it will be overwritten when running the\n// extension generator again.\n//\n// You may edit the file and remove this comment if you plan to manually maintain\n// this file going forward.\n\n#include <php.h>\n#include <Zend/zend_API.h>\n#include <Zend/zend_hash.h>\n#include <Zend/zend_types.h>\n#include <stddef.h>\n\n#include \"{{.BaseName}}.h\"\n#include \"{{.BaseName}}_arginfo.h\"\n#include \"_cgo_export.h\"\n\n{{- if .Classes}}\n\n#define VALIDATE_GO_HANDLE(intern) \\\n    do { \\\n        if ((intern)->go_handle == 0) { \\\n            zend_throw_error(NULL, \"Go object not found in registry\"); \\\n            RETURN_THROWS(); \\\n        } \\\n    } while (0)\n\nstatic zend_object_handlers object_handlers_{{.BaseName}};\n\ntypedef struct {\n    uintptr_t go_handle;\n    zend_object std; /* This must be the last field in the structure: the property store starts at this offset */\n} {{.BaseName}}_object;\n\nstatic inline {{.BaseName}}_object *{{.BaseName}}_object_from_obj(zend_object *obj) {\n    return ({{.BaseName}}_object*)((char*)(obj) - offsetof({{.BaseName}}_object, std));\n}\n\nstatic zend_object *{{.BaseName}}_create_object(zend_class_entry *ce) {\n    {{.BaseName}}_object *intern = ecalloc(1, sizeof({{.BaseName}}_object) + zend_object_properties_size(ce));\n    \n    zend_object_std_init(&intern->std, ce);\n    object_properties_init(&intern->std, ce);\n    \n    intern->std.handlers = &object_handlers_{{.BaseName}};\n    intern->go_handle = 0; /* will be set in __construct */\n\n    return &intern->std;\n}\n\nstatic void {{.BaseName}}_free_object(zend_object *object) {\n    {{.BaseName}}_object *intern = {{.BaseName}}_object_from_obj(object);\n\n    if (intern->go_handle != 0) {\n        removeGoObject(intern->go_handle);\n    }\n    \n    zend_object_std_dtor(&intern->std);\n}\n\nvoid init_object_handlers() {\n    memcpy(&object_handlers_{{.BaseName}}, &std_object_handlers, sizeof(zend_object_handlers));\n    object_handlers_{{.BaseName}}.free_obj = {{.BaseName}}_free_object;\n    object_handlers_{{.BaseName}}.clone_obj = NULL;\n    object_handlers_{{.BaseName}}.offset = offsetof({{.BaseName}}_object, std);\n}\n{{- end}}\n{{ range .Classes}}\nstatic zend_class_entry *{{.Name}}_ce = NULL;\n\nPHP_METHOD({{namespacedClassName $.Namespace .Name}}, __construct) {\n    ZEND_PARSE_PARAMETERS_NONE();\n\n    {{$.BaseName}}_object *intern = {{$.BaseName}}_object_from_obj(Z_OBJ_P(ZEND_THIS));\n\n    /* Constructor is called more than once, make it no-op */\n    if (intern->go_handle != 0) {\n        return;\n    }\n\n    intern->go_handle = create_{{.GoStruct}}_object();\n}\n\n{{ range .Methods}}\nPHP_METHOD({{namespacedClassName $.Namespace .ClassName}}, {{.PhpName}}) {\n    {{$.BaseName}}_object *intern = {{$.BaseName}}_object_from_obj(Z_OBJ_P(ZEND_THIS));\n    \n    VALIDATE_GO_HANDLE(intern);\n    \n    {{- if .Params -}}\n    {{range $i, $param := .Params -}}\n    {{- if eq $param.PhpType \"string\"}}\n    zend_string *{{$param.Name}} = NULL;{{if $param.IsNullable}}\n    zend_bool {{$param.Name}}_is_null = 0;{{end}}\n    {{- else if eq $param.PhpType \"int\"}}\n    zend_long {{$param.Name}} = {{if $param.HasDefault}}{{$param.DefaultValue}}{{else}}0{{end}};{{if $param.IsNullable}}\n    zend_bool {{$param.Name}}_is_null = 0;{{end}}\n    {{- else if eq $param.PhpType \"float\"}}\n    double {{$param.Name}} = {{if $param.HasDefault}}{{$param.DefaultValue}}{{else}}0.0{{end}};{{if $param.IsNullable}}\n    zend_bool {{$param.Name}}_is_null = 0;{{end}}\n    {{- else if eq $param.PhpType \"bool\"}}\n    zend_bool {{$param.Name}} = {{if $param.HasDefault}}{{if eq $param.DefaultValue \"true\"}}1{{else}}0{{end}}{{else}}0{{end}};{{if $param.IsNullable}}\n    zend_bool {{$param.Name}}_is_null = 0;{{end}}\n    {{- else if eq $param.PhpType \"array\"}}\n    zend_array *{{$param.Name}} = NULL;\n    {{- else if eq $param.PhpType \"callable\"}}\n    zval *{{$param.Name}}_callback;\n    {{- end}}\n    {{- end}}\n    \n    {{$requiredCount := 0}}{{range .Params}}{{if not .HasDefault}}{{$requiredCount = add1 $requiredCount}}{{end}}{{end -}}\n    ZEND_PARSE_PARAMETERS_START({{$requiredCount}}, {{len .Params}})\n        {{$optionalStarted := false}}{{range .Params}}{{if .HasDefault}}{{if not $optionalStarted -}}\n        Z_PARAM_OPTIONAL\n        {{$optionalStarted = true}}{{end}}{{end -}}\n        {{if .IsNullable}}{{if eq .PhpType \"string\"}}Z_PARAM_STR_OR_NULL({{.Name}}, {{.Name}}_is_null){{else if eq .PhpType \"int\"}}Z_PARAM_LONG_OR_NULL({{.Name}}, {{.Name}}_is_null){{else if eq .PhpType \"float\"}}Z_PARAM_DOUBLE_OR_NULL({{.Name}}, {{.Name}}_is_null){{else if eq .PhpType \"bool\"}}Z_PARAM_BOOL_OR_NULL({{.Name}}, {{.Name}}_is_null){{else if eq .PhpType \"array\"}}Z_PARAM_ARRAY_HT_OR_NULL({{.Name}}){{else if eq .PhpType \"callable\"}}Z_PARAM_ZVAL_OR_NULL({{.Name}}_callback){{end}}{{else}}{{if eq .PhpType \"string\"}}Z_PARAM_STR({{.Name}}){{else if eq .PhpType \"int\"}}Z_PARAM_LONG({{.Name}}){{else if eq .PhpType \"float\"}}Z_PARAM_DOUBLE({{.Name}}){{else if eq .PhpType \"bool\"}}Z_PARAM_BOOL({{.Name}}){{else if eq .PhpType \"array\"}}Z_PARAM_ARRAY_HT({{.Name}}){{else if eq .PhpType \"callable\"}}Z_PARAM_ZVAL({{.Name}}_callback){{end}}{{end}}\n        {{end -}}\n    ZEND_PARSE_PARAMETERS_END();\n    {{else}}\n    ZEND_PARSE_PARAMETERS_NONE();\n    {{end}}\n    \n    {{- if ne .ReturnType \"void\"}}\n    {{- if eq .ReturnType \"string\"}}\n    zend_string* result = {{.Name}}_wrapper(intern->go_handle{{if .Params}}{{range .Params}}, {{if .IsNullable}}{{if eq .PhpType \"string\"}}{{.Name}}_is_null ? NULL : {{.Name}}{{else if eq .PhpType \"int\"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType \"float\"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType \"bool\"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType \"array\"}}{{.Name}}{{else if eq .PhpType \"callable\"}}{{.Name}}_callback{{end}}{{else}}{{if eq .PhpType \"array\"}}{{.Name}}{{else if eq .PhpType \"callable\"}}{{.Name}}_callback{{else}}{{.Name}}{{end}}{{end}}{{end}}{{end}});\n    if (result) {\n        RETURN_STR(result);\n    }\n    RETURN_EMPTY_STRING();\n    {{- else if eq .ReturnType \"int\"}}\n    zend_long result = {{.Name}}_wrapper(intern->go_handle{{if .Params}}{{range .Params}}, {{if .IsNullable}}{{if eq .PhpType \"string\"}}{{.Name}}_is_null ? NULL : {{.Name}}{{else if eq .PhpType \"int\"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType \"float\"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType \"bool\"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType \"array\"}}{{.Name}}{{else if eq .PhpType \"callable\"}}{{.Name}}_callback{{end}}{{else}}{{if eq .PhpType \"array\"}}{{.Name}}{{else if eq .PhpType \"callable\"}}{{.Name}}_callback{{else}}(long){{.Name}}{{end}}{{end}}{{end}}{{end}});\n    RETURN_LONG(result);\n    {{- else if eq .ReturnType \"float\"}}\n    double result = {{.Name}}_wrapper(intern->go_handle{{if .Params}}{{range .Params}}, {{if .IsNullable}}{{if eq .PhpType \"string\"}}{{.Name}}_is_null ? NULL : {{.Name}}{{else if eq .PhpType \"int\"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType \"float\"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType \"bool\"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType \"array\"}}{{.Name}}{{else if eq .PhpType \"callable\"}}{{.Name}}_callback{{end}}{{else}}{{if eq .PhpType \"array\"}}{{.Name}}{{else if eq .PhpType \"callable\"}}{{.Name}}_callback{{else}}(double){{.Name}}{{end}}{{end}}{{end}}{{end}});\n    RETURN_DOUBLE(result);\n    {{- else if eq .ReturnType \"bool\"}}\n    int result = {{.Name}}_wrapper(intern->go_handle{{if .Params}}{{range .Params}}, {{if .IsNullable}}{{if eq .PhpType \"string\"}}{{.Name}}_is_null ? NULL : {{.Name}}{{else if eq .PhpType \"int\"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType \"float\"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType \"bool\"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType \"array\"}}{{.Name}}{{else if eq .PhpType \"callable\"}}{{.Name}}_callback{{end}}{{else}}{{if eq .PhpType \"array\"}}{{.Name}}{{else if eq .PhpType \"callable\"}}{{.Name}}_callback{{else}}(int){{.Name}}{{end}}{{end}}{{end}}{{end}});\n    RETURN_BOOL(result);\n    {{- else if eq .ReturnType \"array\"}}\n    void* result = {{.Name}}_wrapper(intern->go_handle{{if .Params}}{{range .Params}}, {{if .IsNullable}}{{if eq .PhpType \"string\"}}{{.Name}}_is_null ? NULL : {{.Name}}{{else if eq .PhpType \"int\"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType \"float\"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType \"bool\"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType \"array\"}}{{.Name}}{{else if eq .PhpType \"callable\"}}{{.Name}}_callback{{end}}{{else}}{{if eq .PhpType \"array\"}}{{.Name}}{{else if eq .PhpType \"callable\"}}{{.Name}}_callback{{else}}{{.Name}}{{end}}{{end}}{{end}}{{end}});\n    if (result != NULL) {\n        HashTable *ht = (HashTable*)result;\n        RETURN_ARR(ht);\n    } else {\n        RETURN_NULL();\n    }\n    {{- end}}\n    {{- else}}\n    {{.Name}}_wrapper(intern->go_handle{{if .Params}}{{range .Params}}, {{if .IsNullable}}{{if eq .PhpType \"string\"}}{{.Name}}_is_null ? NULL : {{.Name}}{{else if eq .PhpType \"int\"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType \"float\"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType \"bool\"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType \"array\"}}{{.Name}}{{else if eq .PhpType \"callable\"}}{{.Name}}_callback{{end}}{{else}}{{if eq .PhpType \"string\"}}{{.Name}}{{else if eq .PhpType \"int\"}}(long){{.Name}}{{else if eq .PhpType \"float\"}}(double){{.Name}}{{else if eq .PhpType \"bool\"}}(int){{.Name}}{{else if eq .PhpType \"array\"}}{{.Name}}{{else if eq .PhpType \"callable\"}}{{.Name}}_callback{{end}}{{end}}{{end}}{{end}});\n    {{- end}}\n}\n{{end}}{{end}}\n\n{{- if .Classes}}\nvoid register_all_classes() {\n    init_object_handlers();\n    \n    {{- range .Classes}}\n    {{.Name}}_ce = register_class_{{namespacedClassName $.Namespace .Name}}();\n    if (!{{.Name}}_ce) {\n        php_error_docref(NULL, E_ERROR, \"Failed to register class {{.Name}}\");\n        return;\n    }\n    {{.Name}}_ce->create_object = {{$.BaseName}}_create_object;\n    {{- end}}\n}\n{{- end}}\n\nPHP_MINIT_FUNCTION({{.BaseName}}) {\n    {{ if .Classes}}register_all_classes();{{end}}\n\n    {{- range .Constants}}\n    {{- if eq .ClassName \"\"}}\n    {{- if $.Namespace}}\n        {{if .IsIota}}REGISTER_NS_LONG_CONSTANT(\"{{cString $.Namespace}}\", \"{{.Name}}\", {{.Name}}, CONST_CS | CONST_PERSISTENT);\n        {{else if eq .PhpType \"string\"}}REGISTER_NS_STRING_CONSTANT(\"{{cString $.Namespace}}\", \"{{.Name}}\", {{.CValue}}, CONST_CS | CONST_PERSISTENT);\n        {{else if eq .PhpType \"bool\"}}REGISTER_NS_BOOL_CONSTANT(\"{{cString $.Namespace}}\", \"{{.Name}}\", {{if eq .Value \"true\"}}true{{else}}false{{end}}, CONST_CS | CONST_PERSISTENT);\n        {{else if eq .PhpType \"float\"}}REGISTER_NS_DOUBLE_CONSTANT(\"{{cString $.Namespace}}\", \"{{.Name}}\", {{.CValue}}, CONST_CS | CONST_PERSISTENT);\n        {{else}}REGISTER_NS_LONG_CONSTANT(\"{{cString $.Namespace}}\", \"{{.Name}}\", {{.CValue}}, CONST_CS | CONST_PERSISTENT);\n        {{- end}}\n    {{- else}}\n    {{if .IsIota}}REGISTER_LONG_CONSTANT(\"{{.Name}}\", {{.Name}}, CONST_CS | CONST_PERSISTENT);\n    {{else if eq .PhpType \"string\"}}REGISTER_STRING_CONSTANT(\"{{.Name}}\", {{.CValue}}, CONST_CS | CONST_PERSISTENT);\n    {{else if eq .PhpType \"bool\"}}REGISTER_BOOL_CONSTANT(\"{{.Name}}\", {{if eq .Value \"true\"}}true{{else}}false{{end}}, CONST_CS | CONST_PERSISTENT);\n    {{else if eq .PhpType \"float\"}}REGISTER_DOUBLE_CONSTANT(\"{{.Name}}\", {{.CValue}}, CONST_CS | CONST_PERSISTENT);\n    {{else}}REGISTER_LONG_CONSTANT(\"{{.Name}}\", {{.CValue}}, CONST_CS | CONST_PERSISTENT);\n    {{- end}}\n    {{- end}}\n    {{- end}}\n    {{- end}}\n    return SUCCESS;\n}\n\nzend_module_entry {{.BaseName}}_module_entry = {STANDARD_MODULE_HEADER,\n                                         \"{{.BaseName}}\",\n                                         {{if .Functions}}ext_functions{{else}}NULL{{end}},             /* Functions */\n                                         PHP_MINIT({{.BaseName}}),  /* MINIT */\n                                         NULL,                      /* MSHUTDOWN */\n                                         NULL,                      /* RINIT */\n                                         NULL,                      /* RSHUTDOWN */\n                                         NULL,                      /* MINFO */\n                                         \"1.0.0\",                   /* Version */\n                                         STANDARD_MODULE_PROPERTIES};\n"
  },
  {
    "path": "internal/extgen/templates/extension.go.tpl",
    "content": "package {{.PackageName}}\n\n// AUTOGENERATED FILE - DO NOT EDIT.\n//\n// This file has been automatically generated by FrankenPHP extension generator\n// and should not be edited as it will be overwritten when running the\n// extension generator again.\n//\n// You may edit the file and remove this comment if you plan to manually maintain\n// this file going forward.\n\n// #include <stdlib.h>\n// #include \"{{.BaseName}}.h\"\nimport \"C\"\nimport (\n\t{{if not .Classes}}_ {{end}}\"runtime/cgo\"\n\t\"unsafe\"\n\n\t\"github.com/dunglas/frankenphp\"\n)\n\nfunc init() {\n\tfrankenphp.RegisterExtension(unsafe.Pointer(&C.{{.SanitizedBaseName}}_module_entry))\n}\n\n{{- range .Functions}}\n//export go_{{.Name}}\nfunc go_{{.Name}}({{extractGoFunctionSignatureParams .GoFunction}}) {{extractGoFunctionSignatureReturn .GoFunction}} {\n\t{{if not (isVoid .ReturnType)}}return {{end}}{{extractGoFunctionName .GoFunction}}({{extractGoFunctionCallParams .GoFunction}})\n}\n\n{{- end}}\n{{- if .Classes}}\n//export registerGoObject\nfunc registerGoObject(obj interface{}) C.uintptr_t {\n\thandle := cgo.NewHandle(obj)\n\treturn C.uintptr_t(handle)\n}\n\n//export getGoObject\nfunc getGoObject(handle C.uintptr_t) interface{} {\n\th := cgo.Handle(handle)\n\treturn h.Value()\n}\n\n//export removeGoObject\nfunc removeGoObject(handle C.uintptr_t) {\n\th := cgo.Handle(handle)\n\th.Delete()\n}\n\n{{- end}}\n{{- range $class := .Classes}}\n//export create_{{.GoStruct}}_object\nfunc create_{{.GoStruct}}_object() C.uintptr_t {\n\tobj := &{{.GoStruct}}{}\n\treturn registerGoObject(obj)\n}\n\n{{- range .Methods}}\n//export {{.Name}}_wrapper\nfunc {{.Name}}_wrapper(handle C.uintptr_t{{range .Params}}{{if eq .PhpType \"string\"}}, {{.Name}} *C.zend_string{{else if eq .PhpType \"array\"}}, {{.Name}} *C.zval{{else if eq .PhpType \"callable\"}}, {{.Name}} *C.zval{{else}}, {{.Name}} {{if .IsNullable}}*{{end}}{{phpTypeToGoType .PhpType}}{{end}}{{end}}){{if not (isVoid .ReturnType)}}{{if isStringOrArray .ReturnType}} unsafe.Pointer{{else}} {{phpTypeToGoType .ReturnType}}{{end}}{{end}} {\n\tobj := getGoObject(handle)\n\tif obj == nil {\n{{- if not (isVoid .ReturnType)}}\n{{- if isStringOrArray .ReturnType}}\n\t\treturn nil\n{{- else}}\n\t\tvar zero {{phpTypeToGoType .ReturnType}}\n\t\treturn zero\n{{- end}}\n{{- else}}\n\t\treturn\n{{- end}}\n\t}\n\tstructObj := obj.(*{{$class.GoStruct}})\n\t{{if not (isVoid .ReturnType)}}return {{end}}structObj.{{.Name | title}}({{range $i, $param := .Params}}{{if $i}}, {{end}}{{$param.Name}}{{end}})\n}\n{{end}}\n{{- end}}\n"
  },
  {
    "path": "internal/extgen/templates/extension.h.tpl",
    "content": "// AUTOGENERATED FILE - DO NOT EDIT.\n//\n// This file has been automatically generated by FrankenPHP extension generator\n// and should not be edited as it will be overwritten when running the\n// extension generator again.\n//\n// You may edit the file and remove this comment if you plan to manually maintain\n// this file going forward.\n\n#ifndef _{{.HeaderGuard}}\n#define _{{.HeaderGuard}}\n\n#include <php.h>\n#include <stdint.h>\n\nextern zend_module_entry {{.BaseName}}_module_entry;\n\n{{if .Constants}}\n/* User defined constants */{{end}}\n{{range .Constants}}#define {{.Name}} {{.CValue}}\n{{end}}\n#endif\n"
  },
  {
    "path": "internal/extgen/templates/stub.php.tpl",
    "content": "<?php\n\n/** @generate-class-entries */\n\n// AUTOGENERATED FILE - DO NOT EDIT.\n//\n// This file has been automatically generated by FrankenPHP extension generator\n// and should not be edited as it will be overwritten when running the\n// extension generator again.\n//\n// You may edit the file and remove this comment if you plan to manually maintain\n// this file going forward.\n{{if .Namespace}}\nnamespace {{.Namespace}};\n{{end}}\n{{range .Constants}}{{if eq .ClassName \"\"}}{{if .IsIota}}/**\n * @var int\n * @cvalue {{.Name}}\n */\nconst {{.Name}} = UNKNOWN;\n\n{{else}}/**\n * @var {{phpType .PhpType}}\n */\nconst {{.Name}} = {{.Value}};\n\n{{end}}{{end}}{{end}}{{range .Functions}}function {{.Signature}} {}\n\n{{end}}{{range .Classes}}{{$className := .Name}}class {{.Name}} {\n{{range $.Constants}}{{if eq .ClassName $className}}{{if .IsIota}}    /**\n     * @var int\n     * @cvalue {{.Name}}\n     */\n    public const {{.Name}} = UNKNOWN;\n\n{{else}}    /**\n     * @var {{phpType .PhpType}}\n     */\n    public const {{.Name}} = {{.Value}};\n\n{{end}}{{end}}{{end}}\n    public function __construct() {}\n{{range .Methods}}\n    public function {{.Signature}} {}\n{{end}}\n}\n\n{{end}}\n"
  },
  {
    "path": "internal/extgen/utils.go",
    "content": "package extgen\n\nimport (\n\t\"os\"\n\t\"strings\"\n\t\"unicode\"\n)\n\nfunc writeFile(filename, content string) error {\n\treturn os.WriteFile(filename, []byte(content), 0644)\n}\n\nfunc readFile(filename string) (string, error) {\n\tcontent, err := os.ReadFile(filename)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn string(content), nil\n}\n\n// NamespacedName converts a namespace and name to a C-compatible format.\n// E.g., namespace \"Go\\Extension\" and name \"MyClass\" become \"Go_Extension_MyClass\".\n// This symbol remains exported, so it's usable in templates.\nfunc NamespacedName(namespace, name string) string {\n\tif namespace == \"\" {\n\t\treturn name\n\t}\n\tnamespacePart := strings.ReplaceAll(namespace, `\\`, \"_\")\n\treturn namespacePart + \"_\" + name\n}\n\n// EXPERIMENTAL\nfunc SanitizePackageName(name string) string {\n\tsanitized := strings.ReplaceAll(name, \"-\", \"_\")\n\tsanitized = strings.ReplaceAll(sanitized, \".\", \"_\")\n\n\tif len(sanitized) > 0 && !unicode.IsLetter(rune(sanitized[0])) && sanitized[0] != '_' {\n\t\tsanitized = \"_\" + sanitized\n\t}\n\n\treturn sanitized\n}\n"
  },
  {
    "path": "internal/extgen/utils_namespace_test.go",
    "content": "package extgen\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestNamespacedName(t *testing.T) {\n\ttests := []struct {\n\t\tname      string\n\t\tnamespace string\n\t\titemName  string\n\t\texpected  string\n\t}{\n\t\t{\n\t\t\tname:      \"no namespace\",\n\t\t\tnamespace: \"\",\n\t\t\titemName:  \"TestItem\",\n\t\t\texpected:  \"TestItem\",\n\t\t},\n\t\t{\n\t\t\tname:      \"single level namespace\",\n\t\t\tnamespace: \"MyNamespace\",\n\t\t\titemName:  \"TestItem\",\n\t\t\texpected:  \"MyNamespace_TestItem\",\n\t\t},\n\t\t{\n\t\t\tname:      \"multi level namespace\",\n\t\t\tnamespace: `Go\\Extension`,\n\t\t\titemName:  \"TestItem\",\n\t\t\texpected:  \"Go_Extension_TestItem\",\n\t\t},\n\t\t{\n\t\t\tname:      \"deep namespace\",\n\t\t\tnamespace: `Very\\Deep\\Nested\\Namespace`,\n\t\t\titemName:  \"MyItem\",\n\t\t\texpected:  \"Very_Deep_Nested_Namespace_MyItem\",\n\t\t},\n\t\t{\n\t\t\tname:      \"function name\",\n\t\t\tnamespace: `Go\\Extension`,\n\t\t\titemName:  \"multiply\",\n\t\t\texpected:  \"Go_Extension_multiply\",\n\t\t},\n\t\t{\n\t\t\tname:      \"class name\",\n\t\t\tnamespace: `Go\\Extension`,\n\t\t\titemName:  \"MySuperClass\",\n\t\t\texpected:  \"Go_Extension_MySuperClass\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := NamespacedName(tt.namespace, tt.itemName)\n\t\t\trequire.Equal(t, tt.expected, result, \"NamespacedName(%q, %q) = %q, expected %q\", tt.namespace, tt.itemName, result, tt.expected)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/extgen/utils_test.go",
    "content": "package extgen\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestWriteFile(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\tfilename    string\n\t\tcontent     string\n\t\texpectError bool\n\t}{\n\t\t{\n\t\t\tname:        \"write simple file\",\n\t\t\tfilename:    \"test.txt\",\n\t\t\tcontent:     \"hello world\",\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname:        \"write empty file\",\n\t\t\tfilename:    \"empty.txt\",\n\t\t\tcontent:     \"\",\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname:        \"write file with special characters\",\n\t\t\tfilename:    \"special.txt\",\n\t\t\tcontent:     \"hello\\nworld\\t!@#$%^&*()\",\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname:        \"write to invalid directory\",\n\t\t\tfilename:    \"/nonexistent/directory/file.txt\",\n\t\t\tcontent:     \"test\",\n\t\t\texpectError: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvar filename string\n\t\t\tif !tt.expectError {\n\t\t\t\ttempDir := t.TempDir()\n\t\t\t\tfilename = filepath.Join(tempDir, tt.filename)\n\t\t\t} else {\n\t\t\t\tfilename = tt.filename\n\t\t\t}\n\n\t\t\terr := writeFile(filename, tt.content)\n\n\t\t\tif tt.expectError {\n\t\t\t\tassert.Error(t, err, \"writeFile() should return an error\")\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.NoError(t, err, \"writeFile() should not return an error\")\n\n\t\t\tcontent, err := os.ReadFile(filename)\n\t\t\tassert.NoError(t, err, \"Failed to read written file\")\n\t\t\tassert.Equal(t, tt.content, string(content), \"writeFile() content mismatch\")\n\n\t\t\tinfo, err := os.Stat(filename)\n\t\t\tassert.NoError(t, err, \"Failed to stat file\")\n\n\t\t\texpectedMode := os.FileMode(0644)\n\t\t\tif runtime.GOOS == \"windows\" {\n\t\t\t\texpectedMode = os.FileMode(0666)\n\t\t\t}\n\n\t\t\tassert.Equal(t, expectedMode, info.Mode().Perm(), \"writeFile() wrong permissions\")\n\t\t})\n\t}\n}\n\nfunc TestReadFile(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\tcontent     string\n\t\texpectError bool\n\t}{\n\t\t{\n\t\t\tname:        \"read simple file\",\n\t\t\tcontent:     \"hello world\",\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname:        \"read empty file\",\n\t\t\tcontent:     \"\",\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname:        \"read file with special characters\",\n\t\t\tcontent:     \"hello\\nworld\\t!@#$%^&*()\",\n\t\t\texpectError: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ttempDir := t.TempDir()\n\t\t\tfilename := filepath.Join(tempDir, \"test.txt\")\n\n\t\t\terr := os.WriteFile(filename, []byte(tt.content), 0644)\n\t\t\tassert.NoError(t, err, \"Failed to create test file\")\n\n\t\t\tcontent, err := readFile(filename)\n\n\t\t\tif tt.expectError {\n\t\t\t\tassert.Error(t, err, \"readFile() should return an error\")\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.NoError(t, err, \"readFile() should not return an error\")\n\t\t\tassert.Equal(t, tt.content, content, \"readFile() content mismatch\")\n\t\t})\n\t}\n\n\tt.Run(\"read nonexistent file\", func(t *testing.T) {\n\t\t_, err := readFile(\"/nonexistent/file.txt\")\n\t\tassert.Error(t, err, \"readFile() should return an error for nonexistent file\")\n\t})\n}\n\nfunc TestSanitizePackageName(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"simple valid name\",\n\t\t\tinput:    \"mypackage\",\n\t\t\texpected: \"mypackage\",\n\t\t},\n\t\t{\n\t\t\tname:     \"name with hyphens\",\n\t\t\tinput:    \"my-package\",\n\t\t\texpected: \"my_package\",\n\t\t},\n\t\t{\n\t\t\tname:     \"name with dots\",\n\t\t\tinput:    \"my.package\",\n\t\t\texpected: \"my_package\",\n\t\t},\n\t\t{\n\t\t\tname:     \"name with both hyphens and dots\",\n\t\t\tinput:    \"my-package.name\",\n\t\t\texpected: \"my_package_name\",\n\t\t},\n\t\t{\n\t\t\tname:     \"name starting with number\",\n\t\t\tinput:    \"123package\",\n\t\t\texpected: \"_123package\",\n\t\t},\n\t\t{\n\t\t\tname:     \"name starting with underscore\",\n\t\t\tinput:    \"_package\",\n\t\t\texpected: \"_package\",\n\t\t},\n\t\t{\n\t\t\tname:     \"name starting with letter\",\n\t\t\tinput:    \"Package\",\n\t\t\texpected: \"Package\",\n\t\t},\n\t\t{\n\t\t\tname:     \"name starting with special character\",\n\t\t\tinput:    \"@package\",\n\t\t\texpected: \"_@package\",\n\t\t},\n\t\t{\n\t\t\tname:     \"complex name\",\n\t\t\tinput:    \"123my-complex.package@name\",\n\t\t\texpected: \"_123my_complex_package@name\",\n\t\t},\n\t\t{\n\t\t\tname:     \"empty string\",\n\t\t\tinput:    \"\",\n\t\t\texpected: \"\",\n\t\t},\n\t\t{\n\t\t\tname:     \"single character letter\",\n\t\t\tinput:    \"a\",\n\t\t\texpected: \"a\",\n\t\t},\n\t\t{\n\t\t\tname:     \"single character number\",\n\t\t\tinput:    \"1\",\n\t\t\texpected: \"_1\",\n\t\t},\n\t\t{\n\t\t\tname:     \"single character underscore\",\n\t\t\tinput:    \"_\",\n\t\t\texpected: \"_\",\n\t\t},\n\t\t{\n\t\t\tname:     \"single character special\",\n\t\t\tinput:    \"@\",\n\t\t\texpected: \"_@\",\n\t\t},\n\t\t{\n\t\t\tname:     \"multiple consecutive hyphens\",\n\t\t\tinput:    \"my--package\",\n\t\t\texpected: \"my__package\",\n\t\t},\n\t\t{\n\t\t\tname:     \"multiple consecutive dots\",\n\t\t\tinput:    \"my..package\",\n\t\t\texpected: \"my__package\",\n\t\t},\n\t\t{\n\t\t\tname:     \"mixed case with special chars\",\n\t\t\tinput:    \"MyPackage-name.version\",\n\t\t\texpected: \"MyPackage_name_version\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := SanitizePackageName(tt.input)\n\t\t\tassert.Equal(t, tt.expected, result, \"SanitizePackageName(%q)\", tt.input)\n\t\t})\n\t}\n}\n\nfunc BenchmarkSanitizePackageName(b *testing.B) {\n\ttestCases := []string{\n\t\t\"simple\",\n\t\t\"my-package\",\n\t\t\"my.package.name\",\n\t\t\"123complex-package.name@version\",\n\t\t\"very-long-package-name-with-many-special-characters.and.dots\",\n\t}\n\n\tfor _, tc := range testCases {\n\t\tb.Run(tc, func(b *testing.B) {\n\t\t\tfor b.Loop() {\n\t\t\t\tSanitizePackageName(tc)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/extgen/validator.go",
    "content": "package extgen\n\nimport (\n\t\"fmt\"\n\t\"go/ast\"\n\t\"go/parser\"\n\t\"go/token\"\n\t\"regexp\"\n\t\"slices\"\n\t\"strings\"\n)\n\nvar (\n\tparamTypes     = []phpType{phpString, phpInt, phpFloat, phpBool, phpArray, phpObject, phpMixed, phpCallable}\n\treturnTypes    = []phpType{phpVoid, phpString, phpInt, phpFloat, phpBool, phpArray, phpObject, phpMixed, phpNull, phpTrue, phpFalse}\n\tpropTypes      = []phpType{phpString, phpInt, phpFloat, phpBool, phpArray, phpObject, phpMixed}\n\tsupportedTypes = []phpType{phpString, phpInt, phpFloat, phpBool, phpArray, phpMixed, phpCallable}\n\n\tfunctionNameRegex  = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`)\n\tparameterNameRegex = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`)\n\tclassNameRegex     = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`)\n\tpropNameRegex      = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`)\n)\n\ntype Validator struct{}\n\nfunc (v *Validator) validateFunction(fn phpFunction) error {\n\tif fn.Name == \"\" {\n\t\treturn fmt.Errorf(\"function name cannot be empty\")\n\t}\n\n\tif !functionNameRegex.MatchString(fn.Name) {\n\t\treturn fmt.Errorf(\"invalid function name: %s\", fn.Name)\n\t}\n\n\tfor i, param := range fn.Params {\n\t\tif err := v.validateParameter(param); err != nil {\n\t\t\treturn fmt.Errorf(\"parameter %d (%s): %w\", i, param.Name, err)\n\t\t}\n\t}\n\n\tif err := v.validateReturnType(fn.ReturnType); err != nil {\n\t\treturn fmt.Errorf(\"return type: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (v *Validator) validateParameter(param phpParameter) error {\n\tif param.Name == \"\" {\n\t\treturn fmt.Errorf(\"parameter name cannot be empty\")\n\t}\n\n\tif !parameterNameRegex.MatchString(param.Name) {\n\t\treturn fmt.Errorf(\"invalid parameter name: %s\", param.Name)\n\t}\n\n\tif !slices.Contains(paramTypes, param.PhpType) {\n\t\treturn fmt.Errorf(\"invalid parameter type: %s\", param.PhpType)\n\t}\n\n\treturn nil\n}\n\nfunc (v *Validator) validateReturnType(returnType phpType) error {\n\tif !slices.Contains(returnTypes, returnType) {\n\t\treturn fmt.Errorf(\"invalid return type: %s\", returnType)\n\t}\n\treturn nil\n}\n\nfunc (v *Validator) validateClass(class phpClass) error {\n\tif class.Name == \"\" {\n\t\treturn fmt.Errorf(\"class name cannot be empty\")\n\t}\n\n\tif !classNameRegex.MatchString(class.Name) {\n\t\treturn fmt.Errorf(\"invalid class name: %s\", class.Name)\n\t}\n\n\tfor i, prop := range class.Properties {\n\t\tif err := v.validateClassProperty(prop); err != nil {\n\t\t\treturn fmt.Errorf(\"property %d (%s): %w\", i, prop.Name, err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (v *Validator) validateClassProperty(prop phpClassProperty) error {\n\tif prop.Name == \"\" {\n\t\treturn fmt.Errorf(\"property name cannot be empty\")\n\t}\n\n\tif !propNameRegex.MatchString(prop.Name) {\n\t\treturn fmt.Errorf(\"invalid property name: %s\", prop.Name)\n\t}\n\n\tif !slices.Contains(propTypes, prop.PhpType) {\n\t\treturn fmt.Errorf(\"invalid property type: %s\", prop.PhpType)\n\t}\n\n\treturn nil\n}\n\n// validateTypes checks if PHP signature contains only supported types\nfunc (v *Validator) validateTypes(fn phpFunction) error {\n\tfor i, param := range fn.Params {\n\t\tif !slices.Contains(supportedTypes, param.PhpType) {\n\t\t\treturn fmt.Errorf(\"parameter %d %q has unsupported type %q, supported typed: string, int, float, bool, array and mixed, can be nullable\", i+1, param.Name, param.PhpType)\n\t\t}\n\t}\n\n\tif fn.ReturnType != phpVoid && !slices.Contains(supportedTypes, fn.ReturnType) {\n\t\treturn fmt.Errorf(\"return type %q is not supported, supported typed: string, int, float, bool, array and mixed, can be nullable\", fn.ReturnType)\n\t}\n\n\treturn nil\n}\n\n// validateGoFunctionSignatureWithOptions validates with option for method vs function\nfunc (v *Validator) validateGoFunctionSignatureWithOptions(phpFunc phpFunction, isMethod bool) error {\n\tif phpFunc.GoFunction == \"\" {\n\t\treturn fmt.Errorf(\"no Go function found for PHP function %q\", phpFunc.Name)\n\t}\n\n\tfset := token.NewFileSet()\n\tfile, err := parser.ParseFile(fset, \"\", \"package main\\n\"+phpFunc.GoFunction, 0)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to parse Go function: %w\", err)\n\t}\n\n\tvar goFunc *ast.FuncDecl\n\tfor _, decl := range file.Decls {\n\t\tif funcDecl, ok := decl.(*ast.FuncDecl); ok {\n\t\t\tgoFunc = funcDecl\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif goFunc == nil {\n\t\treturn fmt.Errorf(\"no function declaration found in Go function\")\n\t}\n\n\tgoParamCount := 0\n\tif goFunc.Type.Params != nil {\n\t\tgoParamCount = len(goFunc.Type.Params.List)\n\t}\n\n\thasReceiver := goFunc.Recv != nil && len(goFunc.Recv.List) > 0\n\tparamOffset := 0\n\teffectiveGoParamCount := goParamCount\n\n\tif hasReceiver {\n\t\tparamOffset = 0\n\t\teffectiveGoParamCount = goParamCount\n\t} else if isMethod && goParamCount > 0 {\n\t\t// this is a method-like function, first parameter should be the struct\n\t\tparamOffset = 1\n\t\teffectiveGoParamCount = goParamCount - 1\n\t}\n\n\texpectedGoParams := len(phpFunc.Params)\n\n\tif expectedGoParams != effectiveGoParamCount {\n\t\treturn fmt.Errorf(\"parameter count mismatch: PHP function has %d parameters (expecting %d Go parameters) but Go function has %d\", len(phpFunc.Params), expectedGoParams, effectiveGoParamCount)\n\t}\n\n\tif goFunc.Type.Params != nil && len(phpFunc.Params) > 0 {\n\t\tfor i, phpParam := range phpFunc.Params {\n\t\t\tgoParamIndex := i + paramOffset\n\n\t\t\tif goParamIndex >= len(goFunc.Type.Params.List) {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tgoParam := goFunc.Type.Params.List[goParamIndex]\n\t\t\texpectedGoType := v.phpTypeToGoType(phpParam.PhpType, phpParam.IsNullable)\n\t\t\tactualGoType := v.goTypeToString(goParam.Type)\n\n\t\t\tif !v.isCompatibleGoType(expectedGoType, actualGoType) {\n\t\t\t\treturn fmt.Errorf(\"parameter %d type mismatch: PHP %q requires Go type %q but found %q\", i+1, phpParam.PhpType, expectedGoType, actualGoType)\n\t\t\t}\n\t\t}\n\t}\n\n\texpectedGoReturnType := v.phpReturnTypeToGoType(phpFunc.ReturnType)\n\tactualGoReturnType := v.goReturnTypeToString(goFunc.Type.Results)\n\n\tif !v.isCompatibleGoType(expectedGoReturnType, actualGoReturnType) {\n\t\treturn fmt.Errorf(\"return type mismatch: PHP %q requires Go return type %q but found %q\", phpFunc.ReturnType, expectedGoReturnType, actualGoReturnType)\n\t}\n\n\treturn nil\n}\n\nfunc (v *Validator) phpTypeToGoType(t phpType, isNullable bool) string {\n\tvar baseType string\n\tswitch t {\n\tcase phpString:\n\t\tbaseType = \"*C.zend_string\"\n\tcase phpInt:\n\t\tbaseType = \"int64\"\n\tcase phpFloat:\n\t\tbaseType = \"float64\"\n\tcase phpBool:\n\t\tbaseType = \"bool\"\n\tcase phpArray:\n\t\tbaseType = \"*C.zend_array\"\n\tcase phpMixed:\n\t\tbaseType = \"*C.zval\"\n\tcase phpCallable:\n\t\tbaseType = \"*C.zval\"\n\tdefault:\n\t\tbaseType = \"any\"\n\t}\n\n\tif isNullable && t != phpString && t != phpArray && t != phpCallable {\n\t\treturn \"*\" + baseType\n\t}\n\n\treturn baseType\n}\n\n// isCompatibleGoType checks if the actual Go type is compatible with the expected type.\nfunc (v *Validator) isCompatibleGoType(expectedType, actualType string) bool {\n\tif expectedType == actualType {\n\t\treturn true\n\t}\n\n\tswitch expectedType {\n\tcase \"int64\":\n\t\treturn actualType == \"int\"\n\tcase \"*int64\":\n\t\treturn actualType == \"*int\"\n\tcase \"*float64\":\n\t\treturn actualType == \"*float32\"\n\t}\n\n\treturn false\n}\n\nfunc (v *Validator) phpReturnTypeToGoType(phpReturnType phpType) string {\n\tswitch phpReturnType {\n\tcase phpVoid:\n\t\treturn \"\"\n\tcase phpString:\n\t\treturn \"unsafe.Pointer\"\n\tcase phpInt:\n\t\treturn \"int64\"\n\tcase phpFloat:\n\t\treturn \"float64\"\n\tcase phpBool:\n\t\treturn \"bool\"\n\tcase phpArray:\n\t\treturn \"unsafe.Pointer\"\n\tdefault:\n\t\treturn \"any\"\n\t}\n}\n\nfunc (v *Validator) goTypeToString(expr ast.Expr) string {\n\tswitch t := expr.(type) {\n\tcase *ast.Ident:\n\t\treturn t.Name\n\tcase *ast.StarExpr:\n\t\treturn \"*\" + v.goTypeToString(t.X)\n\tcase *ast.SelectorExpr:\n\t\treturn v.goTypeToString(t.X) + \".\" + t.Sel.Name\n\tdefault:\n\t\treturn \"unknown\"\n\t}\n}\n\nfunc (v *Validator) goReturnTypeToString(results *ast.FieldList) string {\n\tif results == nil || len(results.List) == 0 {\n\t\treturn \"\"\n\t}\n\n\tif len(results.List) == 1 {\n\t\treturn v.goTypeToString(results.List[0].Type)\n\t}\n\n\tvar types []string\n\tfor _, field := range results.List {\n\t\ttypes = append(types, v.goTypeToString(field.Type))\n\t}\n\treturn \"(\" + strings.Join(types, \", \") + \")\"\n}\n"
  },
  {
    "path": "internal/extgen/validator_test.go",
    "content": "package extgen\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestValidateFunction(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\tfunction    phpFunction\n\t\texpectError bool\n\t}{\n\t\t{\n\t\t\tname: \"valid function\",\n\t\t\tfunction: phpFunction{\n\t\t\t\tName:       \"validFunction\",\n\t\t\t\tReturnType: phpString,\n\t\t\t\tParams: []phpParameter{\n\t\t\t\t\t{Name: \"param1\", PhpType: phpString},\n\t\t\t\t\t{Name: \"param2\", PhpType: phpInt},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname: \"valid function with nullable return\",\n\t\t\tfunction: phpFunction{\n\t\t\t\tName:             \"nullableReturn\",\n\t\t\t\tReturnType:       phpString,\n\t\t\t\tIsReturnNullable: true,\n\t\t\t\tParams: []phpParameter{\n\t\t\t\t\t{Name: \"data\", PhpType: phpArray},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname: \"valid function with array parameter\",\n\t\t\tfunction: phpFunction{\n\t\t\t\tName:       \"arrayFunction\",\n\t\t\t\tReturnType: phpArray,\n\t\t\t\tParams: []phpParameter{\n\t\t\t\t\t{Name: \"items\", PhpType: phpArray},\n\t\t\t\t\t{Name: \"filter\", PhpType: phpString},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname: \"valid function with nullable array parameter\",\n\t\t\tfunction: phpFunction{\n\t\t\t\tName:       \"nullableArrayFunction\",\n\t\t\t\tReturnType: phpString,\n\t\t\t\tParams: []phpParameter{\n\t\t\t\t\t{Name: \"items\", PhpType: phpArray, IsNullable: true},\n\t\t\t\t\t{Name: \"name\", PhpType: phpString},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname: \"valid function with array parameter\",\n\t\t\tfunction: phpFunction{\n\t\t\t\tName:       \"arrayFunction\",\n\t\t\t\tReturnType: \"array\",\n\t\t\t\tParams: []phpParameter{\n\t\t\t\t\t{Name: \"items\", PhpType: phpArray},\n\t\t\t\t\t{Name: \"filter\", PhpType: phpString},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname: \"valid function with nullable array parameter\",\n\t\t\tfunction: phpFunction{\n\t\t\t\tName:       \"nullableArrayFunction\",\n\t\t\t\tReturnType: \"string\",\n\t\t\t\tParams: []phpParameter{\n\t\t\t\t\t{Name: \"items\", PhpType: phpArray, IsNullable: true},\n\t\t\t\t\t{Name: \"name\", PhpType: phpString},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname: \"valid function with callable parameter\",\n\t\t\tfunction: phpFunction{\n\t\t\t\tName:       \"callableFunction\",\n\t\t\t\tReturnType: \"array\",\n\t\t\t\tParams: []phpParameter{\n\t\t\t\t\t{Name: \"data\", PhpType: phpArray},\n\t\t\t\t\t{Name: \"callback\", PhpType: phpCallable},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname: \"valid function with nullable callable parameter\",\n\t\t\tfunction: phpFunction{\n\t\t\t\tName:       \"nullableCallableFunction\",\n\t\t\t\tReturnType: \"string\",\n\t\t\t\tParams: []phpParameter{\n\t\t\t\t\t{Name: \"callback\", PhpType: phpCallable, IsNullable: true},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname: \"empty function name\",\n\t\t\tfunction: phpFunction{\n\t\t\t\tName:       \"\",\n\t\t\t\tReturnType: phpString,\n\t\t\t},\n\t\t\texpectError: true,\n\t\t},\n\t\t{\n\t\t\tname: \"invalid function name - starts with number\",\n\t\t\tfunction: phpFunction{\n\t\t\t\tName:       \"123invalid\",\n\t\t\t\tReturnType: phpString,\n\t\t\t},\n\t\t\texpectError: true,\n\t\t},\n\t\t{\n\t\t\tname: \"invalid function name - contains special chars\",\n\t\t\tfunction: phpFunction{\n\t\t\t\tName:       \"invalid-name\",\n\t\t\t\tReturnType: phpString,\n\t\t\t},\n\t\t\texpectError: true,\n\t\t},\n\t\t{\n\t\t\tname: \"invalid parameter name\",\n\t\t\tfunction: phpFunction{\n\t\t\t\tName:       \"validName\",\n\t\t\t\tReturnType: phpString,\n\t\t\t\tParams: []phpParameter{\n\t\t\t\t\t{Name: \"123invalid\", PhpType: phpString},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectError: true,\n\t\t},\n\t\t{\n\t\t\tname: \"empty parameter name\",\n\t\t\tfunction: phpFunction{\n\t\t\t\tName:       \"validName\",\n\t\t\t\tReturnType: phpString,\n\t\t\t\tParams: []phpParameter{\n\t\t\t\t\t{Name: \"\", PhpType: phpString},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectError: true,\n\t\t},\n\t}\n\n\tvalidator := Validator{}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := validator.validateFunction(tt.function)\n\n\t\t\tif tt.expectError {\n\t\t\t\tassert.Error(t, err, \"validateFunction() should return an error for function %s\", tt.function.Name)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err, \"validateFunction() should not return an error for function %s\", tt.function.Name)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestValidateReturnType(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\treturnType  string\n\t\texpectError bool\n\t}{\n\t\t{\n\t\t\tname:        \"valid string type\",\n\t\t\treturnType:  \"string\",\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname:        \"valid int type\",\n\t\t\treturnType:  \"int\",\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname:        \"valid array type\",\n\t\t\treturnType:  \"array\",\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname:        \"valid bool type\",\n\t\t\treturnType:  \"bool\",\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname:        \"valid float type\",\n\t\t\treturnType:  \"float\",\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname:        \"valid void type\",\n\t\t\treturnType:  \"void\",\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname:        \"invalid return type\",\n\t\t\treturnType:  \"invalidType\",\n\t\t\texpectError: true,\n\t\t},\n\t\t{\n\t\t\tname:        \"empty return type\",\n\t\t\treturnType:  \"\",\n\t\t\texpectError: true,\n\t\t},\n\t\t{\n\t\t\tname:        \"case sensitive - String should be invalid\",\n\t\t\treturnType:  \"String\",\n\t\t\texpectError: true,\n\t\t},\n\t}\n\n\tvalidator := Validator{}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := validator.validateReturnType(phpType(tt.returnType))\n\n\t\t\tif tt.expectError {\n\t\t\t\tassert.Error(t, err, \"validateReturnType(%s) should return an error\", tt.returnType)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err, \"validateReturnType(%s) should not return an error\", tt.returnType)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestValidateClassProperty(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\tprop        phpClassProperty\n\t\texpectError bool\n\t}{\n\t\t{\n\t\t\tname: \"valid property\",\n\t\t\tprop: phpClassProperty{\n\t\t\t\tName:    \"validProperty\",\n\t\t\t\tPhpType: phpString,\n\t\t\t\tGoType:  \"string\",\n\t\t\t},\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname: \"valid nullable property\",\n\t\t\tprop: phpClassProperty{\n\t\t\t\tName:       \"nullableProperty\",\n\t\t\t\tPhpType:    phpInt,\n\t\t\t\tGoType:     \"*int\",\n\t\t\t\tIsNullable: true,\n\t\t\t},\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname: \"empty property name\",\n\t\t\tprop: phpClassProperty{\n\t\t\t\tName:    \"\",\n\t\t\t\tPhpType: phpString,\n\t\t\t},\n\t\t\texpectError: true,\n\t\t},\n\t\t{\n\t\t\tname: \"invalid property name\",\n\t\t\tprop: phpClassProperty{\n\t\t\t\tName:    \"123invalid\",\n\t\t\t\tPhpType: phpString,\n\t\t\t},\n\t\t\texpectError: true,\n\t\t},\n\t\t{\n\t\t\tname: \"invalid property type\",\n\t\t\tprop: phpClassProperty{\n\t\t\t\tName:    \"validName\",\n\t\t\t\tPhpType: phpType(\"invalidType\"),\n\t\t\t},\n\t\t\texpectError: true,\n\t\t},\n\t}\n\n\tvalidator := Validator{}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := validator.validateClassProperty(tt.prop)\n\n\t\t\tif tt.expectError {\n\t\t\t\tassert.Error(t, err, \"validateClassProperty() should return an error\")\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err, \"validateClassProperty() should not return an error\")\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestValidateParameter(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\tparam       phpParameter\n\t\texpectError bool\n\t}{\n\t\t{\n\t\t\tname: \"valid string parameter\",\n\t\t\tparam: phpParameter{\n\t\t\t\tName:    \"validParam\",\n\t\t\t\tPhpType: phpString,\n\t\t\t},\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname: \"valid nullable parameter\",\n\t\t\tparam: phpParameter{\n\t\t\t\tName:       \"nullableParam\",\n\t\t\t\tPhpType:    phpInt,\n\t\t\t\tIsNullable: true,\n\t\t\t},\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname: \"valid parameter with default\",\n\t\t\tparam: phpParameter{\n\t\t\t\tName:         \"defaultParam\",\n\t\t\t\tPhpType:      phpString,\n\t\t\t\tHasDefault:   true,\n\t\t\t\tDefaultValue: \"hello\",\n\t\t\t},\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname: \"valid array parameter\",\n\t\t\tparam: phpParameter{\n\t\t\t\tName:    \"arrayParam\",\n\t\t\t\tPhpType: phpArray,\n\t\t\t},\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname: \"valid nullable array parameter\",\n\t\t\tparam: phpParameter{\n\t\t\t\tName:       \"nullableArrayParam\",\n\t\t\t\tPhpType:    phpArray,\n\t\t\t\tIsNullable: true,\n\t\t\t},\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname: \"valid callable parameter\",\n\t\t\tparam: phpParameter{\n\t\t\t\tName:    \"callbackParam\",\n\t\t\t\tPhpType: phpCallable,\n\t\t\t},\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname: \"valid nullable callable parameter\",\n\t\t\tparam: phpParameter{\n\t\t\t\tName:       \"nullableCallbackParam\",\n\t\t\t\tPhpType:    \"callable\",\n\t\t\t\tIsNullable: true,\n\t\t\t},\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname: \"empty parameter name\",\n\t\t\tparam: phpParameter{\n\t\t\t\tName:    \"\",\n\t\t\t\tPhpType: phpString,\n\t\t\t},\n\t\t\texpectError: true,\n\t\t},\n\t\t{\n\t\t\tname: \"invalid parameter name\",\n\t\t\tparam: phpParameter{\n\t\t\t\tName:    \"123invalid\",\n\t\t\t\tPhpType: phpString,\n\t\t\t},\n\t\t\texpectError: true,\n\t\t},\n\t\t{\n\t\t\tname: \"invalid parameter type\",\n\t\t\tparam: phpParameter{\n\t\t\t\tName:    \"validName\",\n\t\t\t\tPhpType: phpType(\"invalidType\"),\n\t\t\t},\n\t\t\texpectError: true,\n\t\t},\n\t}\n\n\tvalidator := Validator{}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := validator.validateParameter(tt.param)\n\n\t\t\tif tt.expectError {\n\t\t\t\tassert.Error(t, err, \"validateParameter() should return an error\")\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err, \"validateParameter() should not return an error\")\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestValidateClass(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\tclass       phpClass\n\t\texpectError bool\n\t}{\n\t\t{\n\t\t\tname: \"valid class\",\n\t\t\tclass: phpClass{\n\t\t\t\tName:     \"ValidClass\",\n\t\t\t\tGoStruct: \"ValidStruct\",\n\t\t\t\tProperties: []phpClassProperty{\n\t\t\t\t\t{Name: \"name\", PhpType: phpString},\n\t\t\t\t\t{Name: \"age\", PhpType: phpInt},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname: \"valid class with nullable properties\",\n\t\t\tclass: phpClass{\n\t\t\t\tName:     \"NullableClass\",\n\t\t\t\tGoStruct: \"NullableStruct\",\n\t\t\t\tProperties: []phpClassProperty{\n\t\t\t\t\t{Name: \"required\", PhpType: phpString, IsNullable: false},\n\t\t\t\t\t{Name: \"optional\", PhpType: phpString, IsNullable: true},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname: \"empty class name\",\n\t\t\tclass: phpClass{\n\t\t\t\tName:     \"\",\n\t\t\t\tGoStruct: \"ValidStruct\",\n\t\t\t},\n\t\t\texpectError: true,\n\t\t},\n\t\t{\n\t\t\tname: \"invalid class name\",\n\t\t\tclass: phpClass{\n\t\t\t\tName:     \"123InvalidClass\",\n\t\t\t\tGoStruct: \"ValidStruct\",\n\t\t\t},\n\t\t\texpectError: true,\n\t\t},\n\t\t{\n\t\t\tname: \"invalid property\",\n\t\t\tclass: phpClass{\n\t\t\t\tName:     \"ValidClass\",\n\t\t\t\tGoStruct: \"ValidStruct\",\n\t\t\t\tProperties: []phpClassProperty{\n\t\t\t\t\t{Name: \"123invalid\", PhpType: phpString},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectError: true,\n\t\t},\n\t}\n\n\tvalidator := Validator{}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := validator.validateClass(tt.class)\n\n\t\t\tif tt.expectError {\n\t\t\t\tassert.Error(t, err, \"validateClass() should return an error\")\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err, \"validateClass() should not return an error\")\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestValidateTypes(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\tfunction    phpFunction\n\t\texpectError bool\n\t\terrorMsg    string\n\t}{\n\t\t{\n\t\t\tname: \"valid scalar parameters only\",\n\t\t\tfunction: phpFunction{\n\t\t\t\tName:       \"validFunction\",\n\t\t\t\tReturnType: phpString,\n\t\t\t\tParams: []phpParameter{\n\t\t\t\t\t{Name: \"stringParam\", PhpType: phpString},\n\t\t\t\t\t{Name: \"intParam\", PhpType: phpInt},\n\t\t\t\t\t{Name: \"floatParam\", PhpType: phpFloat},\n\t\t\t\t\t{Name: \"boolParam\", PhpType: phpBool},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname: \"valid nullable scalar parameters\",\n\t\t\tfunction: phpFunction{\n\t\t\t\tName:       \"nullableFunction\",\n\t\t\t\tReturnType: phpString,\n\t\t\t\tParams: []phpParameter{\n\t\t\t\t\t{Name: \"stringParam\", PhpType: phpString, IsNullable: true},\n\t\t\t\t\t{Name: \"intParam\", PhpType: phpInt, IsNullable: true},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname: \"valid void return type\",\n\t\t\tfunction: phpFunction{\n\t\t\t\tName:       \"voidFunction\",\n\t\t\t\tReturnType: phpVoid,\n\t\t\t\tParams: []phpParameter{\n\t\t\t\t\t{Name: \"stringParam\", PhpType: phpString},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname: \"valid array parameter and return\",\n\t\t\tfunction: phpFunction{\n\t\t\t\tName:       \"arrayFunction\",\n\t\t\t\tReturnType: phpArray,\n\t\t\t\tParams: []phpParameter{\n\t\t\t\t\t{Name: \"arrayParam\", PhpType: phpArray},\n\t\t\t\t\t{Name: \"stringParam\", PhpType: phpString},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname: \"valid nullable array parameter\",\n\t\t\tfunction: phpFunction{\n\t\t\t\tName:       \"nullableArrayFunction\",\n\t\t\t\tReturnType: phpString,\n\t\t\t\tParams: []phpParameter{\n\t\t\t\t\t{Name: \"arrayParam\", PhpType: phpArray, IsNullable: true},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname: \"valid callable parameter\",\n\t\t\tfunction: phpFunction{\n\t\t\t\tName:       \"callableFunction\",\n\t\t\t\tReturnType: \"array\",\n\t\t\t\tParams: []phpParameter{\n\t\t\t\t\t{Name: \"callbackParam\", PhpType: phpCallable},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname: \"valid nullable callable parameter\",\n\t\t\tfunction: phpFunction{\n\t\t\t\tName:       \"nullableCallableFunction\",\n\t\t\t\tReturnType: \"string\",\n\t\t\t\tParams: []phpParameter{\n\t\t\t\t\t{Name: \"callbackParam\", PhpType: phpCallable, IsNullable: true},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname: \"invalid object parameter\",\n\t\t\tfunction: phpFunction{\n\t\t\t\tName:       \"objectFunction\",\n\t\t\t\tReturnType: phpString,\n\t\t\t\tParams: []phpParameter{\n\t\t\t\t\t{Name: \"objectParam\", PhpType: phpObject},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectError: true,\n\t\t\terrorMsg:    `parameter 1 \"objectParam\" has unsupported type \"object\"`,\n\t\t},\n\t\t{\n\t\t\tname: \"invalid object return type\",\n\t\t\tfunction: phpFunction{\n\t\t\t\tName:       \"objectReturnFunction\",\n\t\t\t\tReturnType: phpObject,\n\t\t\t\tParams: []phpParameter{\n\t\t\t\t\t{Name: \"stringParam\", PhpType: phpString},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectError: true,\n\t\t\terrorMsg:    `return type \"object\" is not supported`,\n\t\t},\n\t\t{\n\t\t\tname: \"mixed scalar and invalid parameters\",\n\t\t\tfunction: phpFunction{\n\t\t\t\tName:       \"mixedFunction\",\n\t\t\t\tReturnType: phpString,\n\t\t\t\tParams: []phpParameter{\n\t\t\t\t\t{Name: \"validParam\", PhpType: phpString},\n\t\t\t\t\t{Name: \"invalidParam\", PhpType: phpObject},\n\t\t\t\t\t{Name: \"anotherValidParam\", PhpType: phpInt},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectError: true,\n\t\t\terrorMsg:    `parameter 2 \"invalidParam\" has unsupported type \"object\"`,\n\t\t},\n\t}\n\n\tvalidator := Validator{}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := validator.validateTypes(tt.function)\n\n\t\t\tif tt.expectError {\n\t\t\t\tassert.Error(t, err, \"validateTypes() should return an error for function %s\", tt.function.Name)\n\t\t\t\tassert.Contains(t, err.Error(), tt.errorMsg, \"Error message should contain expected text\")\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err, \"validateTypes() should not return an error for function %s\", tt.function.Name)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestValidateGoFunctionSignature(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\tphpFunc     phpFunction\n\t\texpectError bool\n\t\terrorMsg    string\n\t}{\n\t\t{\n\t\t\tname: \"valid Go function signature\",\n\t\t\tphpFunc: phpFunction{\n\t\t\t\tName:       \"testFunc\",\n\t\t\t\tReturnType: phpString,\n\t\t\t\tParams: []phpParameter{\n\t\t\t\t\t{Name: \"name\", PhpType: phpString},\n\t\t\t\t\t{Name: \"count\", PhpType: phpInt},\n\t\t\t\t},\n\t\t\t\tGoFunction: `func testFunc(name *C.zend_string, count int64) unsafe.Pointer {\n\treturn nil\n}`,\n\t\t\t},\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname: \"valid void return type\",\n\t\t\tphpFunc: phpFunction{\n\t\t\t\tName:       \"voidFunc\",\n\t\t\t\tReturnType: phpVoid,\n\t\t\t\tParams: []phpParameter{\n\t\t\t\t\t{Name: \"message\", PhpType: phpString},\n\t\t\t\t},\n\t\t\t\tGoFunction: `func voidFunc(message *C.zend_string) {\n\t// Do something\n}`,\n\t\t\t},\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname: \"no Go function provided\",\n\t\t\tphpFunc: phpFunction{\n\t\t\t\tName:       \"noGoFunc\",\n\t\t\t\tReturnType: phpString,\n\t\t\t\tParams:     []phpParameter{},\n\t\t\t\tGoFunction: \"\",\n\t\t\t},\n\t\t\texpectError: true,\n\t\t\terrorMsg:    \"no Go function found\",\n\t\t},\n\t\t{\n\t\t\tname: \"parameter count mismatch\",\n\t\t\tphpFunc: phpFunction{\n\t\t\t\tName:       \"countMismatch\",\n\t\t\t\tReturnType: phpString,\n\t\t\t\tParams: []phpParameter{\n\t\t\t\t\t{Name: \"param1\", PhpType: phpString},\n\t\t\t\t\t{Name: \"param2\", PhpType: phpInt},\n\t\t\t\t},\n\t\t\t\tGoFunction: `func countMismatch(param1 *C.zend_string) unsafe.Pointer {\n\treturn nil\n}`,\n\t\t\t},\n\t\t\texpectError: true,\n\t\t\terrorMsg:    \"parameter count mismatch: PHP function has 2 parameters (expecting 2 Go parameters) but Go function has 1\",\n\t\t},\n\t\t{\n\t\t\tname: \"parameter type mismatch\",\n\t\t\tphpFunc: phpFunction{\n\t\t\t\tName:       \"typeMismatch\",\n\t\t\t\tReturnType: phpString,\n\t\t\t\tParams: []phpParameter{\n\t\t\t\t\t{Name: \"name\", PhpType: phpString},\n\t\t\t\t\t{Name: \"count\", PhpType: phpInt},\n\t\t\t\t},\n\t\t\t\tGoFunction: `func typeMismatch(name *C.zend_string, count string) unsafe.Pointer {\n\treturn nil\n}`,\n\t\t\t},\n\t\t\texpectError: true,\n\t\t\terrorMsg:    `parameter 2 type mismatch: PHP \"int\" requires Go type \"int64\" but found \"string\"`,\n\t\t},\n\t\t{\n\t\t\tname: \"return type mismatch\",\n\t\t\tphpFunc: phpFunction{\n\t\t\t\tName:       \"returnMismatch\",\n\t\t\t\tReturnType: phpInt,\n\t\t\t\tParams: []phpParameter{\n\t\t\t\t\t{Name: \"value\", PhpType: phpString},\n\t\t\t\t},\n\t\t\t\tGoFunction: `func returnMismatch(value *C.zend_string) string {\n\treturn \"\"\n}`,\n\t\t\t},\n\t\t\texpectError: true,\n\t\t\terrorMsg:    `return type mismatch: PHP \"int\" requires Go return type \"int64\" but found \"string\"`,\n\t\t},\n\t\t{\n\t\t\tname: \"valid bool parameter and return\",\n\t\t\tphpFunc: phpFunction{\n\t\t\t\tName:       \"boolFunc\",\n\t\t\t\tReturnType: phpBool,\n\t\t\t\tParams: []phpParameter{\n\t\t\t\t\t{Name: \"flag\", PhpType: phpBool},\n\t\t\t\t},\n\t\t\t\tGoFunction: `func boolFunc(flag bool) bool {\n\treturn flag\n}`,\n\t\t\t},\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname: \"valid float parameter and return\",\n\t\t\tphpFunc: phpFunction{\n\t\t\t\tName:       \"floatFunc\",\n\t\t\t\tReturnType: phpFloat,\n\t\t\t\tParams: []phpParameter{\n\t\t\t\t\t{Name: \"value\", PhpType: phpFloat},\n\t\t\t\t},\n\t\t\t\tGoFunction: `func floatFunc(value float64) float64 {\n\treturn value * 2.0\n}`,\n\t\t\t},\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname: \"valid array parameter and return\",\n\t\t\tphpFunc: phpFunction{\n\t\t\t\tName:       \"arrayFunc\",\n\t\t\t\tReturnType: phpArray,\n\t\t\t\tParams: []phpParameter{\n\t\t\t\t\t{Name: \"items\", PhpType: phpArray},\n\t\t\t\t},\n\t\t\t\tGoFunction: `func arrayFunc(items *C.zend_array) unsafe.Pointer {\n\treturn nil\n}`,\n\t\t\t},\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname: \"valid nullable array parameter\",\n\t\t\tphpFunc: phpFunction{\n\t\t\t\tName:       \"nullableArrayFunc\",\n\t\t\t\tReturnType: phpString,\n\t\t\t\tParams: []phpParameter{\n\t\t\t\t\t{Name: \"items\", PhpType: phpArray, IsNullable: true},\n\t\t\t\t\t{Name: \"name\", PhpType: phpString},\n\t\t\t\t},\n\t\t\t\tGoFunction: `func nullableArrayFunc(items *C.zend_array, name *C.zend_string) unsafe.Pointer {\n\treturn nil\n}`,\n\t\t\t},\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname: \"mixed array and scalar parameters\",\n\t\t\tphpFunc: phpFunction{\n\t\t\t\tName:       \"mixedFunc\",\n\t\t\t\tReturnType: phpArray,\n\t\t\t\tParams: []phpParameter{\n\t\t\t\t\t{Name: \"data\", PhpType: phpArray},\n\t\t\t\t\t{Name: \"filter\", PhpType: phpString},\n\t\t\t\t\t{Name: \"limit\", PhpType: phpInt},\n\t\t\t\t},\n\t\t\t\tGoFunction: `func mixedFunc(data *C.zend_array, filter *C.zend_string, limit int64) unsafe.Pointer {\n\treturn nil\n}`,\n\t\t\t},\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname: \"valid callable parameter\",\n\t\t\tphpFunc: phpFunction{\n\t\t\t\tName:       \"callableFunc\",\n\t\t\t\tReturnType: \"array\",\n\t\t\t\tParams: []phpParameter{\n\t\t\t\t\t{Name: \"callback\", PhpType: phpCallable},\n\t\t\t\t},\n\t\t\t\tGoFunction: `func callableFunc(callback *C.zval) unsafe.Pointer {\n\treturn nil\n}`,\n\t\t\t},\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname: \"valid nullable callable parameter\",\n\t\t\tphpFunc: phpFunction{\n\t\t\t\tName:       \"nullableCallableFunc\",\n\t\t\t\tReturnType: \"string\",\n\t\t\t\tParams: []phpParameter{\n\t\t\t\t\t{Name: \"callback\", PhpType: phpCallable, IsNullable: true},\n\t\t\t\t},\n\t\t\t\tGoFunction: `func nullableCallableFunc(callback *C.zval) unsafe.Pointer {\n\treturn nil\n}`,\n\t\t\t},\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname: \"mixed callable and other parameters\",\n\t\t\tphpFunc: phpFunction{\n\t\t\t\tName:       \"mixedCallableFunc\",\n\t\t\t\tReturnType: \"array\",\n\t\t\t\tParams: []phpParameter{\n\t\t\t\t\t{Name: \"data\", PhpType: phpArray},\n\t\t\t\t\t{Name: \"callback\", PhpType: phpCallable},\n\t\t\t\t\t{Name: \"options\", PhpType: \"int\"},\n\t\t\t\t},\n\t\t\t\tGoFunction: `func mixedCallableFunc(data *C.zend_array, callback *C.zval, options int64) unsafe.Pointer {\n\treturn nil\n}`,\n\t\t\t},\n\t\t\texpectError: false,\n\t\t},\n\t}\n\n\tvalidator := Validator{}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := validator.validateGoFunctionSignatureWithOptions(tt.phpFunc, false)\n\n\t\t\tif tt.expectError {\n\t\t\t\tassert.Error(t, err, \"validateGoFunctionSignature() should return an error for function %s\", tt.phpFunc.Name)\n\t\t\t\tassert.Contains(t, err.Error(), tt.errorMsg, \"Error message should contain expected text\")\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err, \"validateGoFunctionSignature() should not return an error for function %s\", tt.phpFunc.Name)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestPhpTypeToGoType(t *testing.T) {\n\ttests := []struct {\n\t\tphpType    string\n\t\tisNullable bool\n\t\texpected   string\n\t}{\n\t\t{\"string\", false, \"*C.zend_string\"},\n\t\t{\"string\", true, \"*C.zend_string\"},\n\t\t{\"int\", false, \"int64\"},\n\t\t{\"int\", true, \"*int64\"},\n\t\t{\"float\", false, \"float64\"},\n\t\t{\"float\", true, \"*float64\"},\n\t\t{\"bool\", false, \"bool\"},\n\t\t{\"bool\", true, \"*bool\"},\n\t\t{\"array\", false, \"*C.zend_array\"},\n\t\t{\"array\", true, \"*C.zend_array\"},\n\t\t{\"callable\", false, \"*C.zval\"},\n\t\t{\"callable\", true, \"*C.zval\"},\n\t\t{\"unknown\", false, \"any\"},\n\t}\n\n\tvalidator := Validator{}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.phpType, func(t *testing.T) {\n\t\t\tresult := validator.phpTypeToGoType(phpType(tt.phpType), tt.isNullable)\n\t\t\tassert.Equal(t, tt.expected, result, \"phpTypeToGoType(%s, %v) should return %s\", tt.phpType, tt.isNullable, tt.expected)\n\t\t})\n\t}\n}\n\nfunc TestPhpReturnTypeToGoType(t *testing.T) {\n\ttests := []struct {\n\t\tphpReturnType string\n\t\texpected      string\n\t}{\n\t\t{\"void\", \"\"},\n\t\t{\"void\", \"\"},\n\t\t{\"string\", \"unsafe.Pointer\"},\n\t\t{\"string\", \"unsafe.Pointer\"},\n\t\t{\"int\", \"int64\"},\n\t\t{\"int\", \"int64\"},\n\t\t{\"float\", \"float64\"},\n\t\t{\"float\", \"float64\"},\n\t\t{\"bool\", \"bool\"},\n\t\t{\"bool\", \"bool\"},\n\t\t{\"array\", \"unsafe.Pointer\"},\n\t\t{\"array\", \"unsafe.Pointer\"},\n\t\t{\"unknown\", \"any\"},\n\t}\n\n\tvalidator := Validator{}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.phpReturnType, func(t *testing.T) {\n\t\t\tresult := validator.phpReturnTypeToGoType(phpType(tt.phpReturnType))\n\t\t\tassert.Equal(t, tt.expected, result, \"phpReturnTypeToGoType(%s) should return %s\", tt.phpReturnType, tt.expected)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/fastabs/filepath.go",
    "content": "//go:build !unix\n\npackage fastabs\n\nimport (\n\t\"path/filepath\"\n)\n\n// FastAbs can't be optimized on Windows because the\n// syscall.FullPath function takes an input.\nfunc FastAbs(path string) (string, error) {\n\t// Normalize forward slashes to backslashes for Windows compatibility\n\treturn filepath.Abs(filepath.FromSlash(path))\n}\n"
  },
  {
    "path": "internal/fastabs/filepath_unix.go",
    "content": "//go:build unix\n\npackage fastabs\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n)\n\nvar (\n\twd    string\n\twderr error\n)\n\nfunc init() {\n\twd, wderr = os.Getwd()\n\n\tif wderr != nil {\n\t\treturn\n\t}\n\n\tcanonicalWD, err := filepath.EvalSymlinks(wd)\n\tif err == nil {\n\t\twd = canonicalWD\n\t}\n}\n\n// FastAbs is an optimized version of filepath.Abs for Unix systems,\n// since we don't expect the working directory to ever change once\n// Caddy is running. Avoid the os.Getwd syscall overhead.\nfunc FastAbs(path string) (string, error) {\n\tif filepath.IsAbs(path) {\n\t\treturn filepath.Clean(path), nil\n\t}\n\n\tif wderr != nil {\n\t\treturn \"\", wderr\n\t}\n\n\treturn filepath.Join(wd, path), nil\n}\n"
  },
  {
    "path": "internal/memory/memory_linux.go",
    "content": "package memory\n\nimport \"syscall\"\n\nfunc TotalSysMemory() uint64 {\n\tsysInfo := &syscall.Sysinfo_t{}\n\terr := syscall.Sysinfo(sysInfo)\n\tif err != nil {\n\t\treturn 0\n\t}\n\n\treturn uint64(sysInfo.Totalram) * uint64(sysInfo.Unit)\n}\n"
  },
  {
    "path": "internal/memory/memory_others.go",
    "content": "//go:build !linux\n\npackage memory\n\n// TotalSysMemory returns 0 if the total system memory cannot be determined\nfunc TotalSysMemory() uint64 {\n\treturn 0\n}\n"
  },
  {
    "path": "internal/phpheaders/phpheaders.go",
    "content": "package phpheaders\n\nimport \"C\"\nimport (\n\t\"context\"\n\t\"strings\"\n\n\t\"github.com/maypok86/otter/v2\"\n)\n\n// Translate header names to PHP header names\n// All headers in 'commonHeaders' can be cached and registered safely\n// All other headers must be sanitized\n// Note: net/http will capitalize lowercase headers, so we don't need to worry about case sensitivity\nvar CommonRequestHeaders = map[string]string{\n\t\"Accept\":                            \"HTTP_ACCEPT\",\n\t\"Accept-Charset\":                    \"HTTP_ACCEPT_CHARSET\",\n\t\"Accept-Encoding\":                   \"HTTP_ACCEPT_ENCODING\",\n\t\"Accept-Language\":                   \"HTTP_ACCEPT_LANGUAGE\",\n\t\"Access-Control-Request-Headers\":    \"HTTP_ACCESS_CONTROL_REQUEST_HEADERS\",\n\t\"Access-Control-Request-Method\":     \"HTTP_ACCESS_CONTROL_REQUEST_METHOD\",\n\t\"Authorization\":                     \"HTTP_AUTHORIZATION\",\n\t\"Cache-Control\":                     \"HTTP_CACHE_CONTROL\",\n\t\"Connection\":                        \"HTTP_CONNECTION\",\n\t\"Content-Disposition\":               \"HTTP_CONTENT_DISPOSITION\",\n\t\"Content-Encoding\":                  \"HTTP_CONTENT_ENCODING\",\n\t\"Content-Length\":                    \"HTTP_CONTENT_LENGTH\",\n\t\"Content-Type\":                      \"HTTP_CONTENT_TYPE\",\n\t\"Cookie\":                            \"HTTP_COOKIE\",\n\t\"Date\":                              \"HTTP_DATE\",\n\t\"Device-Memory\":                     \"HTTP_DEVICE_MEMORY\",\n\t\"Dnt\":                               \"HTTP_DNT\",\n\t\"Downlink\":                          \"HTTP_DOWNLINK\",\n\t\"Dpr\":                               \"HTTP_DPR\",\n\t\"Early-Data\":                        \"HTTP_EARLY_DATA\",\n\t\"Ect\":                               \"HTTP_ECT\",\n\t\"Am-I\":                              \"HTTP_AM_I\",\n\t\"Expect\":                            \"HTTP_EXPECT\",\n\t\"Forwarded\":                         \"HTTP_FORWARDED\",\n\t\"From\":                              \"HTTP_FROM\",\n\t\"Host\":                              \"HTTP_HOST\",\n\t\"If-Match\":                          \"HTTP_IF_MATCH\",\n\t\"If-Modified-Since\":                 \"HTTP_IF_MODIFIED_SINCE\",\n\t\"If-None-Match\":                     \"HTTP_IF_NONE_MATCH\",\n\t\"If-Range\":                          \"HTTP_IF_RANGE\",\n\t\"If-Unmodified-Since\":               \"HTTP_IF_UNMODIFIED_SINCE\",\n\t\"Keep-Alive\":                        \"HTTP_KEEP_ALIVE\",\n\t\"Max-Forwards\":                      \"HTTP_MAX_FORWARDS\",\n\t\"Origin\":                            \"HTTP_ORIGIN\",\n\t\"Pragma\":                            \"HTTP_PRAGMA\",\n\t\"Proxy-Authorization\":               \"HTTP_PROXY_AUTHORIZATION\",\n\t\"Range\":                             \"HTTP_RANGE\",\n\t\"Referer\":                           \"HTTP_REFERER\",\n\t\"Rtt\":                               \"HTTP_RTT\",\n\t\"Save-Data\":                         \"HTTP_SAVE_DATA\",\n\t\"Sec-Ch-Ua\":                         \"HTTP_SEC_CH_UA\",\n\t\"Sec-Ch-Ua-Arch\":                    \"HTTP_SEC_CH_UA_ARCH\",\n\t\"Sec-Ch-Ua-Bitness\":                 \"HTTP_SEC_CH_UA_BITNESS\",\n\t\"Sec-Ch-Ua-Full-Version\":            \"HTTP_SEC_CH_UA_FULL_VERSION\",\n\t\"Sec-Ch-Ua-Full-Version-List\":       \"HTTP_SEC_CH_UA_FULL_VERSION_LIST\",\n\t\"Sec-Ch-Ua-Mobile\":                  \"HTTP_SEC_CH_UA_MOBILE\",\n\t\"Sec-Ch-Ua-Model\":                   \"HTTP_SEC_CH_UA_MODEL\",\n\t\"Sec-Ch-Ua-Platform\":                \"HTTP_SEC_CH_UA_PLATFORM\",\n\t\"Sec-Ch-Ua-Platform-Version\":        \"HTTP_SEC_CH_UA_PLATFORM_VERSION\",\n\t\"Sec-Fetch-Dest\":                    \"HTTP_SEC_FETCH_DEST\",\n\t\"Sec-Fetch-Mode\":                    \"HTTP_SEC_FETCH_MODE\",\n\t\"Sec-Fetch-Site\":                    \"HTTP_SEC_FETCH_SITE\",\n\t\"Sec-Fetch-User\":                    \"HTTP_SEC_FETCH_USER\",\n\t\"Sec-Gpc\":                           \"HTTP_SEC_GPC\",\n\t\"Service-Worker-Navigation-Preload\": \"HTTP_SERVICE_WORKER_NAVIGATION_PRELOAD\",\n\t\"Te\":                                \"HTTP_TE\",\n\t\"Priority\":                          \"HTTP_PRIORITY\",\n\t\"Trailer\":                           \"HTTP_TRAILER\",\n\t\"Transfer-Encoding\":                 \"HTTP_TRANSFER_ENCODING\",\n\t\"Upgrade\":                           \"HTTP_UPGRADE\",\n\t\"Upgrade-Insecure-Requests\":         \"HTTP_UPGRADE_INSECURE_REQUESTS\",\n\t\"User-Agent\":                        \"HTTP_USER_AGENT\",\n\t\"Via\":                               \"HTTP_VIA\",\n\t\"Viewport-Width\":                    \"HTTP_VIEWPORT_WIDTH\",\n\t\"Want-Digest\":                       \"HTTP_WANT_DIGEST\",\n\t\"Warning\":                           \"HTTP_WARNING\",\n\t\"Width\":                             \"HTTP_WIDTH\",\n\t\"X-Forwarded-For\":                   \"HTTP_X_FORWARDED_FOR\",\n\t\"X-Forwarded-Host\":                  \"HTTP_X_FORWARDED_HOST\",\n\t\"X-Forwarded-Path\":                  \"HTTP_X_FORWARDED_PATH\",\n\t\"X-Forwarded-Prefix\":                \"HTTP_X_FORWARDED_PREFIX\",\n\t\"X-Forwarded-Proto\":                 \"HTTP_X_FORWARDED_PROTO\",\n\t\"A-Im\":                              \"HTTP_A_IM\",\n\t\"Accept-Datetime\":                   \"HTTP_ACCEPT_DATETIME\",\n\t\"Content-Md5\":                       \"HTTP_CONTENT_MD5\",\n\t\"Http2-Settings\":                    \"HTTP_HTTP2_SETTINGS\",\n\t\"Prefer\":                            \"HTTP_PREFER\",\n\t\"X-Requested-With\":                  \"HTTP_X_REQUESTED_WITH\",\n\t\"Front-End-Https\":                   \"HTTP_FRONT_END_HTTPS\",\n\t\"X-Http-Method-Override\":            \"HTTP_X_HTTP_METHOD_OVERRIDE\",\n\t\"X-Att-Deviceid\":                    \"HTTP_X_ATT_DEVICEID\",\n\t\"X-Wap-Profile\":                     \"HTTP_X_WAP_PROFILE\",\n\t\"Proxy-Connection\":                  \"HTTP_PROXY_CONNECTION\",\n\t\"X-Uidh\":                            \"HTTP_X_UIDH\",\n\t\"X-Csrf-Token\":                      \"HTTP_X_CSRF_TOKEN\",\n\t\"X-Request-Id\":                      \"HTTP_X_REQUEST_ID\",\n\t\"X-Correlation-Id\":                  \"HTTP_X_CORRELATION_ID\",\n\t// Additional CDN/Framework headers\n\t\"Cloudflare-Visitor\":        \"HTTP_CLOUDFLARE_VISITOR\",\n\t\"Cloudfront-Viewer-Address\": \"HTTP_CLOUDFRONT_VIEWER_ADDRESS\",\n\t\"Cloudfront-Viewer-Country\": \"HTTP_CLOUDFRONT_VIEWER_COUNTRY\",\n\t\"X-Amzn-Trace-Id\":           \"HTTP_X_AMZN_TRACE_ID\",\n\t\"X-Cloud-Trace-Context\":     \"HTTP_X_CLOUD_TRACE_CONTEXT\",\n\t\"Cf-Ray\":                    \"HTTP_CF_RAY\",\n\t\"Cf-Visitor\":                \"HTTP_CF_VISITOR\",\n\t\"Cf-Request-Id\":             \"HTTP_CF_REQUEST_ID\",\n\t\"Cf-Ipcountry\":              \"HTTP_CF_IPCOUNTRY\",\n\t\"X-Device-Type\":             \"HTTP_X_DEVICE_TYPE\",\n\t\"X-Network-Info\":            \"HTTP_X_NETWORK_INFO\",\n\t\"X-Client-Id\":               \"HTTP_X_CLIENT_ID\",\n\t\"X-Livewire\":                \"HTTP_X_LIVEWIRE\",\n\t\"X-Real-Ip\":                 \"HTTP_X_REAL_IP\",\n}\n\n// Cache up to 256 uncommon headers\n// This is ~2.5x faster than converting the header each time\nvar (\n\theaderKeyCache     = otter.Must[string, string](&otter.Options[string, string]{MaximumSize: 256})\n\theaderNameReplacer = strings.NewReplacer(\" \", \"_\", \"-\", \"_\")\n\tloader             = otter.LoaderFunc[string, string](func(_ context.Context, key string) (string, error) {\n\t\treturn \"HTTP_\" + headerNameReplacer.Replace(strings.ToUpper(key)) + \"\\x00\", nil\n\t})\n)\n\nfunc GetUnCommonHeader(ctx context.Context, key string) string {\n\tphpHeaderKey, err := headerKeyCache.Get(ctx, key, loader)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn phpHeaderKey\n}\n"
  },
  {
    "path": "internal/phpheaders/phpheaders_test.go",
    "content": "package phpheaders\n\nimport (\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestAllCommonHeadersAreCorrect(t *testing.T) {\n\tfakeRequest := httptest.NewRequest(\"GET\", \"http://localhost\", nil)\n\n\tfor header, phpHeader := range CommonRequestHeaders {\n\t\t// verify that common and uncommon headers return the same result\n\t\texpectedPHPHeader := GetUnCommonHeader(t.Context(), header)\n\t\tassert.Equal(t, phpHeader+\"\\x00\", expectedPHPHeader, \"header is not well formed: \"+phpHeader)\n\n\t\t// net/http will capitalize lowercase headers, verify that headers are capitalized\n\t\tfakeRequest.Header.Add(header, \"foo\")\n\t\tassert.Contains(t, fakeRequest.Header, header, \"header is not correctly capitalized: \"+header)\n\t}\n}\n"
  },
  {
    "path": "internal/state/state.go",
    "content": "package state\n\nimport \"C\"\nimport (\n\t\"slices\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n)\n\ntype State int\n\nconst (\n\t// lifecycle States of a thread\n\tReserved State = iota\n\tBooting\n\tBootRequested\n\tShuttingDown\n\tDone\n\n\t// these States are 'stable' and safe to transition from at any time\n\tInactive\n\tReady\n\n\t// States necessary for restarting workers\n\tRestarting\n\tYielding\n\n\t// States necessary for transitioning between different handlers\n\tTransitionRequested\n\tTransitionInProgress\n\tTransitionComplete\n)\n\nfunc (s State) String() string {\n\tswitch s {\n\tcase Reserved:\n\t\treturn \"reserved\"\n\tcase Booting:\n\t\treturn \"booting\"\n\tcase BootRequested:\n\t\treturn \"boot requested\"\n\tcase ShuttingDown:\n\t\treturn \"shutting down\"\n\tcase Done:\n\t\treturn \"done\"\n\tcase Inactive:\n\t\treturn \"inactive\"\n\tcase Ready:\n\t\treturn \"ready\"\n\tcase Restarting:\n\t\treturn \"restarting\"\n\tcase Yielding:\n\t\treturn \"yielding\"\n\tcase TransitionRequested:\n\t\treturn \"transition requested\"\n\tcase TransitionInProgress:\n\t\treturn \"transition in progress\"\n\tcase TransitionComplete:\n\t\treturn \"transition complete\"\n\tdefault:\n\t\treturn \"unknown\"\n\t}\n}\n\ntype ThreadState struct {\n\tcurrentState State\n\tmu           sync.RWMutex\n\tsubscribers  []stateSubscriber\n\t// how long threads have been waiting in stable states (unix ms, 0 = not waiting)\n\twaitingSince atomic.Int64\n}\n\ntype stateSubscriber struct {\n\tstates []State\n\tch     chan struct{}\n}\n\nfunc NewThreadState() *ThreadState {\n\treturn &ThreadState{\n\t\tcurrentState: Reserved,\n\t\tsubscribers:  []stateSubscriber{},\n\t\tmu:           sync.RWMutex{},\n\t}\n}\n\nfunc (ts *ThreadState) Is(state State) bool {\n\tts.mu.RLock()\n\tok := ts.currentState == state\n\tts.mu.RUnlock()\n\n\treturn ok\n}\n\nfunc (ts *ThreadState) CompareAndSwap(compareTo State, swapTo State) bool {\n\tts.mu.Lock()\n\tok := ts.currentState == compareTo\n\tif ok {\n\t\tts.currentState = swapTo\n\t\tts.notifySubscribers(swapTo)\n\t}\n\tts.mu.Unlock()\n\n\treturn ok\n}\n\nfunc (ts *ThreadState) Name() string {\n\treturn ts.Get().String()\n}\n\nfunc (ts *ThreadState) Get() State {\n\tts.mu.RLock()\n\tid := ts.currentState\n\tts.mu.RUnlock()\n\n\treturn id\n}\n\nfunc (ts *ThreadState) Set(nextState State) {\n\tts.mu.Lock()\n\tts.currentState = nextState\n\tts.notifySubscribers(nextState)\n\tts.mu.Unlock()\n}\n\nfunc (ts *ThreadState) notifySubscribers(nextState State) {\n\tif len(ts.subscribers) == 0 {\n\t\treturn\n\t}\n\n\tn := 0\n\tfor _, sub := range ts.subscribers {\n\t\tif !slices.Contains(sub.states, nextState) {\n\t\t\tts.subscribers[n] = sub\n\t\t\tn++\n\t\t\tcontinue\n\t\t}\n\t\tclose(sub.ch)\n\t}\n\n\tts.subscribers = ts.subscribers[:n]\n}\n\n// WaitFor blocks until the thread reaches a certain state\nfunc (ts *ThreadState) WaitFor(states ...State) {\n\tts.mu.Lock()\n\tif slices.Contains(states, ts.currentState) {\n\t\tts.mu.Unlock()\n\n\t\treturn\n\t}\n\n\tsub := stateSubscriber{\n\t\tstates: states,\n\t\tch:     make(chan struct{}),\n\t}\n\tts.subscribers = append(ts.subscribers, sub)\n\tts.mu.Unlock()\n\t<-sub.ch\n}\n\n// RequestSafeStateChange safely requests a state change from a different goroutine\nfunc (ts *ThreadState) RequestSafeStateChange(nextState State) bool {\n\tts.mu.Lock()\n\tswitch ts.currentState {\n\t// disallow state changes if shutting down or done\n\tcase ShuttingDown, Done, Reserved:\n\t\tts.mu.Unlock()\n\n\t\treturn false\n\t// ready and inactive are safe states to transition from\n\tcase Ready, Inactive:\n\t\tts.currentState = nextState\n\t\tts.notifySubscribers(nextState)\n\t\tts.mu.Unlock()\n\n\t\treturn true\n\t}\n\tts.mu.Unlock()\n\n\t// wait for the state to change to a safe state\n\tts.WaitFor(Ready, Inactive, ShuttingDown)\n\n\treturn ts.RequestSafeStateChange(nextState)\n}\n\n// MarkAsWaiting hints that the thread reached a stable state and is waiting for requests or shutdown\nfunc (ts *ThreadState) MarkAsWaiting(isWaiting bool) {\n\tif isWaiting {\n\t\tts.waitingSince.Store(time.Now().UnixMilli())\n\t} else {\n\t\tts.waitingSince.Store(0)\n\t}\n}\n\n// IsInWaitingState returns true if a thread is waiting for a request or shutdown\nfunc (ts *ThreadState) IsInWaitingState() bool {\n\treturn ts.waitingSince.Load() != 0\n}\n\n// WaitTime returns the time since the thread is waiting in a stable state in ms\nfunc (ts *ThreadState) WaitTime() int64 {\n\tsince := ts.waitingSince.Load()\n\tif since == 0 {\n\t\treturn 0\n\t}\n\treturn time.Now().UnixMilli() - since\n}\n\nfunc (ts *ThreadState) SetWaitTime(t time.Time) {\n\tts.waitingSince.Store(t.UnixMilli())\n}\n"
  },
  {
    "path": "internal/state/state_test.go",
    "content": "package state\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc Test2GoroutinesYieldToEachOtherViaStates(t *testing.T) {\n\tthreadState := &ThreadState{currentState: Booting}\n\n\tgo func() {\n\t\tthreadState.WaitFor(Inactive)\n\t\tassert.True(t, threadState.Is(Inactive))\n\t\tthreadState.Set(Ready)\n\t}()\n\n\tthreadState.Set(Inactive)\n\tthreadState.WaitFor(Ready)\n\tassert.True(t, threadState.Is(Ready))\n}\n\nfunc TestStateShouldHaveCorrectAmountOfSubscribers(t *testing.T) {\n\tthreadState := &ThreadState{currentState: Booting}\n\n\t// 3 subscribers waiting for different states\n\tgo threadState.WaitFor(Inactive)\n\tgo threadState.WaitFor(Inactive, ShuttingDown)\n\tgo threadState.WaitFor(ShuttingDown)\n\n\tassertNumberOfSubscribers(t, threadState, 3)\n\n\tthreadState.Set(Inactive)\n\tassertNumberOfSubscribers(t, threadState, 1)\n\n\tassert.True(t, threadState.CompareAndSwap(Inactive, ShuttingDown))\n\tassertNumberOfSubscribers(t, threadState, 0)\n}\n\nfunc assertNumberOfSubscribers(t *testing.T, threadState *ThreadState, expected int) {\n\tt.Helper()\n\tfor range 10_000 { // wait for 1 second max\n\t\ttime.Sleep(100 * time.Microsecond)\n\t\tthreadState.mu.RLock()\n\t\tif len(threadState.subscribers) == expected {\n\t\t\tthreadState.mu.RUnlock()\n\t\t\tbreak\n\t\t}\n\t\tthreadState.mu.RUnlock()\n\t}\n\tthreadState.mu.RLock()\n\tassert.Len(t, threadState.subscribers, expected)\n\tthreadState.mu.RUnlock()\n}\n"
  },
  {
    "path": "internal/testcli/main.go",
    "content": "package main\n\nimport (\n\t\"log\"\n\t\"os\"\n\n\t\"github.com/dunglas/frankenphp\"\n)\n\nfunc main() {\n\tif len(os.Args) <= 1 {\n\t\tlog.Println(\"Usage: testcli script.php\")\n\t\tos.Exit(1)\n\t}\n\n\tif len(os.Args) == 3 && os.Args[1] == \"-r\" {\n\t\tos.Exit(frankenphp.ExecutePHPCode(os.Args[2]))\n\t}\n\n\tos.Exit(frankenphp.ExecuteScriptCLI(os.Args[1], os.Args))\n}\n"
  },
  {
    "path": "internal/testext/ext_test.go",
    "content": "package testext\n\nimport \"testing\"\n\nfunc TestRegisterExtension(t *testing.T) {\n\ttestRegisterExtension(t)\n}\n"
  },
  {
    "path": "internal/testext/extension.h",
    "content": "#ifndef _EXTENSIONS_H\n#define _EXTENSIONS_H\n\n#include \"../../frankenphp.h\"\n#include <php.h>\n\nextern zend_module_entry module1_entry;\nextern zend_module_entry module2_entry;\n\n#endif\n"
  },
  {
    "path": "internal/testext/extensions.c",
    "content": "#include \"extension.h\"\n#include <php.h>\n#include <zend_exceptions.h>\n\n#include \"_cgo_export.h\"\n\nzend_module_entry module1_entry = {STANDARD_MODULE_HEADER,\n                                   \"ext1\",\n                                   NULL, /* Functions */\n                                   NULL, /* MINIT */\n                                   NULL, /* MSHUTDOWN */\n                                   NULL, /* RINIT */\n                                   NULL, /* RSHUTDOWN */\n                                   NULL, /* MINFO */\n                                   \"0.1.0\",\n                                   STANDARD_MODULE_PROPERTIES};\n\nzend_module_entry module2_entry = {STANDARD_MODULE_HEADER,\n                                   \"ext2\",\n                                   NULL, /* Functions */\n                                   NULL, /* MINIT */\n                                   NULL, /* MSHUTDOWN */\n                                   NULL, /* RINIT */\n                                   NULL, /* RSHUTDOWN */\n                                   NULL, /* MINFO */\n                                   \"0.1.0\",\n                                   STANDARD_MODULE_PROPERTIES};\n"
  },
  {
    "path": "internal/testext/exttest.go",
    "content": "package testext\n\n// #cgo darwin pkg-config: libxml-2.0\n// #cgo unix CFLAGS: -Wall -Werror\n// #cgo unix CFLAGS: -I/usr/local/include -I/usr/local/include/php -I/usr/local/include/php/main -I/usr/local/include/php/TSRM -I/usr/local/include/php/Zend -I/usr/local/include/php/ext -I/usr/local/include/php/ext/date/lib\n// #cgo linux CFLAGS: -D_GNU_SOURCE\n// #cgo darwin CFLAGS: -I/opt/homebrew/include\n// #cgo unix LDFLAGS: -L/usr/local/lib -L/usr/lib -lphp -lm -lutil\n// #cgo linux LDFLAGS: -ldl -lresolv\n// #cgo darwin LDFLAGS: -Wl,-rpath,/usr/local/lib -L/opt/homebrew/lib -L/opt/homebrew/opt/libiconv/lib -liconv -ldl\n// #cgo windows CFLAGS: -D_WINDOWS -DWINDOWS=1 -DZEND_WIN32=1 -DPHP_WIN32=1 -DWIN32 -D_MBCS -D_USE_MATH_DEFINES -DNDebug -DNDEBUG -DZEND_DEBUG=0 -DZTS=1 -DFD_SETSIZE=256\n// #include \"extension.h\"\nimport \"C\"\nimport (\n\t\"io\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\t\"unsafe\"\n\n\t\"github.com/dunglas/frankenphp\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc testRegisterExtension(t *testing.T) {\n\tfrankenphp.RegisterExtension(unsafe.Pointer(&C.module1_entry))\n\tfrankenphp.RegisterExtension(unsafe.Pointer(&C.module2_entry))\n\n\terr := frankenphp.Init()\n\trequire.Nil(t, err)\n\tdefer frankenphp.Shutdown()\n\n\treq := httptest.NewRequest(\"GET\", \"http://example.com/index.php\", nil)\n\tw := httptest.NewRecorder()\n\n\treq, err = frankenphp.NewRequestWithContext(req, frankenphp.WithRequestDocumentRoot(\"./testdata\", false))\n\tassert.NoError(t, err)\n\n\terr = frankenphp.ServeHTTP(w, req)\n\tassert.NoError(t, err)\n\n\tresp := w.Result()\n\tbody, _ := io.ReadAll(resp.Body)\n\tassert.Contains(t, string(body), \"ext1\")\n\tassert.Contains(t, string(body), \"ext2\")\n}\n"
  },
  {
    "path": "internal/testext/testdata/index.php",
    "content": "<?php\n\nprint_r(get_loaded_extensions());\n"
  },
  {
    "path": "internal/testserver/main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"os\"\n\n\t\"github.com/dunglas/frankenphp\"\n)\n\nfunc main() {\n\tctx := context.Background()\n\tlogger := slog.New(slog.NewTextHandler(os.Stdout, nil))\n\n\tif err := frankenphp.Init(frankenphp.WithContext(ctx), frankenphp.WithLogger(logger)); err != nil {\n\t\tpanic(err)\n\t}\n\tdefer frankenphp.Shutdown()\n\n\thttp.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) {\n\t\treq, err := frankenphp.NewRequestWithContext(r)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\n\t\tif err := frankenphp.ServeHTTP(w, req); err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t})\n\n\tport := os.Getenv(\"PORT\")\n\tif port == \"\" {\n\t\tport = \"8080\"\n\t}\n\n\tif logger.Enabled(ctx, slog.LevelError) {\n\t\tlogger.LogAttrs(ctx, slog.LevelError, \"server error\", slog.Any(\"error\", http.ListenAndServe(\":\"+port, nil)))\n\t}\n\n\tos.Exit(1)\n}\n"
  },
  {
    "path": "internal/watcher/pattern.go",
    "content": "//go:build !nowatcher\n\npackage watcher\n\nimport (\n\t\"log/slog\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/dunglas/frankenphp/internal/fastabs\"\n\t\"github.com/e-dant/watcher/watcher-go\"\n)\n\nconst sep = string(filepath.Separator)\n\ntype pattern struct {\n\tpatternGroup *PatternGroup\n\tvalue        string\n\tparsedValues []string\n\tevents       chan eventHolder\n\tfailureCount int\n\n\twatcher *watcher.Watcher\n}\n\nfunc (p *pattern) startSession() {\n\tp.watcher = watcher.NewWatcher(p.value, p.handle)\n\n\tif globalLogger.Enabled(globalCtx, slog.LevelDebug) {\n\t\tglobalLogger.LogAttrs(globalCtx, slog.LevelDebug, \"watching\", slog.String(\"pattern\", p.value))\n\t}\n}\n\n// this method prepares the pattern struct (aka /path/*pattern)\nfunc (p *pattern) parse() (err error) {\n\t// first we clean the value\n\tabsPattern, err := fastabs.FastAbs(p.value)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tp.value = absPattern\n\n\tvolumeName := filepath.VolumeName(p.value)\n\tp.value = strings.TrimPrefix(p.value, volumeName)\n\n\t// then we split the pattern to determine where the directory ends and the pattern starts\n\tsplitPattern := strings.Split(p.value, sep)\n\tpatternWithoutDir := \"\"\n\tfor i, part := range splitPattern {\n\t\tisFilename := i == len(splitPattern)-1 && strings.Contains(part, \".\")\n\t\tisGlobCharacter := strings.ContainsAny(part, \"[*?{\")\n\n\t\tif isFilename || isGlobCharacter {\n\t\t\tpatternWithoutDir = filepath.Join(splitPattern[i:]...)\n\t\t\tp.value = filepath.Join(splitPattern[:i]...)\n\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// now we split the pattern according to the recursive '**' syntax\n\tp.parsedValues = strings.Split(patternWithoutDir, \"**\")\n\tfor i, pp := range p.parsedValues {\n\t\tp.parsedValues[i] = strings.Trim(pp, sep)\n\t}\n\n\t//  remove the trailing separator and add leading separator (except on Windows)\n\tif volumeName == \"\" {\n\t\tp.value = sep + strings.Trim(p.value, sep)\n\t} else {\n\t\tp.value = volumeName + sep + strings.Trim(p.value, sep)\n\t}\n\n\t// try to canonicalize the path\n\tcanonicalPattern, err := filepath.EvalSymlinks(p.value)\n\tif err == nil {\n\t\tp.value = canonicalPattern\n\t}\n\n\treturn nil\n}\n\nfunc (p *pattern) allowReload(event *watcher.Event) bool {\n\tif !isValidEventType(event.EffectType) || !isValidPathType(event) {\n\t\treturn false\n\t}\n\n\t// some editors create temporary files and never actually modify the original file\n\t// so we need to also check Event.AssociatedPathName\n\t// see https://github.com/php/frankenphp/issues/1375\n\treturn p.isValidPattern(event.PathName) || p.isValidPattern(event.AssociatedPathName)\n}\n\nfunc (p *pattern) handle(event *watcher.Event) {\n\t// If the watcher prematurely sends the die@ event, retry watching\n\tif event.PathType == watcher.PathTypeWatcher && strings.HasPrefix(event.PathName, \"e/self/die@\") && watcherIsActive.Load() {\n\t\tp.retryWatching()\n\n\t\treturn\n\t}\n\n\tif p.allowReload(event) {\n\t\tp.events <- eventHolder{p.patternGroup, event}\n\t}\n}\n\nfunc (p *pattern) stop() {\n\tp.watcher.Close()\n}\n\nfunc isValidEventType(effectType watcher.EffectType) bool {\n\treturn effectType <= watcher.EffectTypeDestroy\n}\n\nfunc isValidPathType(event *watcher.Event) bool {\n\tif event.PathType == watcher.PathTypeWatcher && globalLogger.Enabled(globalCtx, slog.LevelDebug) {\n\t\tglobalLogger.LogAttrs(globalCtx, slog.LevelDebug, \"special e-dant/watcher event\", slog.Any(\"event\", event))\n\t}\n\n\treturn event.PathType <= watcher.PathTypeHardLink\n}\n\nfunc (p *pattern) isValidPattern(fileName string) bool {\n\tif fileName == \"\" {\n\t\treturn false\n\t}\n\n\t// first we remove the dir from the file name\n\tif !strings.HasPrefix(fileName, p.value) {\n\t\treturn false\n\t}\n\n\t// remove the directory path and separator from the filename\n\tfileNameWithoutDir := strings.TrimPrefix(strings.TrimPrefix(fileName, p.value), sep)\n\n\t// if the pattern has size 1 we can match it directly against the filename\n\tif len(p.parsedValues) == 1 {\n\t\treturn matchCurlyBracePattern(p.parsedValues[0], fileNameWithoutDir)\n\t}\n\n\treturn p.matchPatterns(fileNameWithoutDir)\n}\n\nfunc (p *pattern) matchPatterns(fileName string) bool {\n\tpartsToMatch := strings.Split(fileName, sep)\n\tcursor := 0\n\n\t// if there are multiple parsedValues due to '**' we need to match them individually\n\tfor i, pattern := range p.parsedValues {\n\t\tpatternSize := strings.Count(pattern, sep) + 1\n\n\t\t// if we are at the last pattern we will start matching from the end of the filename\n\t\tif i == len(p.parsedValues)-1 {\n\t\t\tcursor = len(partsToMatch) - patternSize\n\n\t\t\tif cursor < 0 {\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\n\t\t// the cursor will move through the fileName until the pattern matches\n\t\tfor j := cursor; j < len(partsToMatch); j++ {\n\t\t\tif j+patternSize > len(partsToMatch) {\n\t\t\t\treturn false\n\t\t\t}\n\n\t\t\tcursor = j\n\t\t\tsubPattern := strings.Join(partsToMatch[j:j+patternSize], sep)\n\n\t\t\tif matchCurlyBracePattern(pattern, subPattern) {\n\t\t\t\tcursor = j + patternSize - 1\n\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tif cursor > len(partsToMatch)-patternSize-1 {\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t}\n\n\treturn true\n}\n\n// we also check for the following syntax: /path/*.{php,twig,yaml}\nfunc matchCurlyBracePattern(pattern string, fileName string) bool {\n\tfor _, subPattern := range expandCurlyBraces(pattern) {\n\t\tif matchPattern(subPattern, fileName) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// {dir1,dir2}/path -> []string{\"dir1/path\", \"dir2/path\"}\nfunc expandCurlyBraces(s string) []string {\n\tbefore, rest, found := strings.Cut(s, \"{\")\n\tif !found {\n\t\treturn []string{s}\n\t}\n\n\tinside, after, found := strings.Cut(rest, \"}\")\n\tif !found {\n\t\treturn []string{s} // no closing brace\n\t}\n\n\tvar out []string\n\tfor _, subPattern := range strings.Split(inside, \",\") {\n\t\tout = append(out, expandCurlyBraces(before+subPattern+after)...)\n\t}\n\n\treturn out\n}\n\nfunc matchPattern(pattern string, fileName string) bool {\n\tif pattern == \"\" {\n\t\treturn true\n\t}\n\n\tpatternMatches, err := filepath.Match(pattern, fileName)\n\n\tif err != nil {\n\t\tif globalLogger.Enabled(globalCtx, slog.LevelError) {\n\t\t\tglobalLogger.LogAttrs(globalCtx, slog.LevelError, \"failed to match filename\", slog.String(\"file\", fileName), slog.Any(\"error\", err))\n\t\t}\n\n\t\treturn false\n\t}\n\n\treturn patternMatches\n}\n"
  },
  {
    "path": "internal/watcher/pattern_test.go",
    "content": "//go:build !nowatcher\n\npackage watcher\n\nimport (\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/e-dant/watcher/watcher-go\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc normalizePath(t *testing.T, path string) string {\n\tt.Helper()\n\n\tif filepath.Separator == '/' {\n\t\treturn path\n\t}\n\n\tpath = filepath.FromSlash(path)\n\tif strings.HasPrefix(path, \"\\\\\") {\n\t\tpath = \"C:\\\\\" + path[1:]\n\t}\n\n\treturn path\n}\n\nfunc newPattern(t *testing.T, value string) pattern {\n\tt.Helper()\n\n\tp := pattern{value: normalizePath(t, value)}\n\trequire.NoError(t, p.parse())\n\n\treturn p\n}\n\nfunc TestDisallowOnEventTypeBiggerThan3(t *testing.T) {\n\tt.Parallel()\n\n\tw := newPattern(t, \"/some/path\")\n\n\tassert.False(t, w.allowReload(&watcher.Event{PathName: \"/some/path/watch-me.php\", EffectType: watcher.EffectTypeOwner}))\n}\n\nfunc TestDisallowOnPathTypeBiggerThan2(t *testing.T) {\n\tt.Parallel()\n\n\tw := newPattern(t, \"/some/path\")\n\n\tassert.False(t, w.allowReload(&watcher.Event{PathName: \"/some/path/watch-me.php\", PathType: watcher.PathTypeSymLink}))\n}\n\nfunc TestWatchesCorrectDir(t *testing.T) {\n\tt.Parallel()\n\n\tdata := []struct {\n\t\tpattern string\n\t\tdir     string\n\t}{\n\t\t{\"/path\", \"/path\"},\n\t\t{\"/path/\", \"/path\"},\n\t\t{\"/path/**/*.php\", \"/path\"},\n\t\t{\"/path/*.php\", \"/path\"},\n\t\t{\"/path/*/*.php\", \"/path\"},\n\t\t{\"/path/?path/*.php\", \"/path\"},\n\t\t{\"/path/{dir1,dir2}/**/*.php\", \"/path\"},\n\t\t{\".\", relativeDir(t, \"\")},\n\t\t{\"./\", relativeDir(t, \"\")},\n\t\t{\"./**\", relativeDir(t, \"\")},\n\t\t{\"..\", relativeDir(t, \"/..\")},\n\t}\n\n\tfor _, d := range data {\n\t\tt.Run(d.pattern, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\thasDir(t, d.pattern, d.dir)\n\t\t})\n\t}\n}\n\nfunc TestValidRecursiveDirectories(t *testing.T) {\n\tt.Parallel()\n\n\tdata := []struct {\n\t\tpattern string\n\t\tfile    string\n\t}{\n\t\t{\"/path\", \"/path/file.php\"},\n\t\t{\"/path\", \"/path/subpath/file.php\"},\n\t\t{\"/path/\", \"/path/subpath/file.php\"},\n\t\t{\"/path**\", \"/path/subpath/file.php\"},\n\t\t{\"/path/**\", \"/path/subpath/file.php\"},\n\t\t{\"/path/**/\", \"/path/subpath/file.php\"},\n\t\t{\".\", relativeDir(t, \"file.php\")},\n\t\t{\".\", relativeDir(t, \"subpath/file.php\")},\n\t\t{\"./**\", relativeDir(t, \"subpath/file.php\")},\n\t\t{\"..\", relativeDir(t, \"subpath/file.php\")},\n\t}\n\n\tfor _, d := range data {\n\t\tt.Run(d.pattern, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tassertPatternMatch(t, d.pattern, d.file)\n\t\t})\n\t}\n}\n\nfunc TestInvalidRecursiveDirectories(t *testing.T) {\n\tt.Parallel()\n\n\tdata := []struct {\n\t\tpattern string\n\t\tdir     string\n\t}{\n\t\t{\"/path\", \"/other/file.php\"},\n\t\t{\"/path/**\", \"/other/file.php\"},\n\t\t{\".\", \"/other/file.php\"},\n\t}\n\n\tfor _, d := range data {\n\t\tt.Run(d.pattern, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tassertPatternNotMatch(t, d.pattern, d.dir)\n\t\t})\n\t}\n}\n\nfunc TestValidNonRecursiveFilePatterns(t *testing.T) {\n\tt.Parallel()\n\n\tdata := []struct {\n\t\tpattern string\n\t\tdir     string\n\t}{\n\t\t{\"/*.php\", \"/file.php\"},\n\t\t{\"/path/*.php\", \"/path/file.php\"},\n\t\t{\"/path/?ile.php\", \"/path/file.php\"},\n\t\t{\"/path/file.php\", \"/path/file.php\"},\n\t\t{\"*.php\", relativeDir(t, \"file.php\")},\n\t\t{\"./*.php\", relativeDir(t, \"file.php\")},\n\t}\n\n\tfor _, d := range data {\n\t\tt.Run(d.pattern, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tassertPatternMatch(t, d.pattern, d.dir)\n\t\t})\n\t}\n}\n\nfunc TestInValidNonRecursiveFilePatterns(t *testing.T) {\n\tt.Parallel()\n\n\tdata := []struct {\n\t\tpattern string\n\t\tdir     string\n\t}{\n\t\t{\"/path/*.txt\", \"/path/file.php\"},\n\t\t{\"/path/*.php\", \"/path/subpath/file.php\"},\n\t\t{\"/*.php\", \"/path/file.php\"},\n\t\t{\"*.txt\", relativeDir(t, \"file.php\")},\n\t\t{\"*.php\", relativeDir(t, \"subpath/file.php\")},\n\t}\n\n\tfor _, d := range data {\n\t\tt.Run(d.pattern, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tassertPatternNotMatch(t, d.pattern, d.dir)\n\t\t})\n\t}\n}\n\nfunc TestValidRecursiveFilePatterns(t *testing.T) {\n\tt.Parallel()\n\n\tdata := []struct {\n\t\tpattern string\n\t\tdir     string\n\t}{\n\t\t{\"/path/**/*.php\", \"/path/file.php\"},\n\t\t{\"/path/**/*.php\", \"/path/subpath/file.php\"},\n\t\t{\"/path/**/?ile.php\", \"/path/subpath/file.php\"},\n\t\t{\"/path/**/file.php\", \"/path/subpath/file.php\"},\n\t\t{\"**/*.php\", relativeDir(t, \"file.php\")},\n\t\t{\"**/*.php\", relativeDir(t, \"subpath/file.php\")},\n\t\t{\"./**/*.php\", relativeDir(t, \"subpath/file.php\")},\n\t}\n\n\tfor _, d := range data {\n\t\tt.Run(d.pattern, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tassertPatternMatch(t, d.pattern, d.dir)\n\t\t})\n\t}\n}\n\nfunc TestInvalidRecursiveFilePatterns(t *testing.T) {\n\tt.Parallel()\n\n\tdata := []struct {\n\t\tpattern string\n\t\tdir     string\n\t}{\n\t\t{\"/path/**/*.txt\", \"/path/file.php\"},\n\t\t{\"/path/**/*.txt\", \"/other/file.php\"},\n\t\t{\"/path/**/*.txt\", \"/path/subpath/file.php\"},\n\t\t{\"/path/**/?ilm.php\", \"/path/subpath/file.php\"},\n\t\t{\"**/*.php\", \"/other/file.php\"},\n\t\t{\".**/*.php\", \"/other/file.php\"},\n\t\t{\"./**/*.php\", \"/other/file.php\"},\n\t\t{\"/a/**/very/long/path.php\", \"/a/short.php\"},\n\t\t{\"\", \"\"},\n\t\t{\"/a/**/b/c/d/**/e.php\", \"/a/x/e.php\"},\n\t}\n\n\tfor _, d := range data {\n\t\tt.Run(d.pattern, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tassertPatternNotMatch(t, d.pattern, d.dir)\n\t\t})\n\t}\n}\n\nfunc TestValidDirectoryPatterns(t *testing.T) {\n\tt.Parallel()\n\n\tdata := []struct {\n\t\tpattern string\n\t\tdir     string\n\t}{\n\t\t{\"/path/*/*.php\", \"/path/subpath/file.php\"},\n\t\t{\"/path/*/*/*.php\", \"/path/subpath/subpath/file.php\"},\n\t\t{\"/path/?/*.php\", \"/path/1/file.php\"},\n\t\t{\"/path/**/vendor/*.php\", \"/path/vendor/file.php\"},\n\t\t{\"/path/**/vendor/*.php\", \"/path/subpath/vendor/file.php\"},\n\t\t{\"/path/**/vendor/**/*.php\", \"/path/vendor/file.php\"},\n\t\t{\"/path/**/vendor/**/*.php\", \"/path/subpath/subpath/vendor/subpath/subpath/file.php\"},\n\t\t{\"/path/**/vendor/*/*.php\", \"/path/subpath/subpath/vendor/subpath/file.php\"},\n\t\t{\"/path*/path*/*\", \"/path1/path2/file.php\"},\n\t}\n\n\tfor _, d := range data {\n\t\tt.Run(d.pattern, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tassertPatternMatch(t, d.pattern, d.dir)\n\t\t})\n\t}\n}\n\nfunc TestInvalidDirectoryPatterns(t *testing.T) {\n\tt.Parallel()\n\n\tdata := []struct {\n\t\tpattern string\n\t\tdir     string\n\t}{\n\t\t{\"/path/subpath/*.php\", \"/path/other/file.php\"},\n\t\t{\"/path/*/*.php\", \"/path/subpath/subpath/file.php\"},\n\t\t{\"/path/?/*.php\", \"/path/subpath/file.php\"},\n\t\t{\"/path/*/*/*.php\", \"/path/subpath/file.php\"},\n\t\t{\"/path/*/*/*.php\", \"/path/subpath/subpath/subpath/file.php\"},\n\t\t{\"/path/**/vendor/*.php\", \"/path/subpath/vendor/subpath/file.php\"},\n\t\t{\"/path/**/vendor/*.php\", \"/path/subpath/file.php\"},\n\t\t{\"/path/**/vendor/**/*.php\", \"/path/subpath/file.php\"},\n\t\t{\"/path/**/vendor/**/*.txt\", \"/path/subpath/vendor/subpath/file.php\"},\n\t\t{\"/path/**/vendor/**/*.php\", \"/path/subpath/subpath/subpath/file.php\"},\n\t\t{\"/path/**/vendor/*/*.php\", \"/path/subpath/vendor/subpath/subpath/file.php\"},\n\t\t{\"/path*/path*\", \"/path1/path1/file.php\"},\n\t}\n\n\tfor _, d := range data {\n\t\tt.Run(d.pattern, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tassertPatternNotMatch(t, d.pattern, d.dir)\n\t\t})\n\t}\n}\n\nfunc TestValidCurlyBracePatterns(t *testing.T) {\n\tt.Parallel()\n\n\tdata := []struct {\n\t\tpattern string\n\t\tfile    string\n\t}{\n\t\t{\"/path/*.{php}\", \"/path/file.php\"},\n\t\t{\"/path/*.{php,twig}\", \"/path/file.php\"},\n\t\t{\"/path/*.{php,twig}\", \"/path/file.twig\"},\n\t\t{\"/path/**/{file.php,file.twig}\", \"/path/subpath/file.twig\"},\n\t\t{\"/path/{dir1,dir2}/file.php\", \"/path/dir1/file.php\"},\n\t\t{\"/path/{dir1,dir2}/file.php\", \"/path/dir2/file.php\"},\n\t\t{\"/app/{app,config,resources}/**/*.php\", \"/app/app/subpath/file.php\"},\n\t\t{\"/app/{app,config,resources}/**/*.php\", \"/app/config/subpath/file.php\"},\n\t\t{\"/path/{dir1,dir2}/{a,b}{a,b}.php\", \"/path/dir1/ab.php\"},\n\t\t{\"/path/{dir1,dir2}/{a,b}{a,b}.php\", \"/path/dir2/aa.php\"},\n\t\t{\"/path/{dir1,dir2}/{a,b}{a,b}.php\", \"/path/dir2/bb.php\"},\n\t\t{\"/path/{dir1/test.php,dir2/test.php}\", \"/path/dir1/test.php\"},\n\t}\n\n\tfor _, d := range data {\n\t\tt.Run(d.pattern, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tassertPatternMatch(t, d.pattern, d.file)\n\t\t})\n\t}\n}\n\nfunc TestInvalidCurlyBracePatterns(t *testing.T) {\n\tt.Parallel()\n\n\tdata := []struct {\n\t\tpattern string\n\t\tdir     string\n\t}{\n\t\t{\"/path/*.{php}\", \"/path/file.txt\"},\n\t\t{\"/path/*.{php,twig}\", \"/path/file.txt\"},\n\t\t{\"/path/{file.php,file.twig}\", \"/path/file.txt\"},\n\t\t{\"/path/{dir1,dir2}/file.php\", \"/path/dir3/file.php\"},\n\t\t{\"/path/{dir1,dir2}/**/*.php\", \"/path/dir1/subpath/file.txt\"},\n\t\t{\"/path/{dir1,dir2}/{a,b}{a,b}.php\", \"/path/dir1/ac.php\"},\n\t\t{\"/path/{}/{a,b}{a,b}.php\", \"/path/dir1/ac.php\"},\n\t\t{\"/path/}dir{/{a,b}{a,b}.php\", \"/path/dir1/aa.php\"},\n\t}\n\n\tfor _, d := range data {\n\t\tt.Run(d.pattern, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tassertPatternNotMatch(t, d.pattern, d.dir)\n\t\t})\n\t}\n}\n\nfunc TestAnAssociatedEventTriggersTheWatcher(t *testing.T) {\n\tt.Parallel()\n\n\tw := newPattern(t, \"/**/*.php\")\n\tw.events = make(chan eventHolder)\n\n\te := &watcher.Event{PathName: normalizePath(t, \"/path/temporary_file\"), AssociatedPathName: normalizePath(t, \"/path/file.php\")}\n\tgo w.handle(e)\n\n\tassert.Equal(t, e, (<-w.events).event)\n}\n\nfunc relativeDir(t *testing.T, relativePath string) string {\n\tt.Helper()\n\n\tdir, err := filepath.Abs(\"./\" + relativePath)\n\tassert.NoError(t, err)\n\n\treturn dir\n}\n\nfunc hasDir(t *testing.T, p string, dir string) {\n\tt.Helper()\n\n\tw := newPattern(t, p)\n\n\tassert.Equal(t, normalizePath(t, dir), w.value)\n}\n\nfunc assertPatternMatch(t *testing.T, p, fileName string) {\n\tt.Helper()\n\n\tw := newPattern(t, p)\n\n\tassert.True(t, w.allowReload(&watcher.Event{PathName: normalizePath(t, fileName)}))\n}\n\nfunc assertPatternNotMatch(t *testing.T, p, fileName string) {\n\tt.Helper()\n\n\tw := newPattern(t, p)\n\n\tassert.False(t, w.allowReload(&watcher.Event{PathName: normalizePath(t, fileName)}))\n}\n"
  },
  {
    "path": "internal/watcher/watcher.go",
    "content": "//go:build !nowatcher\n\npackage watcher\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"log/slog\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/e-dant/watcher/watcher-go\"\n)\n\nconst (\n\t// duration to wait before triggering a reload after a file change\n\tdebounceDuration = 150 * time.Millisecond\n\t// times to retry watching if the watcher was closed prematurely\n\tmaxFailureCount      = 5\n\tfailureResetDuration = 5 * time.Second\n)\n\nvar (\n\tErrAlreadyStarted = errors.New(\"watcher is already running\")\n\n\tfailureMu       sync.Mutex\n\twatcherIsActive atomic.Bool\n\n\t// the currently active file watcher\n\tactiveWatcher *globalWatcher\n\t// after stopping the watcher we will wait for eventual reloads to finish\n\treloadWaitGroup sync.WaitGroup\n\t// we are passing the context from the main package to the watcher\n\tglobalCtx context.Context\n\t// we are passing the globalLogger from the main package to the watcher\n\tglobalLogger *slog.Logger\n)\n\ntype PatternGroup struct {\n\tPatterns []string\n\tCallback func([]*watcher.Event)\n}\n\ntype eventHolder struct {\n\tpatternGroup *PatternGroup\n\tevent        *watcher.Event\n}\n\ntype globalWatcher struct {\n\tgroups   []*PatternGroup\n\twatchers []*pattern\n\tevents   chan eventHolder\n\tstop     chan struct{}\n}\n\nfunc InitWatcher(ct context.Context, slogger *slog.Logger, groups []*PatternGroup) error {\n\tif len(groups) == 0 {\n\t\treturn nil\n\t}\n\n\tif watcherIsActive.Load() {\n\t\treturn ErrAlreadyStarted\n\t}\n\n\twatcherIsActive.Store(true)\n\tglobalCtx = ct\n\tglobalLogger = slogger\n\n\tactiveWatcher = &globalWatcher{groups: groups}\n\n\tfor _, g := range groups {\n\t\tif len(g.Patterns) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, p := range g.Patterns {\n\t\t\tactiveWatcher.watchers = append(activeWatcher.watchers, &pattern{patternGroup: g, value: p})\n\t\t}\n\t}\n\n\tif err := activeWatcher.startWatching(); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc DrainWatcher() {\n\tif !watcherIsActive.Load() {\n\t\treturn\n\t}\n\n\twatcherIsActive.Store(false)\n\n\tif globalLogger.Enabled(globalCtx, slog.LevelDebug) {\n\t\tglobalLogger.LogAttrs(globalCtx, slog.LevelDebug, \"stopping watcher\")\n\t}\n\n\tactiveWatcher.stopWatching()\n\treloadWaitGroup.Wait()\n\tactiveWatcher = nil\n}\n\n// TODO: how to test this?\nfunc (p *pattern) retryWatching() {\n\tfailureMu.Lock()\n\tdefer failureMu.Unlock()\n\n\tif p.failureCount >= maxFailureCount {\n\t\tif globalLogger.Enabled(globalCtx, slog.LevelWarn) {\n\t\t\tglobalLogger.LogAttrs(globalCtx, slog.LevelWarn, \"giving up watching\", slog.String(\"pattern\", p.value))\n\t\t}\n\n\t\treturn\n\t}\n\n\tif globalLogger.Enabled(globalCtx, slog.LevelInfo) {\n\t\tglobalLogger.LogAttrs(globalCtx, slog.LevelInfo, \"watcher was closed prematurely, retrying...\", slog.String(\"pattern\", p.value))\n\t}\n\n\tp.failureCount++\n\n\tp.startSession()\n\n\t// reset the failure-count if the watcher hasn't reached max failures after 5 seconds\n\tgo func() {\n\t\ttime.Sleep(failureResetDuration)\n\n\t\tfailureMu.Lock()\n\t\tif p.failureCount < maxFailureCount {\n\t\t\tp.failureCount = 0\n\t\t}\n\t\tfailureMu.Unlock()\n\t}()\n}\n\nfunc (g *globalWatcher) startWatching() error {\n\tg.events = make(chan eventHolder)\n\tg.stop = make(chan struct{})\n\n\tif err := g.parseFilePatterns(); err != nil {\n\t\treturn err\n\t}\n\n\tfor _, w := range g.watchers {\n\t\tw.events = g.events\n\t\tw.startSession()\n\t}\n\n\tgo g.listenForFileEvents()\n\n\treturn nil\n}\n\nfunc (g *globalWatcher) parseFilePatterns() error {\n\tfor _, w := range g.watchers {\n\t\tif err := w.parse(); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (g *globalWatcher) stopWatching() {\n\tclose(g.stop)\n\tfor _, w := range g.watchers {\n\t\tw.stop()\n\t}\n}\n\nfunc (g *globalWatcher) listenForFileEvents() {\n\ttimer := time.NewTimer(debounceDuration)\n\ttimer.Stop()\n\n\teventsPerGroup := make(map[*PatternGroup][]*watcher.Event, len(g.groups))\n\n\tdefer timer.Stop()\n\tfor {\n\t\tselect {\n\t\tcase <-g.stop:\n\t\t\treturn\n\t\tcase eh := <-g.events:\n\t\t\ttimer.Reset(debounceDuration)\n\n\t\t\teventsPerGroup[eh.patternGroup] = append(eventsPerGroup[eh.patternGroup], eh.event)\n\t\tcase <-timer.C:\n\t\t\ttimer.Stop()\n\n\t\t\tif globalLogger.Enabled(globalCtx, slog.LevelInfo) {\n\t\t\t\tvar events []*watcher.Event\n\t\t\t\tfor _, eventList := range eventsPerGroup {\n\t\t\t\t\tevents = append(events, eventList...)\n\t\t\t\t}\n\n\t\t\t\tglobalLogger.LogAttrs(globalCtx, slog.LevelInfo, \"filesystem changes detected\", slog.Any(\"events\", events))\n\t\t\t}\n\n\t\t\tg.scheduleReload(eventsPerGroup)\n\t\t\teventsPerGroup = make(map[*PatternGroup][]*watcher.Event, len(g.groups))\n\t\t}\n\t}\n}\n\nfunc (g *globalWatcher) scheduleReload(eventsPerGroup map[*PatternGroup][]*watcher.Event) {\n\treloadWaitGroup.Add(1)\n\n\t// Call callbacks in order\n\tfor _, g := range g.groups {\n\t\tif len(g.Patterns) == 0 {\n\t\t\tg.Callback(nil)\n\t\t}\n\n\t\tif e, ok := eventsPerGroup[g]; ok {\n\t\t\tg.Callback(e)\n\t\t}\n\t}\n\n\treloadWaitGroup.Done()\n}\n"
  },
  {
    "path": "log_test.go",
    "content": "package frankenphp_test\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"sync\"\n\t\"testing\"\n)\n\nfunc newTestLogger(t *testing.T) (*slog.Logger, fmt.Stringer) {\n\tt.Helper()\n\n\tvar buf syncBuffer\n\n\treturn slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug})), &buf\n}\n\n// SyncBuffer is a thread-safe buffer for capturing logs in tests.\ntype syncBuffer struct {\n\tb  bytes.Buffer\n\tmu sync.RWMutex\n}\n\nfunc (s *syncBuffer) Write(p []byte) (n int, err error) {\n\ts.mu.Lock()\n\tdefer s.mu.Unlock()\n\n\treturn s.b.Write(p)\n}\n\nfunc (s *syncBuffer) String() string {\n\ts.mu.RLock()\n\tdefer s.mu.RUnlock()\n\n\treturn s.b.String()\n}\n"
  },
  {
    "path": "mercure-skip.go",
    "content": "//go:build nomercure\n\npackage frankenphp\n\n// #include <stdint.h>\n// #include <php.h>\nimport \"C\"\n\ntype mercureContext struct {\n}\n\n//export go_mercure_publish\nfunc go_mercure_publish(threadIndex C.uintptr_t, topics *C.struct__zval_struct, data *C.zend_string, private bool, id, typ *C.zend_string, retry uint64) (generatedID *C.zend_string, error C.short) {\n\treturn nil, 3\n}\n\nfunc (w *worker) configureMercure(_ *workerOpt) {\n}\n"
  },
  {
    "path": "mercure.go",
    "content": "//go:build !nomercure\n\npackage frankenphp\n\n// #include <stdint.h>\n// #include \"frankenphp.h\"\n// #include <php.h>\nimport \"C\"\nimport (\n\t\"log/slog\"\n\t\"unsafe\"\n\n\t\"github.com/dunglas/mercure\"\n)\n\ntype mercureContext struct {\n\tmercureHub *mercure.Hub\n}\n\n//export go_mercure_publish\nfunc go_mercure_publish(threadIndex C.uintptr_t, topics *C.struct__zval_struct, data *C.zend_string, private bool, id, typ *C.zend_string, retry uint64) (generatedID *C.zend_string, error C.short) {\n\tthread := phpThreads[threadIndex]\n\tctx := thread.context()\n\tfc := thread.frankenPHPContext()\n\n\tif fc.mercureHub == nil {\n\t\tif fc.logger.Enabled(ctx, slog.LevelError) {\n\t\t\tfc.logger.LogAttrs(ctx, slog.LevelError, \"No Mercure hub configured\")\n\t\t}\n\n\t\treturn nil, 1\n\t}\n\n\tu := &mercure.Update{\n\t\tEvent: mercure.Event{\n\t\t\tData:  GoString(unsafe.Pointer(data)),\n\t\t\tID:    GoString(unsafe.Pointer(id)),\n\t\t\tRetry: retry,\n\t\t\tType:  GoString(unsafe.Pointer(typ)),\n\t\t},\n\t\tPrivate: private,\n\t\tDebug:   fc.logger.Enabled(ctx, slog.LevelDebug),\n\t}\n\n\tzvalType := C.zval_get_type(topics)\n\tswitch zvalType {\n\tcase C.IS_STRING:\n\t\tu.Topics = []string{GoString(unsafe.Pointer(*(**C.zend_string)(unsafe.Pointer(&topics.value[0]))))}\n\tcase C.IS_ARRAY:\n\t\tts, err := GoPackedArray[string](unsafe.Pointer(*(**C.zend_array)(unsafe.Pointer(&topics.value[0]))))\n\t\tif err != nil {\n\t\t\tif fc.logger.Enabled(ctx, slog.LevelError) {\n\t\t\t\tfc.logger.LogAttrs(ctx, slog.LevelError, \"invalid topics type\", slog.Any(\"error\", err))\n\t\t\t}\n\n\t\t\treturn nil, 1\n\t\t}\n\n\t\tu.Topics = ts\n\tdefault:\n\t\t// Never happens as the function is called from C with proper types\n\t\tpanic(\"invalid topics type\")\n\t}\n\n\tif err := fc.mercureHub.Publish(ctx, u); err != nil {\n\t\tif fc.logger.Enabled(ctx, slog.LevelError) {\n\t\t\tfc.logger.LogAttrs(ctx, slog.LevelError, \"Unable to publish Mercure update\", slog.Any(\"error\", err))\n\t\t}\n\n\t\treturn nil, 2\n\t}\n\n\treturn (*C.zend_string)(PHPString(u.ID, false)), 0\n}\n\nfunc (w *worker) configureMercure(o *workerOpt) {\n\tif o.mercureHub == nil {\n\t\treturn\n\t}\n\n\tw.mercureHub = o.mercureHub\n}\n\n// WithMercureHub sets the mercure.Hub to use to publish updates\nfunc WithMercureHub(hub *mercure.Hub) RequestOption {\n\treturn func(o *frankenPHPContext) error {\n\t\to.mercureHub = hub\n\n\t\treturn nil\n\t}\n}\n\n// WithWorkerMercureHub sets the mercure.Hub in the worker script and used to dispatch hot reloading-related mercure.Update.\nfunc WithWorkerMercureHub(hub *mercure.Hub) WorkerOption {\n\treturn func(w *workerOpt) error {\n\t\tw.mercureHub = hub\n\n\t\tw.requestOptions = append(w.requestOptions, WithMercureHub(hub))\n\n\t\treturn nil\n\t}\n}\n"
  },
  {
    "path": "mercure_test.go",
    "content": "//go:build !nomercure\n\npackage frankenphp_test\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/dunglas/frankenphp\"\n\t\"github.com/dunglas/mercure\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestMercurePublish_module(t *testing.T) { testMercurePublish(t, &testOptions{}) }\nfunc TestMercurePublish_worker(t *testing.T) {\n\ttestMercurePublish(t, &testOptions{workerScript: \"index.php\"})\n}\nfunc testMercurePublish(t *testing.T, opts *testOptions) {\n\th, err := mercure.NewHub(t.Context(), mercure.WithTransport(mercure.NewLocalTransport(mercure.NewSubscriberList(0))))\n\trequire.NoError(t, err)\n\n\topts.requestOpts = []frankenphp.RequestOption{frankenphp.WithMercureHub(h)}\n\n\trunTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {\n\t\tbody, _ := testGet(fmt.Sprintf(\"https://example.com/mercure-publish.php?i=%d\", i), handler, t)\n\t\tassert.Contains(t, body, \"update 1: \")\n\t\tassert.Contains(t, body, \"update 2: \")\n\t}, opts)\n}\n"
  },
  {
    "path": "metrics.go",
    "content": "package frankenphp\n\nimport (\n\t\"errors\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/prometheus/client_golang/prometheus\"\n)\n\nconst (\n\tStopReasonCrash = iota\n\tStopReasonRestart\n\tStopReasonBootFailure // worker crashed before reaching frankenphp_handle_request\n)\n\ntype StopReason int\n\ntype Metrics interface {\n\t// StartWorker collects started workers\n\tStartWorker(name string)\n\t// ReadyWorker collects ready workers\n\tReadyWorker(name string)\n\t// StopWorker collects stopped workers\n\tStopWorker(name string, reason StopReason)\n\t// TotalWorkers collects expected workers\n\tTotalWorkers(name string, num int)\n\t// TotalThreads collects total threads\n\tTotalThreads(num int)\n\t// StartRequest collects started requests\n\tStartRequest()\n\t// StopRequest collects stopped requests\n\tStopRequest()\n\t// StopWorkerRequest collects stopped worker requests\n\tStopWorkerRequest(name string, duration time.Duration)\n\t// StartWorkerRequest collects started worker requests\n\tStartWorkerRequest(name string)\n\tShutdown()\n\tQueuedWorkerRequest(name string)\n\tDequeuedWorkerRequest(name string)\n\tQueuedRequest()\n\tDequeuedRequest()\n}\n\ntype nullMetrics struct{}\n\nfunc (n nullMetrics) StartWorker(string) {\n}\n\nfunc (n nullMetrics) ReadyWorker(string) {\n}\n\nfunc (n nullMetrics) StopWorker(string, StopReason) {\n}\n\nfunc (n nullMetrics) TotalWorkers(string, int) {\n}\n\nfunc (n nullMetrics) TotalThreads(int) {\n}\n\nfunc (n nullMetrics) StartRequest() {\n}\n\nfunc (n nullMetrics) StopRequest() {\n}\n\nfunc (n nullMetrics) StopWorkerRequest(string, time.Duration) {\n}\n\nfunc (n nullMetrics) StartWorkerRequest(string) {\n}\n\nfunc (n nullMetrics) Shutdown() {\n}\n\nfunc (n nullMetrics) QueuedWorkerRequest(string) {}\n\nfunc (n nullMetrics) DequeuedWorkerRequest(string) {}\n\nfunc (n nullMetrics) QueuedRequest()   {}\nfunc (n nullMetrics) DequeuedRequest() {}\n\ntype PrometheusMetrics struct {\n\tregistry           prometheus.Registerer\n\ttotalThreads       prometheus.Counter\n\tbusyThreads        prometheus.Gauge\n\ttotalWorkers       *prometheus.GaugeVec\n\tbusyWorkers        *prometheus.GaugeVec\n\treadyWorkers       *prometheus.GaugeVec\n\tworkerCrashes      *prometheus.CounterVec\n\tworkerRestarts     *prometheus.CounterVec\n\tworkerRequestTime  *prometheus.CounterVec\n\tworkerRequestCount *prometheus.CounterVec\n\tworkerQueueDepth   *prometheus.GaugeVec\n\tqueueDepth         prometheus.Gauge\n\tmu                 sync.Mutex\n}\n\nfunc (m *PrometheusMetrics) StartWorker(name string) {\n\tm.busyThreads.Inc()\n\n\t// tests do not register workers before starting them\n\tif m.totalWorkers == nil {\n\t\treturn\n\t}\n\n\tm.totalWorkers.WithLabelValues(name).Inc()\n}\n\nfunc (m *PrometheusMetrics) ReadyWorker(name string) {\n\tif m.totalWorkers == nil {\n\t\treturn\n\t}\n\n\tm.readyWorkers.WithLabelValues(name).Inc()\n}\n\nfunc (m *PrometheusMetrics) StopWorker(name string, reason StopReason) {\n\tm.busyThreads.Dec()\n\n\t// tests do not register workers before starting them\n\tif m.totalWorkers == nil {\n\t\treturn\n\t}\n\n\tm.totalWorkers.WithLabelValues(name).Dec()\n\n\t// only decrement readyWorkers if the worker actually reached frankenphp_handle_request\n\tif reason != StopReasonBootFailure {\n\t\tm.readyWorkers.WithLabelValues(name).Dec()\n\t}\n\n\tswitch reason {\n\tcase StopReasonCrash, StopReasonBootFailure:\n\t\tm.workerCrashes.WithLabelValues(name).Inc()\n\tcase StopReasonRestart:\n\t\tm.workerRestarts.WithLabelValues(name).Inc()\n\t}\n}\n\nfunc (m *PrometheusMetrics) TotalWorkers(string, int) {\n\tm.mu.Lock()\n\tdefer m.mu.Unlock()\n\n\tconst ns, sub = \"frankenphp\", \"worker\"\n\tbasicLabels := []string{\"worker\"}\n\n\tif m.totalWorkers == nil {\n\t\tm.totalWorkers = prometheus.NewGaugeVec(prometheus.GaugeOpts{\n\t\t\tNamespace: ns,\n\t\t\tName:      \"total_workers\",\n\t\t\tHelp:      \"Total number of PHP workers for this worker\",\n\t\t}, basicLabels)\n\t\tif err := m.registry.Register(m.totalWorkers); err != nil &&\n\t\t\t!errors.As(err, &prometheus.AlreadyRegisteredError{}) {\n\t\t\tpanic(err)\n\t\t}\n\t}\n\n\tif m.readyWorkers == nil {\n\t\tm.readyWorkers = prometheus.NewGaugeVec(prometheus.GaugeOpts{\n\t\t\tNamespace: ns,\n\t\t\tName:      \"ready_workers\",\n\t\t\tHelp:      \"Running workers that have successfully called frankenphp_handle_request at least once\",\n\t\t}, basicLabels)\n\t\tif err := m.registry.Register(m.readyWorkers); err != nil &&\n\t\t\t!errors.As(err, &prometheus.AlreadyRegisteredError{}) {\n\t\t\tpanic(err)\n\t\t}\n\t}\n\n\tif m.busyWorkers == nil {\n\t\tm.busyWorkers = prometheus.NewGaugeVec(prometheus.GaugeOpts{\n\t\t\tNamespace: ns,\n\t\t\tName:      \"busy_workers\",\n\t\t\tHelp:      \"Number of busy PHP workers for this worker\",\n\t\t}, basicLabels)\n\t\tif err := m.registry.Register(m.busyWorkers); err != nil &&\n\t\t\t!errors.As(err, &prometheus.AlreadyRegisteredError{}) {\n\t\t\tpanic(err)\n\t\t}\n\t}\n\n\tif m.workerCrashes == nil {\n\t\tm.workerCrashes = prometheus.NewCounterVec(prometheus.CounterOpts{\n\t\t\tNamespace: ns,\n\t\t\tSubsystem: sub,\n\t\t\tName:      \"crashes\",\n\t\t\tHelp:      \"Number of PHP worker crashes for this worker\",\n\t\t}, basicLabels)\n\t\tif err := m.registry.Register(m.workerCrashes); err != nil &&\n\t\t\t!errors.As(err, &prometheus.AlreadyRegisteredError{}) {\n\t\t\tpanic(err)\n\t\t}\n\t}\n\n\tif m.workerRestarts == nil {\n\t\tm.workerRestarts = prometheus.NewCounterVec(prometheus.CounterOpts{\n\t\t\tNamespace: ns,\n\t\t\tSubsystem: sub,\n\t\t\tName:      \"restarts\",\n\t\t\tHelp:      \"Number of PHP worker restarts for this worker\",\n\t\t}, basicLabels)\n\t\tif err := m.registry.Register(m.workerRestarts); err != nil &&\n\t\t\t!errors.As(err, &prometheus.AlreadyRegisteredError{}) {\n\t\t\tpanic(err)\n\t\t}\n\t}\n\n\tif m.workerRequestTime == nil {\n\t\tm.workerRequestTime = prometheus.NewCounterVec(prometheus.CounterOpts{\n\t\t\tNamespace: ns,\n\t\t\tSubsystem: sub,\n\t\t\tName:      \"request_time\",\n\t\t}, basicLabels)\n\t\tif err := m.registry.Register(m.workerRequestTime); err != nil &&\n\t\t\t!errors.As(err, &prometheus.AlreadyRegisteredError{}) {\n\t\t\tpanic(err)\n\t\t}\n\t}\n\n\tif m.workerRequestCount == nil {\n\t\tm.workerRequestCount = prometheus.NewCounterVec(prometheus.CounterOpts{\n\t\t\tNamespace: ns,\n\t\t\tSubsystem: sub,\n\t\t\tName:      \"request_count\",\n\t\t}, basicLabels)\n\t\tif err := m.registry.Register(m.workerRequestCount); err != nil &&\n\t\t\t!errors.As(err, &prometheus.AlreadyRegisteredError{}) {\n\t\t\tpanic(err)\n\t\t}\n\t}\n\n\tif m.workerQueueDepth == nil {\n\t\tm.workerQueueDepth = prometheus.NewGaugeVec(prometheus.GaugeOpts{\n\t\t\tNamespace: \"frankenphp\",\n\t\t\tSubsystem: sub,\n\t\t\tName:      \"queue_depth\",\n\t\t}, basicLabels)\n\t\tif err := m.registry.Register(m.workerQueueDepth); err != nil &&\n\t\t\t!errors.As(err, &prometheus.AlreadyRegisteredError{}) {\n\t\t\tpanic(err)\n\t\t}\n\t}\n}\n\nfunc (m *PrometheusMetrics) TotalThreads(num int) {\n\tm.totalThreads.Add(float64(num))\n}\n\nfunc (m *PrometheusMetrics) StartRequest() {\n\tm.busyThreads.Inc()\n}\n\nfunc (m *PrometheusMetrics) StopRequest() {\n\tm.busyThreads.Dec()\n}\n\nfunc (m *PrometheusMetrics) StopWorkerRequest(name string, duration time.Duration) {\n\tif m.workerRequestTime == nil {\n\t\treturn\n\t}\n\n\tm.workerRequestCount.WithLabelValues(name).Inc()\n\tm.busyWorkers.WithLabelValues(name).Dec()\n\tm.workerRequestTime.WithLabelValues(name).Add(duration.Seconds())\n}\n\nfunc (m *PrometheusMetrics) StartWorkerRequest(name string) {\n\tif m.busyWorkers == nil {\n\t\treturn\n\t}\n\tm.busyWorkers.WithLabelValues(name).Inc()\n}\n\nfunc (m *PrometheusMetrics) QueuedWorkerRequest(name string) {\n\tif m.workerQueueDepth == nil {\n\t\treturn\n\t}\n\tm.workerQueueDepth.WithLabelValues(name).Inc()\n}\n\nfunc (m *PrometheusMetrics) DequeuedWorkerRequest(name string) {\n\tif m.workerQueueDepth == nil {\n\t\treturn\n\t}\n\tm.workerQueueDepth.WithLabelValues(name).Dec()\n}\n\nfunc (m *PrometheusMetrics) QueuedRequest() {\n\tm.queueDepth.Inc()\n}\n\nfunc (m *PrometheusMetrics) DequeuedRequest() {\n\tm.queueDepth.Dec()\n}\n\nfunc (m *PrometheusMetrics) Shutdown() {\n\tm.registry.Unregister(m.totalThreads)\n\tm.registry.Unregister(m.busyThreads)\n\tm.registry.Unregister(m.queueDepth)\n\n\tif m.totalWorkers != nil {\n\t\tm.registry.Unregister(m.totalWorkers)\n\t\tm.totalWorkers = nil\n\t}\n\n\tif m.busyWorkers != nil {\n\t\tm.registry.Unregister(m.busyWorkers)\n\t\tm.busyWorkers = nil\n\t}\n\n\tif m.workerRequestTime != nil {\n\t\tm.registry.Unregister(m.workerRequestTime)\n\t\tm.workerRequestTime = nil\n\t}\n\n\tif m.workerRequestCount != nil {\n\t\tm.registry.Unregister(m.workerRequestCount)\n\t\tm.workerRequestCount = nil\n\t}\n\n\tif m.workerCrashes != nil {\n\t\tm.registry.Unregister(m.workerCrashes)\n\t\tm.workerCrashes = nil\n\t}\n\n\tif m.workerRestarts != nil {\n\t\tm.registry.Unregister(m.workerRestarts)\n\t\tm.workerRestarts = nil\n\t}\n\n\tif m.readyWorkers != nil {\n\t\tm.registry.Unregister(m.readyWorkers)\n\t\tm.readyWorkers = nil\n\t}\n\n\tif m.workerQueueDepth != nil {\n\t\tm.registry.Unregister(m.workerQueueDepth)\n\t\tm.workerQueueDepth = nil\n\t}\n\n\tm.totalThreads = prometheus.NewCounter(prometheus.CounterOpts{\n\t\tName: \"frankenphp_total_threads\",\n\t\tHelp: \"Total number of PHP threads\",\n\t})\n\tm.busyThreads = prometheus.NewGauge(prometheus.GaugeOpts{\n\t\tName: \"frankenphp_busy_threads\",\n\t\tHelp: \"Number of busy PHP threads\",\n\t})\n\tm.queueDepth = prometheus.NewGauge(prometheus.GaugeOpts{\n\t\tName: \"frankenphp_queue_depth\",\n\t\tHelp: \"Number of regular queued requests\",\n\t})\n\n\tif err := m.registry.Register(m.totalThreads); err != nil &&\n\t\t!errors.As(err, &prometheus.AlreadyRegisteredError{}) {\n\t\tpanic(err)\n\t}\n\n\tif err := m.registry.Register(m.busyThreads); err != nil &&\n\t\t!errors.As(err, &prometheus.AlreadyRegisteredError{}) {\n\t\tpanic(err)\n\t}\n\n\tif err := m.registry.Register(m.queueDepth); err != nil &&\n\t\t!errors.As(err, &prometheus.AlreadyRegisteredError{}) {\n\t\tpanic(err)\n\t}\n}\n\nfunc NewPrometheusMetrics(registry prometheus.Registerer) *PrometheusMetrics {\n\tif registry == nil {\n\t\tregistry = prometheus.NewRegistry()\n\t}\n\n\tm := &PrometheusMetrics{\n\t\tregistry: registry,\n\t\ttotalThreads: prometheus.NewCounter(prometheus.CounterOpts{\n\t\t\tName: \"frankenphp_total_threads\",\n\t\t\tHelp: \"Total number of PHP threads\",\n\t\t}),\n\t\tbusyThreads: prometheus.NewGauge(prometheus.GaugeOpts{\n\t\t\tName: \"frankenphp_busy_threads\",\n\t\t\tHelp: \"Number of busy PHP threads\",\n\t\t}),\n\t\tqueueDepth: prometheus.NewGauge(prometheus.GaugeOpts{\n\t\t\tName: \"frankenphp_queue_depth\",\n\t\t\tHelp: \"Number of regular queued requests\",\n\t\t}),\n\t\ttotalWorkers:       nil,\n\t\tbusyWorkers:        nil,\n\t\tworkerRequestTime:  nil,\n\t\tworkerRequestCount: nil,\n\t\tworkerRestarts:     nil,\n\t\tworkerCrashes:      nil,\n\t\treadyWorkers:       nil,\n\t\tworkerQueueDepth:   nil,\n\t}\n\n\tif err := m.registry.Register(m.totalThreads); err != nil &&\n\t\t!errors.As(err, &prometheus.AlreadyRegisteredError{}) {\n\t\tpanic(err)\n\t}\n\n\tif err := m.registry.Register(m.busyThreads); err != nil &&\n\t\t!errors.As(err, &prometheus.AlreadyRegisteredError{}) {\n\t\tpanic(err)\n\t}\n\n\tif err := m.registry.Register(m.queueDepth); err != nil &&\n\t\t!errors.As(err, &prometheus.AlreadyRegisteredError{}) {\n\t\tpanic(err)\n\t}\n\n\treturn m\n}\n"
  },
  {
    "path": "metrics_test.go",
    "content": "package frankenphp\n\nimport (\n\t\"strings\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/prometheus/client_golang/prometheus\"\n\t\"github.com/prometheus/client_golang/prometheus/testutil\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc createPrometheusMetrics() *PrometheusMetrics {\n\treturn &PrometheusMetrics{\n\t\tregistry:     prometheus.NewRegistry(),\n\t\ttotalThreads: prometheus.NewCounter(prometheus.CounterOpts{Name: \"frankenphp_total_threads\"}),\n\t\tbusyThreads:  prometheus.NewGauge(prometheus.GaugeOpts{Name: \"frankenphp_busy_threads\"}),\n\t\tqueueDepth:   prometheus.NewGauge(prometheus.GaugeOpts{Name: \"frankenphp_queue_depth\"}),\n\t\tmu:           sync.Mutex{},\n\t}\n}\n\nfunc TestPrometheusMetrics_TotalWorkers(t *testing.T) {\n\tm := createPrometheusMetrics()\n\n\trequire.Nil(t, m.totalWorkers)\n\trequire.Nil(t, m.busyWorkers)\n\trequire.Nil(t, m.readyWorkers)\n\trequire.Nil(t, m.workerCrashes)\n\trequire.Nil(t, m.workerRestarts)\n\trequire.Nil(t, m.workerRequestTime)\n\trequire.Nil(t, m.workerRequestCount)\n\n\tm.TotalWorkers(\"test_worker\", 2)\n\n\trequire.NotNil(t, m.totalWorkers)\n\trequire.NotNil(t, m.busyWorkers)\n\trequire.NotNil(t, m.readyWorkers)\n\trequire.NotNil(t, m.workerCrashes)\n\trequire.NotNil(t, m.workerRestarts)\n\trequire.NotNil(t, m.workerRequestTime)\n\trequire.NotNil(t, m.workerRequestCount)\n}\n\nfunc TestPrometheusMetrics_StopWorkerRequest(t *testing.T) {\n\tm := createPrometheusMetrics()\n\tm.TotalWorkers(\"test_worker\", 2)\n\tm.StopWorkerRequest(\"test_worker\", 2*time.Second)\n\n\tinputs := []struct {\n\t\tname     string\n\t\tc        prometheus.Collector\n\t\tmetadata string\n\t\texpect   string\n\t}{\n\t\t{\n\t\t\tname: \"Testing WorkerRequestCount\",\n\t\t\tc:    m.workerRequestCount,\n\t\t\tmetadata: `\n\t\t\t\t# HELP frankenphp_worker_request_count\n\t\t\t\t# TYPE frankenphp_worker_request_count counter\n\t\t\t`,\n\t\t\texpect: `\n\t\t\t\tfrankenphp_worker_request_count{worker=\"test_worker\"} 1\n\t\t\t`,\n\t\t},\n\t\t{\n\t\t\tname: \"Testing BusyWorkers\",\n\t\t\tc:    m.busyWorkers,\n\t\t\tmetadata: `\n\t\t\t\t# HELP frankenphp_busy_workers Number of busy PHP workers for this worker\n\t\t\t\t# TYPE frankenphp_busy_workers gauge\n\t\t\t`,\n\t\t\texpect: `\n\t\t\t\tfrankenphp_busy_workers{worker=\"test_worker\"} -1\n\t\t\t`,\n\t\t},\n\t\t{\n\t\t\tname: \"Testing WorkerRequestTime\",\n\t\t\tc:    m.workerRequestTime,\n\t\t\tmetadata: `\n\t\t\t\t# HELP frankenphp_worker_request_time\n\t\t\t\t# TYPE frankenphp_worker_request_time counter\n\t\t\t`,\n\t\t\texpect: `\n\t\t\t\tfrankenphp_worker_request_time{worker=\"test_worker\"} 2\n\t\t\t`,\n\t\t},\n\t}\n\n\tfor _, input := range inputs {\n\t\tt.Run(input.name, func(t *testing.T) {\n\t\t\trequire.NoError(t, testutil.CollectAndCompare(input.c, strings.NewReader(input.metadata+input.expect)))\n\t\t})\n\n\t}\n}\n\nfunc TestPrometheusMetrics_StartWorkerRequest(t *testing.T) {\n\tm := createPrometheusMetrics()\n\tm.TotalWorkers(\"test_worker\", 2)\n\tm.StartWorkerRequest(\"test_worker\")\n\n\tinputs := []struct {\n\t\tname     string\n\t\tc        prometheus.Collector\n\t\tmetadata string\n\t\texpect   string\n\t}{\n\t\t{\n\t\t\tname: \"Testing BusyWorkers\",\n\t\t\tc:    m.busyWorkers,\n\t\t\tmetadata: `\n\t\t\t\t# HELP frankenphp_busy_workers Number of busy PHP workers for this worker\n\t\t\t\t# TYPE frankenphp_busy_workers gauge\n\t\t\t`,\n\t\t\texpect: `\n\t\t\t\tfrankenphp_busy_workers{worker=\"test_worker\"} 1\n\t\t\t`,\n\t\t},\n\t}\n\n\tfor _, input := range inputs {\n\t\tt.Run(input.name, func(t *testing.T) {\n\t\t\trequire.NoError(t, testutil.CollectAndCompare(input.c, strings.NewReader(input.metadata+input.expect)))\n\t\t})\n\n\t}\n}\n\nfunc TestPrometheusMetrics_TestStopReasonCrash(t *testing.T) {\n\tm := createPrometheusMetrics()\n\tm.TotalWorkers(\"test_worker\", 2)\n\tm.StopWorker(\"test_worker\", StopReasonCrash)\n\n\tinputs := []struct {\n\t\tname     string\n\t\tc        prometheus.Collector\n\t\tmetadata string\n\t\texpect   string\n\t}{\n\t\t{\n\t\t\tname: \"Testing BusyThreads\",\n\t\t\tc:    m.busyThreads,\n\t\t\tmetadata: `\n\t\t\t\t# HELP frankenphp_busy_threads\n\t\t\t\t# TYPE frankenphp_busy_threads gauge\n\t\t\t`,\n\t\t\texpect: `\n\t\t\t\tfrankenphp_busy_threads -1\n\t\t\t`,\n\t\t},\n\t\t{\n\t\t\tname: \"Testing TotalWorkers\",\n\t\t\tc:    m.totalWorkers,\n\t\t\tmetadata: `\n\t\t\t\t# HELP frankenphp_total_workers Total number of PHP workers for this worker\n\t\t\t\t# TYPE frankenphp_total_workers gauge\n\t\t\t`,\n\t\t\texpect: `\n\t\t\t\tfrankenphp_total_workers{worker=\"test_worker\"} -1\n\t\t\t`,\n\t\t},\n\t\t{\n\t\t\tname: \"Testing ReadyWorkers\",\n\t\t\tc:    m.readyWorkers,\n\t\t\tmetadata: `\n\t\t\t\t# HELP frankenphp_ready_workers Running workers that have successfully called frankenphp_handle_request at least once\n\t\t\t\t# TYPE frankenphp_ready_workers gauge\n\t\t\t`,\n\t\t\texpect: `\n\t\t\t\tfrankenphp_ready_workers{worker=\"test_worker\"} -1\n\t\t\t`,\n\t\t},\n\t\t{\n\t\t\tname: \"Testing WorkerCrashes\",\n\t\t\tc:    m.workerCrashes,\n\t\t\tmetadata: `\n\t\t\t\t# HELP frankenphp_worker_crashes Number of PHP worker crashes for this worker\n\t\t\t\t# TYPE frankenphp_worker_crashes counter\n\t\t\t`,\n\t\t\texpect: `\n\t\t\t\tfrankenphp_worker_crashes{worker=\"test_worker\"} 1\n\t\t\t`,\n\t\t},\n\t}\n\n\tfor _, input := range inputs {\n\t\tt.Run(input.name, func(t *testing.T) {\n\t\t\trequire.NoError(t, testutil.CollectAndCompare(input.c, strings.NewReader(input.metadata+input.expect)))\n\t\t})\n\n\t}\n}\n"
  },
  {
    "path": "options.go",
    "content": "package frankenphp\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"time\"\n)\n\n// defaultMaxConsecutiveFailures is the default maximum number of consecutive failures before panicking\nconst defaultMaxConsecutiveFailures = 6\n\n// Option instances allow to configure FrankenPHP.\ntype Option func(h *opt) error\n\n// WorkerOption instances allow configuring FrankenPHP worker.\ntype WorkerOption func(*workerOpt) error\n\n// opt contains the available options.\n//\n// If you change this, also update the Caddy module and the documentation.\ntype opt struct {\n\thotReloadOpt\n\n\tctx         context.Context\n\tnumThreads  int\n\tmaxThreads  int\n\tworkers     []workerOpt\n\tlogger      *slog.Logger\n\tmetrics     Metrics\n\tphpIni      map[string]string\n\tmaxWaitTime time.Duration\n\tmaxIdleTime time.Duration\n}\n\ntype workerOpt struct {\n\tmercureContext\n\n\tname                   string\n\tfileName               string\n\tnum                    int\n\tmaxThreads             int\n\tenv                    PreparedEnv\n\trequestOptions         []RequestOption\n\twatch                  []string\n\tmaxConsecutiveFailures int\n\textensionWorkers       *extensionWorkers\n\tonThreadReady          func(int)\n\tonThreadShutdown       func(int)\n\tonServerStartup        func()\n\tonServerShutdown       func()\n}\n\n// WithContext sets the main context to use.\nfunc WithContext(ctx context.Context) Option {\n\treturn func(h *opt) error {\n\t\th.ctx = ctx\n\n\t\treturn nil\n\t}\n}\n\n// WithNumThreads configures the number of PHP threads to start.\nfunc WithNumThreads(numThreads int) Option {\n\treturn func(o *opt) error {\n\t\to.numThreads = numThreads\n\n\t\treturn nil\n\t}\n}\n\nfunc WithMaxThreads(maxThreads int) Option {\n\treturn func(o *opt) error {\n\t\to.maxThreads = maxThreads\n\n\t\treturn nil\n\t}\n}\n\nfunc WithMetrics(m Metrics) Option {\n\treturn func(o *opt) error {\n\t\to.metrics = m\n\n\t\treturn nil\n\t}\n}\n\n// WithWorkers configures the PHP workers to start\nfunc WithWorkers(name, fileName string, num int, options ...WorkerOption) Option {\n\treturn func(o *opt) error {\n\t\tworker := workerOpt{\n\t\t\tname:                   name,\n\t\t\tfileName:               fileName,\n\t\t\tnum:                    num,\n\t\t\tenv:                    PrepareEnv(nil),\n\t\t\twatch:                  []string{},\n\t\t\tmaxConsecutiveFailures: defaultMaxConsecutiveFailures,\n\t\t}\n\n\t\tfor _, option := range options {\n\t\t\tif err := option(&worker); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\to.workers = append(o.workers, worker)\n\n\t\treturn nil\n\t}\n}\n\n// EXPERIMENTAL: WithExtensionWorkers allow extensions to create workers.\n//\n// A worker script with the provided name, fileName and thread count will be registered, along with additional\n// configuration through WorkerOptions.\n//\n// Workers are designed to run indefinitely and will be gracefully shut down when FrankenPHP shuts down.\n//\n// Extension workers receive the lowest priority when determining thread allocations. If the requested number of threads\n// cannot be allocated, then FrankenPHP will panic and provide this information to the user (who will need to allocate\n// more total threads). Don't be greedy.\nfunc WithExtensionWorkers(name, fileName string, numThreads int, options ...WorkerOption) (Workers, Option) {\n\tw := &extensionWorkers{\n\t\tname:     name,\n\t\tfileName: fileName,\n\t\tnum:      numThreads,\n\t}\n\n\tw.options = append(options, withExtensionWorkers(w))\n\n\treturn w, WithWorkers(w.name, w.fileName, w.num, w.options...)\n}\n\n// WithLogger configures the global logger to use.\nfunc WithLogger(l *slog.Logger) Option {\n\treturn func(o *opt) error {\n\t\to.logger = l\n\n\t\treturn nil\n\t}\n}\n\n// WithPhpIni configures user defined PHP ini settings.\nfunc WithPhpIni(overrides map[string]string) Option {\n\treturn func(o *opt) error {\n\t\to.phpIni = overrides\n\t\treturn nil\n\t}\n}\n\n// WithMaxWaitTime configures the max time a request may be stalled waiting for a thread.\nfunc WithMaxWaitTime(maxWaitTime time.Duration) Option {\n\treturn func(o *opt) error {\n\t\to.maxWaitTime = maxWaitTime\n\n\t\treturn nil\n\t}\n}\n\n// WithMaxIdleTime configures the max time an autoscaled thread may be idle before being deactivated.\nfunc WithMaxIdleTime(maxIdleTime time.Duration) Option {\n\treturn func(o *opt) error {\n\t\to.maxIdleTime = maxIdleTime\n\n\t\treturn nil\n\t}\n}\n\n// WithWorkerEnv sets environment variables for the worker\nfunc WithWorkerEnv(env map[string]string) WorkerOption {\n\treturn func(w *workerOpt) error {\n\t\tw.env = PrepareEnv(env)\n\n\t\treturn nil\n\t}\n}\n\n// WithWorkerRequestOptions sets options for the main dummy request created for the worker\nfunc WithWorkerRequestOptions(options ...RequestOption) WorkerOption {\n\treturn func(w *workerOpt) error {\n\t\tw.requestOptions = append(w.requestOptions, options...)\n\n\t\treturn nil\n\t}\n}\n\n// WithWorkerMaxThreads sets the max number of threads for this specific worker\nfunc WithWorkerMaxThreads(num int) WorkerOption {\n\treturn func(w *workerOpt) error {\n\t\tw.maxThreads = num\n\n\t\treturn nil\n\t}\n}\n\n// WithWorkerWatchMode sets directories to watch for file changes\nfunc WithWorkerWatchMode(watch []string) WorkerOption {\n\treturn func(w *workerOpt) error {\n\t\tw.watch = watch\n\n\t\treturn nil\n\t}\n}\n\n// WithWorkerMaxFailures sets the maximum number of consecutive failures before panicking\nfunc WithWorkerMaxFailures(maxFailures int) WorkerOption {\n\treturn func(w *workerOpt) error {\n\t\tif maxFailures < -1 {\n\t\t\treturn fmt.Errorf(\"max consecutive failures must be >= -1, got %d\", maxFailures)\n\t\t}\n\t\tw.maxConsecutiveFailures = maxFailures\n\n\t\treturn nil\n\t}\n}\n\nfunc WithWorkerOnReady(f func(int)) WorkerOption {\n\treturn func(w *workerOpt) error {\n\t\tw.onThreadReady = f\n\n\t\treturn nil\n\t}\n}\n\nfunc WithWorkerOnShutdown(f func(int)) WorkerOption {\n\treturn func(w *workerOpt) error {\n\t\tw.onThreadShutdown = f\n\n\t\treturn nil\n\t}\n}\n\n// WithWorkerOnServerStartup adds a function to be called right after server startup. Useful for extensions.\nfunc WithWorkerOnServerStartup(f func()) WorkerOption {\n\treturn func(w *workerOpt) error {\n\t\tw.onServerStartup = f\n\n\t\treturn nil\n\t}\n}\n\n// WithWorkerOnServerShutdown adds a function to be called right before server shutdown. Useful for extensions.\nfunc WithWorkerOnServerShutdown(f func()) WorkerOption {\n\treturn func(w *workerOpt) error {\n\t\tw.onServerShutdown = f\n\n\t\treturn nil\n\t}\n}\n\nfunc withExtensionWorkers(w *extensionWorkers) WorkerOption {\n\treturn func(wo *workerOpt) error {\n\t\two.extensionWorkers = w\n\n\t\treturn nil\n\t}\n}\n"
  },
  {
    "path": "package/Caddyfile",
    "content": "# The Caddyfile is an easy way to configure FrankenPHP and the Caddy web server.\n#\n# https://frankenphp.dev/docs/config\n# https://caddyserver.com/docs/caddyfile\n{\n\tfrankenphp\n}\n\nhttp:// {\n\troot /usr/share/frankenphp/\n\tencode zstd br gzip\n\n\tphp_server\n}\n\n# As an alternative to editing the above site block, you can add your own site\n# block files in the Caddyfile.d directory, and they will be included as long\n# as they use the .caddyfile extension.\nimport Caddyfile.d/*.caddyfile\n"
  },
  {
    "path": "package/alpine/frankenphp.openrc",
    "content": "#!/sbin/openrc-run\n\nname=\"FrankenPHP\"\ndescription=\"The modern PHP app server\"\n\ncommand=\"/usr/bin/frankenphp\"\ncommand_args=\"run --environ --config /etc/frankenphp/Caddyfile\"\ncommand_user=\"frankenphp:frankenphp\"\ncommand_background=\"yes\"\ncapabilities=\"^cap_net_bind_service\"\npidfile=\"/run/frankenphp/frankenphp.pid\"\nstart_stop_daemon_args=\"--chdir /var/lib/frankenphp\"\nrespawn_delay=3\nrespawn_max=10\n\ndepend() {\n    need net\n    after firewall\n}\n\nstart_pre() {\n    checkpath --directory --owner frankenphp:frankenphp --mode 0755 /run/frankenphp\n\n    $command validate --config /etc/frankenphp/Caddyfile\n}\n\nreload() {\n    ebegin \"Reloading $name configuration\"\n    $command reload --config /etc/frankenphp/Caddyfile --force\n    eend $?\n}\n"
  },
  {
    "path": "package/alpine/post-deinstall.sh",
    "content": "#!/bin/sh\n\nif getent passwd frankenphp >/dev/null; then\n\tdeluser frankenphp\nfi\n\nif getent group frankenphp >/dev/null; then\n\tdelgroup frankenphp\nfi\n\nrmdir /var/lib/frankenphp 2>/dev/null || true\n\nexit 0\n"
  },
  {
    "path": "package/alpine/post-install.sh",
    "content": "#!/bin/sh\n\nif ! getent group frankenphp >/dev/null; then\n\taddgroup -S frankenphp\nfi\n\nif ! getent passwd frankenphp >/dev/null; then\n\tadduser -S -h /var/lib/frankenphp -s /sbin/nologin -G frankenphp -g \"FrankenPHP web server\" frankenphp\nfi\n\nchown -R frankenphp:frankenphp /var/lib/frankenphp\nchmod 755 /var/lib/frankenphp\n\n# allow binding to privileged ports\nif command -v setcap >/dev/null 2>&1; then\n\tsetcap cap_net_bind_service=+ep /usr/bin/frankenphp || true\nfi\n\n# check if 0.0.0.0:2019 or 127.0.0.1:2019 are in use\nport_in_use() {\n\tport_hex=$(printf '%04X' \"$1\")\n\tgrep -qE \"(00000000|0100007F):${port_hex}\" /proc/net/tcp 2>/dev/null\n}\n\n# trust frankenphp certificates if the admin api can start\nif [ -x /usr/bin/frankenphp ]; then\n\tif ! port_in_use 2019; then\n\t\tHOME=/var/lib/frankenphp /usr/bin/frankenphp run >/dev/null 2>&1 &\n\t\tFRANKENPHP_PID=$!\n\t\tsleep 2\n\t\tHOME=/var/lib/frankenphp /usr/bin/frankenphp trust || true\n\t\tkill -TERM $FRANKENPHP_PID 2>/dev/null || true\n\n\t\tchown -R frankenphp:frankenphp /var/lib/frankenphp\n\tfi\nfi\n\nif command -v rc-update >/dev/null 2>&1; then\n\trc-update add frankenphp default\n\trc-service frankenphp start\nfi\n\nexit 0\n"
  },
  {
    "path": "package/alpine/pre-deinstall.sh",
    "content": "#!/bin/sh\n\nif command -v rc-service >/dev/null 2>&1; then\n\trc-service frankenphp stop || true\nfi\n\nif command -v rc-update >/dev/null 2>&1; then\n\trc-update del frankenphp default || true\nfi\n\nexit 0\n"
  },
  {
    "path": "package/content/index.php",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n    <meta charset=\"UTF-8\">\n    <title>Test Page for FrankenPHP</title>\n    <style>\n        * {\n            box-sizing: border-box;\n        }\n\n        html {\n            height: 100%;\n        }\n\n        body {\n            background: #fff;\n            color: #000;\n            font-size: 0.9em;\n            font-family: sans-serif, helvetica;\n            margin: 0;\n            padding: 0;\n            height: 100%;\n        }\n\n        a {\n            color: #390075;\n            font-weight: bold;\n        }\n\n        h2 {\n            font-size: 1.5em;\n            font-weight: bold;\n            color: #390075;\n        }\n\n        main {\n            display: flex;\n            margin-left: auto;\n            margin-right: auto;\n            height: 100%;\n        }\n\n        .content {\n            flex: 1;\n            display: flex;\n            flex-direction: column;\n            gap: 10px;\n            padding: 1.5em 3em;\n\n            section {\n                max-width: 600px;\n            }\n        }\n\n        .runtime-info {\n            background: #efefef;\n            padding: 0.5em;\n            margin-top: 1em;\n            font-size: 0.85em;\n            border-left: 3px solid #390075;\n        }\n\n        .right {\n            overflow: auto;\n            flex: 1;\n            display: flex;\n            flex-direction: column;\n        }\n\n        .left {\n            width: 40%;\n            background: #230143;\n            color: #fff;\n            position: relative;\n            text-align: center;\n            padding-top: 1em;\n            display: flex;\n            flex-direction: column;\n            justify-content: center;\n            align-items: center;\n            gap: 1em;\n\n            .logo {\n                width: 80%;\n                max-width: 400px;\n                margin-left: auto;\n                margin-right: auto;\n            }\n\n            .franky {\n                width: 80%;\n                max-width: 400px;\n                margin-bottom: 1em;\n            }\n        }\n\n        footer {\n            border-top: 1px solid #ccc;\n            font-size: 0.8em;\n            padding: 1em 3em;\n            display: flex;\n            flex-direction: column;\n            gap: 1em;\n\n            .logos {\n                display: flex;\n                gap: 12px;\n                align-items: center;\n            }\n\n            svg {\n                width: 120px;\n            }\n\n            p {\n                padding: 0;\n                margin: 0;\n            }\n        }\n\n        @media (max-width: 768px) {\n            main {\n                flex-direction: column;\n            }\n\n            .right {\n                overflow: visible;\n            }\n\n            .left {\n                width: 100%;\n                flex-direction: row;\n                padding: 1em;\n\n                .logo {\n                    max-width: 250px;\n                }\n\n                .franky {\n                    height: 100px;\n                    width: auto;\n                }\n\n                .logo {\n                    margin: 0;\n                }\n            }\n\n            .content {\n                padding: 1em 2em;\n            }\n\n            footer {\n                padding: 1em 2em;\n                text-align: center;\n                margin-top: 1em;\n\n                .logos {\n                    justify-content: center;\n                }\n\n            }\n        }\n    </style>\n</head>\n\n<body>\n    <main>\n        <div class=\"left\">\n            <h1 class=\"logo\">\n                <svg xmlns=\"http://www.w3.org/2000/svg\" id=\"logo\" viewBox=\"0 0 699.37 92.29\">\n                    <defs fill=\"#000000\">\n                        <style>\n                            .logo_b {\n                                fill: #fff;\n                            }\n\n                            .c {\n                                fill: #b3d133;\n                            }\n                        </style>\n                    </defs>\n                    <path class=\"c\"\n                        d=\"M22.65,38.71h20.32c3.02,0,5.65,1.1,7.89,3.3,2.24,2.2,3.36,4.85,3.36,7.96s-1.12,5.67-3.36,7.96c-2.24,2.29-4.96,3.43-8.15,3.43H22.65v19.8c0,3.45-1.12,6.17-3.37,8.15-2.24,1.99-4.88,2.98-7.89,2.98s-5.78-.99-8.02-2.98c-2.24-1.98-3.37-4.7-3.37-8.15V20.2c0-3.45,.86-6.64,2.59-9.58,1.72-2.93,4.03-5.26,6.92-6.99,2.89-1.72,6.06-2.59,9.51-2.59h28.86c3.19,0,5.89,1.1,8.09,3.3,2.2,2.2,3.3,4.81,3.3,7.83s-1.14,5.8-3.43,8.09c-2.29,2.29-4.94,3.43-7.96,3.43H22.65v15.01Z\"\n                        fill=\"#000000\" />\n                    <path class=\"c\"\n                        d=\"M118.29,54.76c3.88,2.42,6.66,5.18,8.35,8.28,1.68,3.11,2.52,6.77,2.52,11v6.6c0,3.11-1.08,5.76-3.24,7.96-2.16,2.2-4.75,3.3-7.77,3.3s-5.74-1.12-7.89-3.36c-2.16-2.24-3.24-4.87-3.24-7.89v-6.34l-22.13-26.92v33.26c0,3.11-1.08,5.76-3.24,7.96-2.16,2.2-4.79,3.3-7.89,3.3s-5.89-1.1-8.09-3.3c-2.2-2.2-3.3-4.85-3.3-7.96V12.44c0-3.19,1.1-5.89,3.3-8.09s4.9-3.3,8.09-3.3h25.24c5.09,0,9.62,1.86,13.59,5.57l11,10.61c4.05,4.06,6.08,8.72,6.08,13.98v5.05c0,8.54-3.8,14.71-11.39,18.51Zm-33.39-9.32h22.13V23.7h-22.13v21.74Z\"\n                        fill=\"#000000\" />\n                    <path class=\"c\"\n                        d=\"M270.74,88.54c-2.16,2.24-4.83,3.36-8.02,3.36s-6-1.12-8.15-3.36c-2.16-2.24-3.24-4.96-3.24-8.15V23.57h-20.84v57.07c0,3.19-1.12,5.87-3.37,8.02-2.24,2.16-4.96,3.24-8.15,3.24s-5.76-1.1-7.96-3.3c-2.2-2.2-3.3-4.85-3.3-7.96V12.05c0-3.19,1.12-5.89,3.37-8.09,2.24-2.2,4.87-3.3,7.89-3.3,3.19,0,5.91,1.1,8.15,3.3,2.24,2.2,3.37,4.9,3.37,8.09v3.75c0,.43,.08,.65,.26,.65s.26-.08,.26-.26c1.55-5,4.12-8.8,7.7-11.39,3.58-2.59,7.87-3.88,12.88-3.88,6.47,0,11.82,1.92,16.05,5.76,4.23,3.84,6.34,8.99,6.34,15.47v58.24c0,3.19-1.08,5.91-3.24,8.15Z\"\n                        fill=\"#000000\" />\n                    <path class=\"c\"\n                        d=\"M347.1,80.64c0,3.11-1.14,5.78-3.43,8.02-2.29,2.24-4.98,3.37-8.09,3.37s-5.8-1.12-8.09-3.37c-2.29-2.24-3.43-4.92-3.43-8.02v-24.72l-22.13-.26v24.98c0,3.19-1.1,5.89-3.3,8.09s-4.9,3.3-8.09,3.3-5.89-1.1-8.09-3.3-3.3-4.9-3.3-8.09V11.66c0-3.02,1.1-5.63,3.3-7.83,2.2-2.2,4.9-3.3,8.09-3.3s5.89,1.08,8.09,3.24c2.2,2.16,3.3,4.79,3.3,7.89V53.72l22.13-27.31V11.66c0-3.02,1.14-5.63,3.43-7.83,2.29-2.2,4.94-3.3,7.96-3.3s5.63,1.1,7.83,3.3c2.2,2.2,3.3,4.81,3.3,7.83v15.14c0,4.32-.84,8.05-2.52,11.19-1.68,3.15-4.51,5.98-8.48,8.48,7.68,3.62,11.52,9.79,11.52,18.51v15.66Z\"\n                        fill=\"#000000\" />\n                    <path class=\"c\"\n                        d=\"M374.66,35.6h25.24c3.11,0,5.76,1.1,7.96,3.3,2.2,2.2,3.3,4.9,3.3,8.09,0,3.02-1.12,5.65-3.37,7.89-2.24,2.24-4.88,3.36-7.89,3.36h-25.24v10.87h27.44c3.11,0,5.76,1.1,7.96,3.3,2.2,2.2,3.3,4.9,3.3,8.09,0,3.02-1.1,5.61-3.3,7.77-2.2,2.16-4.85,3.32-7.96,3.49h-31.19c-3.37,0-6.49-.86-9.38-2.59-2.89-1.73-5.2-4.05-6.92-6.99-1.73-2.93-2.59-6.12-2.59-9.58v-6.99c0-5.26,.73-9.55,2.2-12.88,1.47-3.32,4.49-5.76,9.06-7.31-4.32-1.47-7.27-3.77-8.87-6.92-1.6-3.15-2.39-7.23-2.39-12.23v-6.08c0-3.45,.86-6.64,2.59-9.58,1.73-2.93,4.03-5.26,6.92-6.99,2.89-1.72,6.02-2.59,9.38-2.59h31.19c3.11,0,5.76,1.1,7.96,3.3s3.3,4.81,3.3,7.83-1.12,5.8-3.37,8.09c-2.24,2.29-4.88,3.43-7.89,3.43h-27.44v11.91Z\"\n                        fill=\"#000000\" />\n                    <path class=\"c\"\n                        d=\"M480.4,88.54c-2.16,2.24-4.83,3.36-8.02,3.36s-6-1.12-8.15-3.36c-2.16-2.24-3.24-4.96-3.24-8.15V23.57h-20.84v57.07c0,3.19-1.12,5.87-3.37,8.02-2.24,2.16-4.96,3.24-8.15,3.24s-5.76-1.1-7.96-3.3c-2.2-2.2-3.3-4.85-3.3-7.96V12.05c0-3.19,1.12-5.89,3.37-8.09,2.24-2.2,4.87-3.3,7.89-3.3,3.19,0,5.91,1.1,8.15,3.3,2.24,2.2,3.37,4.9,3.37,8.09v3.75c0,.43,.08,.65,.26,.65s.26-.08,.26-.26c1.55-5,4.12-8.8,7.7-11.39,3.58-2.59,7.87-3.88,12.88-3.88,6.47,0,11.82,1.92,16.05,5.76,4.23,3.84,6.34,8.99,6.34,15.47v58.24c0,3.19-1.08,5.91-3.24,8.15Z\"\n                        fill=\"#000000\" />\n                    <path class=\"logo_b\"\n                        d=\"M511.07,80.25c0,3.19-1.06,5.91-3.17,8.15-2.12,2.24-4.72,3.37-7.83,3.37s-5.78-1.12-8.02-3.37c-2.24-2.24-3.37-4.96-3.37-8.15V20.07c0-3.45,.86-6.64,2.59-9.58,1.73-2.93,4.05-5.26,6.99-6.99,2.93-1.72,6.12-2.59,9.58-2.59h17.21c5.09,0,9.53,1.77,13.33,5.31l12.42,12.42c1.81,1.73,3.17,3.97,4.08,6.73,.91,2.76,1.36,5.74,1.36,8.93,0,3.71-.58,7.29-1.75,10.74-1.16,3.45-2.78,6.17-4.85,8.15l-11.26,10.74c-3.62,3.62-8.07,5.44-13.33,5.44h-13.98v10.87Zm21.74-56.69l-21.48-.26v23.42h21.48V23.57Z\"\n                        fill=\"#000000\" />\n                    <path class=\"logo_b\"\n                        d=\"M560.11,11.92c0-3.45,1.12-6.17,3.37-8.15,2.24-1.98,4.92-2.98,8.02-2.98s5.76,.97,7.96,2.91c2.2,1.94,3.3,4.64,3.3,8.09v28.47h21.35V11.92c0-3.45,1.14-6.17,3.43-8.15,2.29-1.98,4.98-2.98,8.09-2.98s5.76,.97,7.96,2.91,3.3,4.64,3.3,8.09V81.16c0,3.45-1.12,6.17-3.37,8.15-2.24,1.99-4.92,2.98-8.02,2.98s-5.78-.99-8.02-2.98c-2.24-1.98-3.37-4.7-3.37-8.15V43.5l-21.35,19.28v18.38c0,3.45-1.12,6.17-3.36,8.15-2.24,1.99-4.88,2.98-7.89,2.98s-5.78-.99-8.02-2.98c-2.24-1.98-3.37-4.7-3.37-8.15V11.92Z\"\n                        fill=\"#000000\" />\n                    <path class=\"logo_b\"\n                        d=\"M654.2,80.25c0,3.19-1.06,5.91-3.17,8.15-2.12,2.24-4.72,3.37-7.83,3.37s-5.78-1.12-8.02-3.37c-2.24-2.24-3.37-4.96-3.37-8.15V20.07c0-3.45,.86-6.64,2.59-9.58,1.73-2.93,4.05-5.26,6.99-6.99,2.93-1.72,6.12-2.59,9.58-2.59h17.21c5.09,0,9.53,1.77,13.33,5.31l12.42,12.42c1.81,1.73,3.17,3.97,4.08,6.73,.91,2.76,1.36,5.74,1.36,8.93,0,3.71-.58,7.29-1.75,10.74-1.16,3.45-2.78,6.17-4.85,8.15l-11.26,10.74c-3.62,3.62-8.07,5.44-13.33,5.44h-13.98v10.87Zm21.74-56.69l-21.48-.26v23.42h21.48V23.57Z\"\n                        fill=\"#000000\" />\n                    <g>\n                        <path class=\"c\"\n                            d=\"M142.76,32.66v-3.51c0-1.32,1.04-2.4,2.32-2.4,1.29,0,2.33,1.07,2.33,2.39v3.51l7.92-.02v-3.51c0-1.32,1.04-2.4,2.32-2.4,1.29,0,2.33,1.07,2.33,2.39v3.51h6.64v-3.53c0-1.32,1.03-2.4,2.32-2.4s2.33,1.07,2.33,2.39v3.51h6.83v-3.53c0-1.32,1.03-2.4,2.32-2.4,1.29,0,2.33,1.07,2.33,2.39v3.51l7.69-.02v-3.51c0-1.32,1.03-2.4,2.32-2.4s2.33,1.07,2.33,2.39v3.51h6.89v-.85c-.17-3.02-.89-5.52-2.14-7.51-1.25-1.98-3.22-4.1-5.89-6.34l-16.95-13.85c-3.19-2.67-6.13-4.01-8.8-4.01s-5.61,1.34-9.06,4.01l-16.57,13.85c-2.68,2.33-4.66,4.49-5.95,6.47-1.29,1.99-2.07,4.44-2.33,7.38v.97l8.46-.02Z\"\n                            fill=\"#000000\" />\n                        <path class=\"c\"\n                            d=\"M195.11,37.2v3.5c0,1.32-1.03,2.4-2.32,2.4s-2.33-1.07-2.33-2.39v-3.5l-7.69,.02v3.5c0,1.32-1.03,2.4-2.32,2.4s-2.33-1.07-2.33-2.39v-3.5h-6.83v3.51c0,1.32-1.04,2.4-2.32,2.4-1.29,0-2.33-1.07-2.33-2.39v-3.5h-6.64v3.51c0,1.32-1.03,2.4-2.32,2.4-1.29,0-2.33-1.07-2.33-2.39v-3.5l-7.92,.02v3.5c0,1.32-1.03,2.4-2.32,2.4-1.29,0-2.33-1.07-2.33-2.39v-3.5l-8.47,.02v43.43c0,3.11,1.12,5.74,3.37,7.89,2.24,2.16,4.92,3.24,8.02,3.24s5.76-1.08,7.96-3.24c2.2-2.16,3.3-4.79,3.3-7.89v-31.84l22.52,20.84v11c0,3.11,1.1,5.74,3.3,7.89,2.2,2.16,4.9,3.24,8.09,3.24s5.74-1.08,7.9-3.24,3.24-4.79,3.24-7.89V37.19h-6.88Z\"\n                            fill=\"#000000\" />\n                    </g>\n                </svg>\n                <span>Test page</span>\n            </h1>\n            <svg class=\"franky\" xmlns=\"http://www.w3.org/2000/svg\" id=\"Calque_1\" class=\"franky\"\n                viewBox=\"0 0 668.77 742.85\">\n                <defs>\n                    <style>\n                        .cls-1 {\n                            fill: #010101\n                        }\n\n                        .cls-3 {\n                            fill: #fff\n                        }\n\n                        .cls-4 {\n                            fill: #feffff\n                        }\n\n                        .cls-7 {\n                            fill: #b3cd38\n                        }\n\n                        .cls-8 {\n                            fill: #c3b2d3\n                        }\n\n                        .cls-9 {\n                            fill: #38276b\n                        }\n\n                        .cls-10 {\n                            fill: rgba(255, 255, 255, .6)\n                        }\n\n                        .cls-11 {\n                            fill: rgba(1, 1, 1, .19)\n                        }\n\n                        .cls-12 {\n                            opacity: .15\n                        }\n\n                        .cls-13 {\n                            opacity: .19\n                        }\n\n                        .cls-14,\n                        .cls-15 {\n                            opacity: .3\n                        }\n\n                        .cls-20 {\n                            opacity: .5\n                        }\n\n                        .cls-21 {\n                            opacity: .7\n                        }\n                    </style>\n                </defs>\n                <path\n                    d=\"m322.04 433.86-20.02 54.01s-102.75-3.02-160.97-43.78c9.22.24 20.68-2.07 26.69-13.28 10.15-18.96 34.85-56.38 66.66-61.26-1.05 2.76-1.54 5.53-1.32 8.35 2.52 30.64 88.97 55.96 88.97 55.96\"\n                    class=\"cls-7\" />\n                <path\n                    d=\"M566.16 490.35s-6.54-45.67 23.34-42.26c29.88 3.41 39.28-9.25 39.28-9.25s1.43 54.68-25.6 63.63c-19.28 6.39-31.15-8.29-31.15-8.29\"\n                    class=\"cls-9\" />\n                <path\n                    d=\"M628.74 443.89c-4.52 6.07-15.63 16.6-37.19 14.14-21.33-2.44-24.11 20.15-23.95 33.27l-1.44-.94s-6.55-45.67 23.33-42.26c29.89 3.4 39.28-9.25 39.28-9.25s.06 1.9-.04 5.05Z\"\n                    class=\"cls-3\" style=\"opacity:.35\" />\n                <g class=\"cls-12\">\n                    <path\n                        d=\"M603.18 502.46c-19.29 6.39-31.15-8.29-31.15-8.29l-5.86-3.82s-1.31-9.19-.08-19.02c3.82 5.08 10.79 11.09 22.42 11.45 18.84.58 32.85-21.94 40.16-37.22-.62 14.88-4.34 49.89-25.49 56.9Z\"\n                        class=\"cls-9\" />\n                </g>\n                <path\n                    d=\"M593.47 508.95c-15.51 0-24.74-11.15-25.21-11.73-.53-.65-.86-1.39-1-2.16-.14.03-.28.06-.42.08-2.65.39-5.1-1.46-5.47-4.1-.17-1.16-3.91-28.48 8.59-41.34 5.05-5.2 11.83-7.36 20.09-6.41 26.23 2.99 34.81-7.3 34.89-7.4 1.23-1.66 3.35-2.32 5.32-1.7 1.97.62 3.3 2.46 3.35 4.53.06 2.38 1.18 58.39-28.92 68.36-4.03 1.34-7.78 1.88-11.23 1.88Zm-21.45-19.61c1.41 0 2.81.61 3.77 1.8.39.47 10.13 11.94 25.86 6.74 15.92-5.27 20.97-32.15 22.06-49.42-6.77 3.38-17.84 6.38-34.76 4.45-5.21-.6-9.15.56-12.04 3.53-7.55 7.76-6.9 26.26-5.98 33.03.36-.08.73-.13 1.1-.13Z\"\n                    class=\"cls-9\" />\n                <path\n                    d=\"M260.37 518.13s-82.72-10.58-105.81 55.88c-20.99 60.42 37.01 110.5 37.01 110.5s-26.58 3.88-29.71 22.63c-3.11 18.67 6.99 37.47 87.19 27.57 47.73-5.89 24.18-79.93 24.18-88.84s52.1-15.84 124.98-35.65c12.28-3.34-137.84-92.09-137.84-92.09\"\n                    class=\"cls-7\" />\n                <path\n                    d=\"M210.7 742.24c-23.85 0-39.54-4.17-47.6-12.58-5.59-5.84-7.62-13.68-6.01-23.32 2.41-14.45 15.73-21.33 24.91-24.4-5.51-5.75-13.69-15.26-20.97-27.58-16.28-27.52-20.1-55.86-11.04-81.95 10.01-28.81 32.48-48.16 65-55.97 24.15-5.8 45.11-3.22 45.99-3.11.65.08 1.28.3 1.84.63.35.21 35.39 20.93 69.83 42.47 72.21 45.16 71.62 49.53 71.05 53.76-.31 2.26-1.92 4.05-4.23 4.67-26.69 7.25-50.78 12.83-70.14 17.31-24.09 5.57-46.9 10.85-51.2 14.49.19 1.49.74 4.32 1.24 6.87 3.27 16.86 10.07 51.93-4.41 71.87-5.8 7.99-14.32 12.73-25.31 14.08-14.8 1.83-27.77 2.74-38.94 2.74Zm37.22-219.89c-7.96 0-19.13.68-30.95 3.55-29.42 7.14-48.89 23.86-57.86 49.68-19.65 56.55 35.05 104.78 35.6 105.26 1.43 1.24 2 3.19 1.47 5.01a4.827 4.827 0 0 1-3.94 3.42c-.22.03-23.14 3.63-25.64 18.65-1.11 6.66-.01 11.44 3.46 15.06 6.49 6.77 25.37 13.48 78.39 6.93 8.34-1.03 14.45-4.36 18.68-10.18 12.02-16.55 5.76-48.84 2.75-64.36-1-5.16-1.48-7.72-1.48-9.52 0-9.09 13.15-12.54 58.75-23.09 17.79-4.12 39.59-9.16 63.68-15.57-15.59-13.32-76.22-51.37-132.02-84.38-1.7-.16-5.58-.47-10.89-.47Z\"\n                    class=\"cls-9\" />\n                <path\n                    d=\"M194.35 680.56s-.11-.07-.1-.06l.03.03s.02.03.08.06l.25.18c.22.13.4.26.65.39.46.26.98.51 1.51.76 1.08.47 2.21.93 3.4 1.31 2.37.78 4.86 1.45 7.42 1.96 2.55.54 5.15.93 7.78 1.29l3.96.44 3.99.34h.07c1.33.12 2.32 1.29 2.21 2.62a2.414 2.414 0 0 1-2.25 2.21l-4.13.27-4.15.16c-2.77.04-5.57.04-8.39-.13a68.18 68.18 0 0 1-8.54-.94c-1.44-.24-2.9-.58-4.38-.98-.75-.22-1.49-.44-2.27-.73-.38-.13-.79-.31-1.19-.47l-.64-.3c-.1-.04-.24-.12-.36-.19l-.19-.1-.31-.2a4.826 4.826 0 0 1-1.16-6.73 4.826 4.826 0 0 1 6.73-1.16\"\n                    class=\"cls-9\" />\n                <g class=\"cls-14\">\n                    <path\n                        d=\"M394.99 611.86c-71.84 19.52-123.2 26.36-123.2 35.14 0 3.23 3.14 15.15 4.78 29.22 2.79 24.18 1.13 54.69-28.61 58.37-79.06 9.75-89.02-8.79-85.95-27.18.26-1.57.7-3.03 1.27-4.4 3.46 6.68 13.39 18.52 40.32 19.15 45.29 1.08 60.21-9.6 61.7-26.37 1.63-18.18-11.24-54.29-20.85-70.96-15.63-27.09-40.53-49.67-30.69-88.81 1.32-5.21 4.3-9.64 8.52-13.39 20.28-3.67 36.84-1.55 36.84-1.55s147.99 87.49 135.89 90.78\"\n                        class=\"cls-1\" />\n                </g>\n                <path\n                    d=\"M484.61 526.42s40.06 5.29 63.65-22.27c25.82-30.17 43.37-28.78 44.75-20.41 1.38 8.37-13.05 12.94-25.66 37.79-11.87 23.39-27.73 43.79-64.39 39.02\"\n                    class=\"cls-7\" />\n                <g class=\"cls-12\">\n                    <path\n                        d=\"M567.35 521.54c-11.88 23.4-27.74 43.8-64.4 39.02l-18.35-34.13s2.79.37 7.31.41l1.88 6.4c34.89 12.24 54.62-4.44 71.08-24.86 11.68-14.52 22.29-19.99 28.14-24.57 1.31 8.34-13.09 12.95-25.67 37.74\"\n                        class=\"cls-1\" />\n                </g>\n                <path\n                    d=\"M513.16 566.07c-3.42 0-7.01-.23-10.82-.73a4.816 4.816 0 0 1-4.16-5.41c.34-2.64 2.77-4.52 5.41-4.16 31.92 4.16 46.85-11.55 59.47-36.42 6.88-13.55 14.16-21.23 19.47-26.84 3.83-4.04 5.99-6.4 5.73-7.98-.04-.23-.11-.65-1.15-.94-4.15-1.16-16.84 2.3-35.16 23.7-25.01 29.22-66.21 24.15-67.95 23.92a4.83 4.83 0 0 1-4.15-5.42c.35-2.64 2.77-4.51 5.41-4.15.38.05 37.8 4.56 59.36-20.62 17.01-19.87 33.86-29.85 45.09-26.72 4.37 1.22 7.39 4.46 8.08 8.67 1.05 6.38-3.05 10.71-8.24 16.19-4.91 5.18-11.63 12.27-17.87 24.57-10.78 21.25-26.18 42.35-58.5 42.35Z\"\n                    class=\"cls-9\" />\n                <path\n                    d=\"M622.64 301.98c-52 39.58-118.56 71.02-150.03 84.84-11.26 4.95-18.03 7.64-18.03 7.64l-.58-2.45-6.82-28.71-17.11-72.15-1.42-5.99c9.26-2.27 21.56-7.18 35.67-13.53 57.2-25.73 143.96-74.98 176.39-66.94 8.28 2.05 21.03 13.36 22.97 30.74 1.94 17.25-6.76 40.47-41.03 66.55\"\n                    class=\"cls-7\" />\n                <g class=\"cls-20\">\n                    <path\n                        d=\"M663.69 244.42c-.01-.06-.01-.13-.02-.19-1.94-17.38-14.69-28.69-22.97-30.74-39.88-9.88-161.87 66.83-210.08 79.96l-1.97-8.29c46.78-11.49 171.62-90.49 212.05-80.47 8.28 2.05 21.03 13.36 22.97 30.74.33 2.84.35 5.84.02 8.99\"\n                        class=\"cls-3\" />\n                </g>\n                <g class=\"cls-14\">\n                    <path\n                        d=\"M622.64 301.98c-70.6 53.74-168.06 92.48-168.06 92.48l-.58-2.45 4.47-41.31s81.64-5.72 136.45-41.2c40.93-26.51 60.98-53.65 68.75-74.06 1.94 17.25-6.76 40.47-41.03 66.55\"\n                        class=\"cls-1\" />\n                </g>\n                <path\n                    d=\"M454.58 399.29c-1.92 0-3.74-1.15-4.49-3.05-.98-2.48.23-5.29 2.7-6.27.97-.38 97.69-39.14 166.93-91.83 29.07-22.12 42.79-45.34 38.65-65.36-2.79-13.48-12.88-21.93-18.83-23.41-23.32-5.78-82.61 23.06-130.24 46.22-31.46 15.3-61.17 29.75-79.5 34.25-2.6.64-5.2-.95-5.84-3.54-.64-2.59.95-5.2 3.54-5.84 17.34-4.26 46.6-18.49 77.58-33.56 54.11-26.31 110.07-53.52 136.79-46.91 10.01 2.48 22.51 14.12 25.97 30.82 3.49 16.88-1 43.6-42.26 75-70.31 53.51-168.23 92.73-169.21 93.12-.58.23-1.19.34-1.78.34Z\"\n                    class=\"cls-9\" />\n                <path\n                    d=\"M481.95 613.06c-33.16 17.95-86.73 25.72-142.28 25.71-26.32 0-84.93-4.73-112.44-65.49-20.27-44.75-.28-76.35 5.02-144.97.04-.48.07-.97.11-1.45 0 0 106.53-124.29 165.69-138.2 13.43-3.16 23.89-1.69 32.01 2.49 27.66 14.17 28.4 59.55 28.4 59.55.12.31.24.63.36.95 4.12 10.69 8.8 22.59 13.78 35.17 15.56 39.21 34.06 85.09 47.55 121.94 14.83 40.57-3.54 85.5-38.21 104.29\"\n                    class=\"cls-7\" />\n                <path\n                    d=\"M481.95 613.06c-33.16 17.95-86.73 25.72-142.28 25.71-21.85 0-65.95-3.26-96.03-39.12 29.18 21.18 63.4 23.45 82.12 23.45 58.5.01 114.93-8.17 149.84-27.08 29.37-15.9 47.62-49.59 45.54-84.38 12.97 39.79-5.35 83.08-39.2 101.42\"\n                    class=\"cls-11\" />\n                <path\n                    d=\"M460.72 349.83c13.01 27.24 24.39 55.13 35.49 83.17 5.58 14.01 11.13 28.03 16.59 42.11 2.73 7.04 5.43 14.09 8.09 21.16l3.96 10.62 1.81 5.7c.29.96.63 1.9.87 2.86l.63 2.93c1.9 7.76 2.42 15.82 2.13 23.77-.2 7.99-1.81 15.88-4.09 23.5-2.24 7.66-5.77 14.9-9.94 21.69-4.13 6.84-9.39 12.99-15.27 18.42l-2.24 2.02-1.12 1.01c-.38.33-.8.61-1.19.92l-4.83 3.61c-1.67 1.12-3.42 2.12-5.13 3.18-.86.5-1.71 1.1-2.59 1.51l-2.63 1.33c-3.48 1.86-7.09 3.32-10.66 4.91-3.64 1.36-7.24 2.86-10.93 4.01-14.68 4.94-29.72 8.22-44.83 10.56-15.11 2.35-30.31 3.82-45.51 4.58-7.6.37-15.21.64-22.81.7-7.54.12-15.38.07-23.11-.63-15.49-1.34-30.99-4.86-45.41-11.37-7.19-3.27-14.07-7.29-20.44-12.01-6.39-4.71-12.18-10.22-17.33-16.24-5.15-6.03-9.62-12.6-13.38-19.51-.94-1.73-1.89-3.45-2.7-5.24-.83-1.78-1.75-3.51-2.44-5.37l-2.21-5.5-.55-1.38c-.16-.47-.3-.95-.45-1.42l-.87-2.85-.87-2.85-.43-1.42-.32-1.46-1.24-5.84c-.24-.97-.35-1.96-.46-2.95l-.37-2.96-.36-2.96c-.13-.99-.11-1.98-.17-2.96l-.24-5.93.15-5.89c0-1.97.21-3.9.37-5.84.19-1.93.28-3.89.54-5.8 1.84-15.35 5.01-30.12 7.52-44.87 2.58-14.74 4.62-29.5 5.81-44.35a4.83 4.83 0 0 1 5.2-4.44c2.64.21 4.62 2.51 4.43 5.15-1.08 15.21-3.01 30.36-5.51 45.33-2.47 14.97-5.54 29.72-7.2 44.32-1.59 14.56-1.24 29.03 3.23 42.6l.79 2.55c.14.42.25.85.4 1.27l.51 1.24 2.03 4.98c.62 1.68 1.49 3.27 2.26 4.9.76 1.64 1.63 3.21 2.5 4.79 3.48 6.3 7.53 12.24 12.17 17.67 4.65 5.42 9.85 10.36 15.59 14.59a101.9 101.9 0 0 0 18.47 10.83c13.09 5.9 27.39 9.18 41.9 10.42 7.28.66 14.53.7 22.06.58 7.47-.07 14.92-.33 22.37-.7 14.89-.76 29.74-2.21 44.41-4.49l5.5-.88 5.47-1.01c3.66-.62 7.25-1.48 10.87-2.23 7.18-1.7 14.34-3.51 21.27-5.88 3.51-1.07 6.88-2.47 10.31-3.73 3.32-1.47 6.71-2.81 9.89-4.51l2.42-1.21c.83-.38 1.52-.89 2.29-1.33 1.49-.92 3.03-1.77 4.49-2.75l4.24-3.14c.35-.27.72-.51 1.05-.79l.98-.88 1.97-1.76c5.18-4.75 9.86-10.15 13.54-16.21 3.71-6.01 6.91-12.42 8.91-19.24 2.05-6.77 3.54-13.78 3.73-20.88.27-7.07-.14-14.21-1.81-21.1l-.55-2.6c-.21-.86-.51-1.69-.77-2.54l-1.58-5.08-3.89-10.57c-2.62-7.04-5.28-14.08-7.97-21.1-5.38-14.05-10.86-28.08-16.37-42.09-11.07-28.01-22.26-56.02-30.93-85.03-.38-1.28.35-2.63 1.62-3.01 1.16-.35 2.38.21 2.88 1.27Z\"\n                    class=\"cls-9\" />\n                <path\n                    d=\"M511.48 499.78c22.09 38.73 74.9 175.05 91.89 181.82 23.59 9.4 58.1 48.69-39.78 55.83-79.01 5.77-96.14-97-132.6-127.66\"\n                    class=\"cls-7\" />\n                <path\n                    d=\"M563.58 737.44c-79 5.77-96.12-96.99-132.6-127.66 0 0 21.51 4.44 50.19 51.58 18.31 30.09 51.97 58.16 86.66 55.63 32.63-2.39 50.54-8.34 59.06-15.56 7.36 14.45-2.02 31.53-63.32 36\"\n                    class=\"cls-1\" style=\"opacity:.25\" />\n                <path d=\"M511.41 491c-1.16-2.37 5.21-.45 6.52 1.83\" class=\"cls-9\" />\n                <path\n                    d=\"M517.93 492.82c2.79 4.87 5.26 9.75 7.65 14.67 2.38 4.91 4.69 9.84 6.92 14.78 4.47 9.89 8.77 19.82 13.04 29.75 8.52 19.87 16.95 39.74 25.54 59.48 4.31 9.87 8.68 19.69 13.26 29.37 2.29 4.84 4.64 9.64 7.07 14.36 2.44 4.7 4.98 9.36 7.72 13.67 1.37 2.14 2.82 4.22 4.27 5.87.7.81 1.49 1.55 1.86 1.76l.07.06h.02l.11.06.26.11.52.22c.69.31 1.41.58 2.07.92 5.4 2.6 10.29 5.91 14.71 9.98 2.22 2.02 4.25 4.33 6.09 6.87 1.84 2.53 3.43 5.46 4.4 8.81.99 3.32 1.23 7.19.28 10.85-.28.89-.52 1.81-.88 2.66-.4.82-.81 1.64-1.23 2.45-.45.78-.99 1.46-1.5 2.19-.5.75-1.08 1.35-1.66 1.96-4.7 4.89-10.2 7.65-15.57 9.81-5.42 2.1-10.89 3.64-16.36 4.82-10.95 2.39-21.9 3.54-32.85 4.34-11.39.77-23.13-.69-34.01-4.72-5.38-2.12-10.62-4.64-15.38-7.85-2.46-1.5-4.69-3.28-6.97-5-1.16-.84-2.17-1.84-3.26-2.75-1.05-.95-2.18-1.82-3.16-2.84-8.18-7.82-15.15-16.62-21.27-25.79-6.15-9.16-11.62-18.64-16.87-28.12l-7.75-14.22c-2.53-4.73-5.09-9.44-7.71-14.05-2.64-4.6-5.3-9.18-8.27-13.49-2.92-4.34-6.09-8.47-9.67-12.2l-.04-.04c-.93-.96-.9-2.5.07-3.42.85-.82 2.15-.89 3.08-.23 4.65 3.28 8.59 7.36 12.23 11.58 1.82 2.13 3.48 4.33 5.15 6.55.83 1.11 1.59 2.25 2.38 3.38.8 1.13 1.57 2.27 2.32 3.42.75 1.15 1.52 2.28 2.25 3.44l2.15 3.49c1.44 2.32 2.82 4.66 4.21 7l8.03 14.07c5.27 9.37 10.66 18.58 16.6 27.32 5.9 8.76 12.5 16.92 19.98 23.99.9.93 1.92 1.7 2.87 2.55.99.81 1.91 1.72 2.94 2.45 2.04 1.51 4.03 3.11 6.21 4.4 4.21 2.82 8.78 4.95 13.44 6.77 9.42 3.4 19.55 4.64 29.7 3.91 10.57-.83 21.15-2.03 31.29-4.31 5.07-1.13 10.01-2.57 14.65-4.39 4.6-1.82 8.9-4.26 11.66-7.23 1.39-1.47 2.38-3.01 2.73-4.6.4-1.58.34-3.28-.19-5.12-.52-1.83-1.5-3.76-2.83-5.58-1.33-1.82-2.86-3.61-4.64-5.22-3.51-3.26-7.66-6.07-11.98-8.15-.54-.28-1.09-.48-1.63-.73l-.95-.42c-.3-.14-.6-.29-.89-.44l-.44-.24-.35-.23c-1.91-1.26-3.02-2.48-4.1-3.69-2.11-2.41-3.72-4.8-5.27-7.2-3.05-4.8-5.67-9.65-8.2-14.53-2.51-4.88-4.9-9.78-7.23-14.7-4.64-9.85-9.04-19.75-13.37-29.68-8.65-19.85-24.55-59.57-24.55-59.57l-25.24-56.42 6.45-6.96Z\"\n                    class=\"cls-9\" />\n                <g style=\"opacity:.4\">\n                    <ellipse cx=\"304.82\" cy=\"532.72\" rx=\"66.87\" ry=\"81.46\" style=\"fill:#eeeff3\"\n                        transform=\"rotate(-10.12 304.677 532.525)\" />\n                </g>\n                <path\n                    d=\"M478.89 267.79s-66.49 20.77-66.64 21.09c-28.79 63.49 93.81 118.78 93.81 118.78s-63.19-90.07-27.18-139.87Z\"\n                    class=\"cls-3\" />\n                <g class=\"cls-13\">\n                    <path\n                        d=\"M415.22 287.79s.52-.21 1.42-.51c-3.71 39.9 46.24 74.77 76.22 91.96 8.48 16.38 16.17 27.34 16.17 27.34s-122.6-55.29-93.81-118.78Z\"\n                        class=\"cls-1\" />\n                </g>\n                <path\n                    d=\"m517.46 417.09-13-5.86c-.8-.36-19.88-9.02-41.42-23.53-12.7-8.55-23.58-17.36-32.34-26.18-11.12-11.19-18.87-22.46-23.03-33.51-5.26-13.94-4.92-27.65 1.02-40.75.71-1.57 1.96-2 3.07-2.37.51-.17 1.25-.42 2.19-.73 1.75-.57 4.3-1.39 7.6-2.44 5.54-1.76 13.25-4.2 22.91-7.24 16.45-5.17 33.1-10.37 33.27-10.43l11.23-3.51-6.9 9.54c-7.68 10.61-11.11 24.19-10.21 40.36.74 13.25 4.36 28.23 10.77 44.53 11 27.98 26.5 50.22 26.66 50.44l8.19 11.68Zm-102.29-125.1c-4.41 10.68-4.48 21.86-.18 33.25 8.62 22.85 33.21 43.01 52.32 55.9 10.43 7.03 20.42 12.72 27.84 16.68-5.65-9.44-13.41-23.67-19.82-39.98-6.71-17.08-10.51-32.88-11.29-46.96-.77-13.77 1.35-25.93 6.31-36.31-22.09 6.91-48.71 15.28-55.18 17.43Z\"\n                    class=\"cls-9\" />\n                <path\n                    d=\"M169.61 365.65s66.34-21.26 66.64-21.09c60.08 35.37-8.4 151.13-8.4 151.13s-.12-110.02-58.23-130.04Z\"\n                    class=\"cls-3\" />\n                <g class=\"cls-13\">\n                    <path\n                        d=\"M233.19 345.39s-.54.13-1.45.4c25.99 30.5 5.19 87.76-9.45 119.07 2.48 18.28 2.5 31.66 2.5 31.66s68.48-115.75 8.4-151.13Z\"\n                        class=\"cls-1\" />\n                </g>\n                <path\n                    d=\"m223.95 509.97-.02-14.27c0-.27-.12-27.51-7.25-56.73-4.14-16.99-9.8-31.3-16.81-42.54-8.56-13.71-19.16-22.82-31.53-27.08l-11.13-3.83 11.21-3.59c.17-.05 16.78-5.38 33.21-10.61 9.65-3.07 17.36-5.52 22.91-7.26 3.3-1.04 5.86-1.84 7.62-2.37.94-.29 1.69-.51 2.21-.66 1.12-.33 2.39-.69 3.87.18 12.39 7.3 20.56 18.31 24.28 32.74 2.95 11.43 3.09 25.11.44 40.66-2.09 12.25-5.93 25.71-11.39 40.02-9.27 24.25-19.9 42.31-20.34 43.07l-7.26 12.28ZM180.5 366.28c10.02 5.64 18.75 14.37 26.05 26.07 7.46 11.97 13.44 27.08 17.77 44.91 4.14 17.04 5.98 33.13 6.79 44.11 3.8-7.51 8.69-17.91 13.18-29.66 8.22-21.54 16.74-52.17 10.64-75.82-3.04-11.79-9.52-20.89-19.27-27.09-6.53 1.97-33.12 10.43-55.16 17.48Z\"\n                    class=\"cls-9\" />\n                <path\n                    d=\"M395.02 59.33s26.76-42.86 58.49-52.9 60.14 10.55 71.13 50.87c10.98 40.31 7.58 76.3 17.7 102.68 10.12 26.38 51.11 53.06 31.65 88.8-19.46 35.74-42.07 55.92-87.69 52.01-45.62-3.91-126.77-17.28-126.77-17.28l35.49-224.18Z\"\n                    class=\"cls-7\" />\n                <path\n                    d=\"M498.05 299.26c-19.14-3.32-45.16.19-67.14-5.21-.64-12.75-2.5-23.36.18-34.97 16.05-69.61 73.52 22.89 109.73-18.21 17.11-19.41-43.12-150.1-46.9-195.72-1.88-22.64 6.32-25.4 15.05-22.45 6.71 9.29 12.32 21.32 16.34 35.59 11.63 41.36 10.92 77.5 21 104.75s54.75 59.48 27.68 85.73c-25.09 24.32-37.4 57.16-75.94 50.48Z\"\n                    class=\"cls-11\" />\n                <path\n                    d=\"M454.82 28.32c-22.44 7.1-36.87 27.08-36.2 47.73a42.196 42.196 0 0 1-4.97-10.47c-7.67-24.25 7.66-50.74 34.24-59.15 26.59-8.41 54.36 4.43 62.04 28.68 1.2 3.78 1.83 7.61 1.96 11.42-11.33-17.27-34.62-25.32-57.06-18.22Z\"\n                    class=\"cls-10\" />\n                <path\n                    d=\"M497.01 305.18c-3.56 0-7.24-.16-11.05-.49-45.24-3.88-126.26-17.19-127.07-17.32l-3.84-.63 36.22-228.8.42-.67c.28-.45 6.99-11.14 17.74-23.22 14.62-16.42 29.05-26.97 42.89-31.35 14.35-4.54 28.55-3.33 41.07 3.49 16.18 8.83 28.62 26.61 35.01 50.08 5.46 20.06 7.44 39.22 9.18 56.14 1.83 17.81 3.42 33.2 8.4 46.17 2.98 7.75 8.95 15.74 15.26 24.2 7.22 9.66 14.69 19.65 18.76 30.68 4.8 13 3.96 25.17-2.59 37.2-11.2 20.58-22.1 33.62-35.33 42.28-12.54 8.21-27.37 12.25-45.08 12.25ZM364 280.27c16.63 2.68 83.32 13.25 122.63 16.62 10.89.93 20.4.48 29.05-1.38 8.19-1.76 15.42-4.75 22.12-9.13 12.09-7.91 22.19-20.08 32.74-39.47 5.48-10.07 6.16-19.84 2.13-30.75-3.68-9.96-10.8-19.49-17.69-28.7-6.65-8.9-12.94-17.31-16.3-26.08-5.35-13.94-7.06-30.57-8.88-48.18-1.71-16.61-3.65-35.44-8.95-54.88-5.83-21.39-16.91-37.47-31.21-45.26-10.6-5.78-22.69-6.78-34.96-2.9-27.9 8.83-52.46 45.26-55.94 50.6l-34.75 219.52Z\"\n                    class=\"cls-9\" />\n                <path\n                    d=\"M117.12 154.36s-46.54-19.67-78.28-9.63C7.11 154.77-4.29 187.96 9.92 227.25s37.7 66.77 44.59 94.17-11.28 72.8 25.2 90.85c36.48 18.04 66.58 21.54 101.64-7.9 35.06-29.44 93.75-87.06 93.75-87.06L117.12 154.36Z\"\n                    class=\"cls-7\" />\n                <path\n                    d=\"M164.51 417.58c13.75-13.72 43.42-33.51 58.28-50.57-6.82-10.8-11.39-20.55-20.26-28.5-53.17-47.71-46.97 61.02-100.23 48.23-25.16-6.04-51.07-147.58-74.22-187.07-11.49-19.6-19.78-17.15-25.22-9.7-.14 11.46 2.18 24.53 7.11 38.51 14.28 40.53 35.65 69.68 43.08 97.76s-9.68 73.92 26.59 84.24c22.61 6.43 67.59 24.35 84.88 7.1Z\"\n                    class=\"cls-11\" />\n                <path\n                    d=\"M50.37 163.4c22.44-7.1 45.73.94 57.06 18.22a42.04 42.04 0 0 0-1.96-11.42c-7.67-24.25-35.45-37.1-62.04-28.68-26.59 8.41-41.92 34.89-34.24 59.15 1.2 3.78 2.88 7.28 4.97 10.47-.67-20.65 13.75-40.63 36.2-47.73Z\"\n                    class=\"cls-10\" />\n                <path\n                    d=\"M128.66 430.09c-15.03 0-30.8-4.47-50.68-14.3-12.28-6.07-19.97-15.54-23.52-28.94-3.01-11.36-2.65-23.83-2.3-35.88.31-10.55.59-20.52-1.43-28.57-3.39-13.48-10.95-26.97-19.69-42.6-8.3-14.84-17.72-31.65-24.78-51.2-8.27-22.87-8.33-44.57-.17-61.1 6.31-12.78 17.23-21.94 31.59-26.48 13.84-4.38 31.71-4.05 53.11.97 15.74 3.7 27.38 8.58 27.87 8.78l.73.31.55.57 160.7 165.74-2.77 2.72c-.59.58-59.2 58.07-93.98 87.27-17.45 14.65-34.68 22.07-52.68 22.67-.84.03-1.69.04-2.53.04ZM59.48 145.8c-6.77 0-13.42.76-19.45 2.67-12.27 3.88-21.58 11.66-26.93 22.49-7.21 14.6-7.03 34.13.51 54.98 6.85 18.95 16.1 35.46 24.25 50.04 8.64 15.44 16.81 30.03 20.45 44.51 2.29 9.11 1.99 19.6 1.67 30.71-.33 11.5-.68 23.39 2.04 33.65 2.98 11.24 9.15 18.85 19.43 23.93 19.79 9.79 35.04 13.93 49.49 13.45 8-.27 15.64-1.99 23.35-5.26 8.15-3.46 16.18-8.55 24.56-15.59 30.22-25.37 78.69-72.38 90.75-84.14L114.88 157.68c-4.63-1.85-30.85-11.88-55.41-11.88Z\"\n                    class=\"cls-9\" />\n                <path\n                    d=\"M491.19 588.11c-159.82-46.08-241.51-209.96-241.51-209.98-152.04-83.46-144.45-179.68-83.33-243.45l205.24-64.94c63.52 19.38 101.89 88.17 52.99 232.85-41.18 146.84 82.52 225.46 82.52 225.46l-46.14 7.42 30.23 52.64Z\"\n                    class=\"cls-7\" />\n                <path\n                    d=\"M213.78 119.08c-84.76 61.25-101.14 132.02 72.53 227.35 0 .02 44 156.39 172.96 193.69 5.86 3.37 26.35 43.29 26.35 43.29-157.58-45.83-234.06-208.69-234.06-208.7C58.1 268.52 108.92 173.45 213.78 119.09Z\"\n                    class=\"cls-11\" />\n                <path\n                    d=\"m436.86 132.87 11.74 37.1 22.71-7.19 1.79 5.65c2.69 8.49 11.76 13.21 20.26 10.52l9.83-3.11c8.49-2.69 13.19-11.76 10.5-20.25l-15.05-47.56c-2.69-8.49-11.75-13.2-20.24-10.51l-9.83 3.11c-8.49 2.69-13.21 11.75-10.52 20.24l1.52 4.81-22.71 7.19Z\"\n                    class=\"cls-8\" />\n                <g class=\"cls-21\">\n                    <path\n                        d=\"m452.99 130.61 1.52 4.81-15.31 4.85-2.34-7.4 15.55-4.92c.12.89.3 1.78.58 2.66ZM508.63 165.33l-15.05-47.56c-2.69-8.49-11.75-13.2-20.24-10.51l-9.83 3.11a16.02 16.02 0 0 0-6.06 3.53c.8-6.01 4.96-11.32 11.12-13.27l9.83-3.11c8.49-2.69 17.55 2.02 20.24 10.51l15.05 47.56c1.95 6.16 0 12.62-4.46 16.73.3-2.28.14-4.66-.6-6.99Z\"\n                        class=\"cls-3\" />\n                </g>\n                <g class=\"cls-15\">\n                    <path\n                        d=\"m447.22 129.59 2.59 8.2c2.98 9.42 13.04 14.64 22.46 11.66 3.12-.99 6.45.74 7.44 3.86 2.69 8.5 11.76 13.21 20.26 10.52l9.82-3.11c1.66-.53 3.17-1.3 4.51-2.26.95 7.58-3.58 14.99-11.12 17.38l-9.82 3.11c-8.5 2.69-17.57-2.02-20.26-10.52l-1.79-5.65-22.71 7.19-11.74-37.1 10.36-3.28Z\"\n                        class=\"cls-9\" />\n                </g>\n                <path\n                    d=\"m467.92 170.07-.15-.47-17.53 5.55a5.44 5.44 0 0 1-6.82-3.54l-11.74-37.1c-.91-2.86.68-5.91 3.54-6.82l17.54-5.55c-3.33-11.23 2.96-23.14 14.18-26.69l9.83-3.11c11.33-3.59 23.47 2.72 27.06 14.05l15.05 47.56c3.59 11.34-2.71 23.48-14.04 27.07l-9.83 3.11c-11.34 3.59-23.49-2.72-27.07-14.06Zm2.29-64.26c-5.63 1.78-8.76 7.8-6.98 13.43l1.52 4.81c.91 2.86-.68 5.91-3.54 6.82l-17.53 5.55 8.46 26.74 17.53-5.55c2.86-.9 5.91.68 6.82 3.54l1.79 5.65c1.78 5.63 7.81 8.76 13.44 6.98l9.83-3.11c5.62-1.78 8.75-7.81 6.96-13.44l-15.05-47.56c-1.78-5.62-7.8-8.75-13.42-6.97l-9.83 3.11Z\"\n                    class=\"cls-9\" />\n                <path\n                    d=\"m129.88 256 315.53-99.84c2.14-.68 3.33-2.96 2.65-5.1s-2.96-3.33-5.1-2.65l-315.53 99.84a4.063 4.063 0 0 0-2.65 5.1 4.063 4.063 0 0 0 5.1 2.65Z\"\n                    class=\"cls-9\" />\n                <path\n                    d=\"M419.91 180.48a5.816 5.816 0 0 0 3.79-7.29l-8.53-26.94a5.816 5.816 0 0 0-7.29-3.79 5.816 5.816 0 0 0-3.79 7.29l8.53 26.94a5.816 5.816 0 0 0 7.29 3.79ZM372.24 195.56a5.816 5.816 0 0 0 3.79-7.29l-8.53-26.94a5.816 5.816 0 0 0-7.29-3.79 5.816 5.816 0 0 0-3.79 7.29l8.53 26.94a5.816 5.816 0 0 0 7.29 3.79ZM324.56 210.65a5.816 5.816 0 0 0 3.79-7.29l-8.53-26.94a5.816 5.816 0 0 0-7.29-3.79 5.816 5.816 0 0 0-3.79 7.29l8.53 26.94a5.816 5.816 0 0 0 7.29 3.79ZM164.72 261.23a5.816 5.816 0 0 0 3.79-7.29L159.98 227a5.816 5.816 0 0 0-7.29-3.79 5.816 5.816 0 0 0-3.79 7.29l8.53 26.94a5.816 5.816 0 0 0 7.29 3.79ZM212.4 246.14a5.816 5.816 0 0 0 3.79-7.29l-8.53-26.94a5.816 5.816 0 0 0-7.29-3.79 5.816 5.816 0 0 0-3.79 7.29l8.53 26.94a5.816 5.816 0 0 0 7.29 3.79ZM260.07 231.06a5.816 5.816 0 0 0 3.79-7.29l-8.53-26.94a5.816 5.816 0 0 0-7.29-3.79 5.816 5.816 0 0 0-3.79 7.29l8.53 26.94a5.816 5.816 0 0 0 7.29 3.79Z\"\n                    class=\"cls-9\" />\n                <path\n                    d=\"m125.35 236.76 11.74 37.1-22.71 7.19 1.79 5.65c2.69 8.49-2.03 17.57-10.52 20.26l-9.83 3.11c-8.49 2.69-17.56-2.03-20.24-10.52l-15.05-47.56c-2.69-8.49 2.02-17.55 10.51-20.24l9.83-3.11c8.49-2.69 17.57 2.01 20.25 10.5l1.52 4.81 22.71-7.19Z\"\n                    class=\"cls-8\" />\n                <g class=\"cls-21\">\n                    <path\n                        d=\"m110.86 244.2 1.52 4.81 15.31-4.85-2.34-7.4-15.55 4.92c.41.8.77 1.63 1.05 2.52ZM85.32 304.6l-15.05-47.56c-2.69-8.49 2.02-17.55 10.51-20.24l9.83-3.11c2.34-.74 4.71-.9 6.99-.6-4.11-4.46-10.57-6.41-16.73-4.46l-9.83 3.11c-8.49 2.69-13.2 11.75-10.51 20.24l15.05 47.56c1.95 6.16 7.26 10.32 13.27 11.12a16.096 16.096 0 0 1-3.53-6.06Z\"\n                        class=\"cls-3\" />\n                </g>\n                <g class=\"cls-15\">\n                    <path\n                        d=\"m115 240.04 2.59 8.2c2.98 9.42-2.24 19.48-11.66 22.46a5.927 5.927 0 0 0-3.86 7.44c2.69 8.5-2.02 17.57-10.52 20.26l-9.82 3.11c-1.66.53-3.34.76-4.99.75 3.58 6.74 11.56 10.2 19.09 7.81l9.82-3.11c8.5-2.69 13.21-11.76 10.52-20.26l-1.79-5.65 22.71-7.19-11.74-37.1-10.36 3.28Z\"\n                        class=\"cls-9\" />\n                </g>\n                <path\n                    d=\"m107.29 312.14-9.83 3.11c-11.33 3.59-23.47-2.72-27.06-14.06l-15.05-47.56c-3.59-11.33 2.72-23.47 14.05-27.06l9.83-3.11c11.22-3.55 23.22 2.58 26.95 13.68l17.54-5.55c2.86-.9 5.91.68 6.82 3.54l11.74 37.1c.91 2.86-.68 5.91-3.54 6.82l-17.53 5.55.15.47c3.59 11.34-2.72 23.49-14.06 27.08Zm-34.61-75.21c-5.62 1.78-8.75 7.8-6.97 13.42l15.05 47.56c1.78 5.63 7.8 8.76 13.43 6.98l9.83-3.11c5.63-1.78 8.76-7.81 6.98-13.44l-1.79-5.65c-.91-2.86.68-5.91 3.54-6.82l17.53-5.55-8.46-26.74-17.53 5.55a5.44 5.44 0 0 1-6.82-3.54l-1.52-4.81c-1.78-5.62-7.81-8.75-13.43-6.96l-9.83 3.11ZM345.59 396.05c-19.9 10.03-31.58 28.01-28.61 42.47-1.24-1.38-2.3-2.9-3.15-4.58-7.59-15.07 4.41-36.44 26.81-47.73s46.71-8.22 54.31 6.86a20.81 20.81 0 0 1 1.81 5.25c-9.85-10.99-31.26-12.31-51.16-2.28Z\"\n                    class=\"cls-9\" />\n                <path\n                    d=\"M361.94 425.47c-15.45 7.79-24.25 22.28-21.59 34.21-10.52-11.58-2.16-32.76 17.47-42.39 19.42-10.05 41.42-4.17 44.46 11.18-8-9.24-24.89-10.79-40.34-3ZM383.72 455.91c-11.87 5.98-18.66 17.04-16.66 26.12-8.03-8.82-1.53-24.93 13.54-32.33 14.91-7.7 31.73-3.34 34.03 8.36-6.1-7-19.04-8.13-30.9-2.15ZM172.17 213.6l-33.05-41.54c7.89-14.07 18.4-27.21 30.77-39.1l7.18-2.27 11.16-3.53 25.96-8.21 11.69-3.7 18.96-6 17.61-5.57 20.04-6.34 13.85-4.38 22.73-7.19 13.85-4.38 20.04-6.34 18.84-3.11c24.42 6 38.61 18.82 53.3 39.84l-3.35 22.87-33.9-42.61-3.22 54.35-33.41-41.99-3.17 53.56-30.95-38.9-2.94 49.63-33.41-41.99-3.17 53.56-34.39-43.23-3.26 55.14-28-35.2-2.66 44.9-33.9-42.61-3.22 54.35ZM138.7 172.85l-3.1 52.32-10.76-13.53c2.28-13.49 7.07-26.49 13.86-38.79Z\"\n                    class=\"cls-9\" />\n                <path\n                    d=\"m499.4 594.55-9.29-2.68c-36.89-10.64-72.68-28.41-106.37-52.84-26.9-19.5-52.54-43.26-76.19-70.62-21.03-24.33-36.57-47.06-45.91-61.85-5.3-8.39-9.2-15.17-11.55-19.38-1.72-3.09-2.76-4.99-3.38-6.21-38.19-21.09-68.15-44.05-89.04-68.28-19.39-22.48-31.11-46.14-34.82-70.31-3.03-19.77-.7-39.69 6.94-59.21 7.03-17.96 18.38-35.19 33.73-51.2l.69-.72.95-.3 206.4-65.31 1.16.35c13.75 4.2 25.99 10.54 36.39 18.84 11.05 8.84 20.06 19.94 26.76 33 10.51 20.46 15.45 45.82 14.71 75.39-.8 31.96-8.3 69.15-22.27 110.52-9.18 32.79-10.67 64.76-4.43 95.05 5 24.26 14.95 47.52 29.57 69.15 13.06 19.31 27.5 33.72 37.32 42.41 10.67 9.44 18.34 14.35 18.41 14.4l8.68 5.53-50.7 8.16 32.23 56.12ZM253.58 377.17c2.98 5.79 23.18 43.8 60.42 86.75 23.23 26.78 48.37 50.02 74.72 69.07 29.96 21.66 61.58 37.94 94.14 48.49l-28.12-48.96 42.35-6.81a215.74 215.74 0 0 1-11.27-9.29c-10.16-8.97-25.12-23.85-38.67-43.82-15.25-22.49-25.65-46.73-30.89-72.02-6.55-31.61-5.02-64.92 4.54-99.03l.06-.2c13.74-40.66 21.11-77.1 21.9-108.31.71-28.24-3.95-52.34-13.85-71.62-6.2-12.07-14.51-22.32-24.69-30.46-9.33-7.46-20.3-13.21-32.63-17.11l-203.11 64.27c-14.29 15.06-24.85 31.18-31.4 47.92-7.13 18.22-9.32 36.78-6.5 55.17 3.48 22.69 14.59 45.03 33.01 66.38 20.48 23.74 50.08 46.33 87.97 67.13l1.99 1.09.02 1.37Z\"\n                    class=\"cls-9\" />\n                <path\n                    d=\"M315.85 249.39c9.33 19.11 31.49 29.02 52.38 22.41 20.89-6.61 33.31-27.46 29.95-48.46l-82.33 26.05Z\"\n                    class=\"cls-3\" />\n                <path\n                    d=\"M369.65 276.3a48.368 48.368 0 0 1-33.82-1.73c-10.58-4.59-19.18-12.8-24.22-23.11l-2.4-4.92 92.77-29.35.86 5.41c1.81 11.33-.5 22.99-6.52 32.83a48.367 48.367 0 0 1-26.67 20.87Zm-46.77-24.18c4.12 6.04 9.9 10.84 16.71 13.79 8.6 3.73 18.27 4.22 27.22 1.39s16.57-8.8 21.46-16.8a38.94 38.94 0 0 0 5.73-20.89l-71.12 22.5Z\"\n                    class=\"cls-9\" />\n                <circle cx=\"359.31\" cy=\"245.38\" r=\"10.66\" class=\"cls-9\" />\n                <path\n                    d=\"M213.64 282.92c9.33 19.11 31.49 29.02 52.38 22.41 20.89-6.61 33.31-27.46 29.95-48.46l-82.33 26.05Z\"\n                    class=\"cls-3\" />\n                <path\n                    d=\"M267.44 309.83a48.368 48.368 0 0 1-33.82-1.73c-10.58-4.59-19.18-12.8-24.22-23.11l-2.4-4.92 92.77-29.35.86 5.41c1.81 11.33-.5 22.99-6.52 32.83a48.367 48.367 0 0 1-26.67 20.87Zm-46.77-24.18c4.12 6.04 9.9 10.84 16.71 13.79 8.6 3.73 18.27 4.22 27.22 1.39s16.57-8.8 21.46-16.8a38.94 38.94 0 0 0 5.73-20.89l-71.12 22.5Z\"\n                    class=\"cls-9\" />\n                <circle cx=\"257.1\" cy=\"278.91\" r=\"10.66\" class=\"cls-9\" />\n                <path\n                    d=\"M189.64 276.54c84.13-24.9 168.23-49.85 252.33-74.84 1.82-.54 2.28-.04 2.73 1.6 1.8 6.37 3.64 12.72 5.64 19.01.58 1.83.16 2.39-1.61 2.9-10.25 2.95-20.46 6.06-30.71 9.02-1.59.47-2 1.05-1.43 2.64.88 2.48 1.39 5.09 2.31 7.53.71 1.89.07 2.48-1.67 2.91-2.63.63-5.17 1.57-7.8 2.25-1.31.33-1.63.85-1.2 2.15.9 2.73 1.57 5.52 2.5 8.23.51 1.45.1 1.94-1.3 2.29-2.55.64-5.01 1.56-7.56 2.17-1.73.42-2.36 1.01-1.66 2.89.96 2.52 1.51 5.2 2.38 7.76.48 1.37.19 1.9-1.24 2.32a7385.42 7385.42 0 0 0-43.28 12.83c-1.29.39-1.82.21-2.17-1.14-.73-2.85-1.7-5.64-2.45-8.49-.35-1.34-.91-1.58-2.18-1.15-2.64.88-5.36 1.52-7.99 2.44-1.52.52-2.24.31-2.68-1.37-.86-3.23-1.95-6.39-2.95-9.57-3.35.99-6.72 1.94-10.04 3.01-1.08.35-1.56.18-1.87-.95-.92-3.38-1.93-6.73-2.9-10.09l.02.02s0-.01-.01-.02c-.14-.06-.32-.21-.42-.17-7.32 2.12-14.61 4.3-21.93 6.41-1.41.41-.71 1.31-.66 2.08.95 3.21 1.9 6.41 2.84 9.62-.03.96-.89.73-1.39.88-3.28 1.03-6.57 1.99-9.86 2.97h-.01c.91 3.22 1.75 6.45 2.75 9.63.45 1.41.34 2.12-1.3 2.5-2.78.67-5.49 1.66-8.26 2.38-1.35.34-1.57.93-1.15 2.2.89 2.72 1.58 5.51 2.49 8.23.46 1.34.28 1.92-1.19 2.35-14.44 4.23-28.87 8.5-43.27 12.84-1.54.46-1.93-.07-2.27-1.38-.73-2.77-1.68-5.49-2.39-8.26-.34-1.34-.92-1.57-2.17-1.16-2.5.82-5.07 1.4-7.54 2.31-1.8.65-2.67.44-3.11-1.6-.58-2.64-1.55-5.19-2.24-7.81-.31-1.21-.79-1.51-1.99-1.11-2.73.89-5.53 1.54-8.23 2.5-1.67.59-2.22.07-2.62-1.49-.68-2.7-1.61-5.34-2.32-8.03-.34-1.29-.84-1.65-2.16-1.21-2.72.91-5.52 1.58-8.23 2.52-1.44.49-1.89.1-2.29-1.29-1.85-6.51-3.75-13.01-5.77-19.47-.54-1.69-.24-2.28 1.49-2.79Z\"\n                    class=\"cls-9\" />\n                <path\n                    d=\"m251.02 298.65-3.51-12.06.08.05c-4 1.21-8 2.41-12 3.62l.04-.08c1.41 3.19 2.12 6.61 3.1 9.94.4 1.35.87 1.77 2.31 1.3 3.31-1.06 6.67-1.92 10.02-2.86l-.05.09Z\"\n                    style=\"fill:#fefffd\" />\n                <path\n                    d=\"M357.26 251.05c.38.7.84.95 1.7.67 3.44-1.12 6.91-2.15 10.37-3.21-.97-3.28-1.99-6.55-2.89-9.85-.32-1.18-.77-1.54-1.99-1.14-2.87.93-5.77 1.8-8.69 2.58-1.23.33-1.44.85-1.07 2.02.92 2.96 1.72 5.96 2.57 8.94M334.59 258.93c3.26-.97 6.51-2 9.79-2.88 1.33-.36 1.57-.89 1.15-2.17-1.05-3.24-1.98-6.52-2.95-9.78l-10.08 2.91c-.75.22-1.65.3-1.24 1.51 1.17 3.45 1.79 7.07 3.32 10.41\"\n                    class=\"cls-4\" />\n                <path\n                    d=\"M372.91 260.57c-3.49.98-6.96 2.03-10.48 2.9-1.38.34-.82 1.25-.9 2.01.37.85.74 1.7.71 2.66-.16 1.38-.08 2.68 1.12 3.64 1.43 3.62 1.45 3.68 5.32 2.44 2.46-.79 6.11-.61 7.05-2.62.81-1.75-1-4.73-1.67-7.16-.36-1.3-.77-2.59-1.15-3.88\"\n                    style=\"fill:#fbfbfb\" />\n                <path\n                    d=\"M247.5 286.59c3.43-.98 6.87-1.97 10.31-2.93.9-.25 1.35-.55 1-1.63-.97-3.03-1.88-6.07-2.71-9.14-.3-1.1-.81-1.1-1.71-.82-2.96.92-5.92 1.82-8.91 2.63-1.17.32-1.26.88-.94 1.9 1.05 3.33 2.03 6.69 3.04 10.04l-.08-.05Z\"\n                    class=\"cls-3\" />\n                <path\n                    d=\"M235.59 290.25c-.46-3.65-1.97-7.01-2.85-10.55-.24-.96-.77-1-1.58-.75-2.97.91-5.94 1.82-8.93 2.64-1.09.3-1.4.74-1.04 1.85.92 2.88 1.8 5.78 2.57 8.7.34 1.29.95 1.36 2.06 1.01 3.26-1.04 6.54-1.99 9.82-2.98l-.04.08M251.07 298.56c.97 3.27 1.99 6.52 2.88 9.81.35 1.28.84 1.64 2.15 1.2 2.78-.93 5.6-1.73 8.43-2.51 1.09-.3 1.58-.64 1.15-1.93-.98-2.92-1.79-5.9-2.66-8.85-.2-.7-.3-1.4-1.37-1.03-3.51 1.21-7.2 1.85-10.61 3.39l.05-.09\"\n                    class=\"cls-4\" />\n                <path\n                    d=\"M167.27 433.19c9.62-29.21 55.01-60.77 82.24-107.29 11.54-19.71 2.97-51.8-22.95-56.38-56.15-9.91-96.62 37.51-96.62 37.51s-37.46 35.45-25.85 86.54c0 0 7.93 30.62 36.95 50.52l23.64.81 2.6-11.71Z\"\n                    class=\"cls-7\" />\n                <path\n                    d=\"M254.51 307.83c-3.13-13.15-12.37-24.66-27.22-27.28-57-10.05-98.1 38.09-98.1 38.09s-21.89 20.72-27.31 53.37c.84-39.23 28.05-64.98 28.05-64.98s40.47-47.42 96.63-37.51c18.87 3.33 28.53 21.22 27.95 38.31\"\n                    class=\"cls-3\" style=\"opacity:.4\" />\n                <path\n                    d=\"M164.98 432.44c1.35-6.88 4.47-13.2 8.05-18.95 3.61-5.76 7.73-11.08 12.05-16.12 8.69-10.05 17.73-19.53 26.34-29.27 8.63-9.73 16.9-19.67 24.26-30.18.93-1.31 1.84-2.63 2.72-3.97.89-1.33 1.78-2.66 2.62-4.02.86-1.35 1.72-2.7 2.52-4.07l1.23-2.05.31-.51.25-.46.49-.92c2.53-4.97 3.65-10.77 3.49-16.53-.17-5.76-1.68-11.5-4.38-16.45-2.69-4.97-6.7-9.02-11.51-11.52-2.41-1.26-5.01-2.12-7.74-2.6l-4.57-.66c-1.53-.18-3.07-.27-4.6-.41-6.14-.37-12.32-.08-18.42.85-12.21 1.84-24.01 6.43-34.88 12.65-5.41 3.16-10.64 6.7-15.56 10.63-2.46 1.96-4.86 4-7.16 6.13-1.15 1.07-2.29 2.15-3.38 3.25a74.216 74.216 0 0 0-3.09 3.27l-.21.23-.19.18c-.4.38-.99 1-1.48 1.53l-1.54 1.7a97.533 97.533 0 0 0-8.39 11.21 99.179 99.179 0 0 0-8.63 16.57c-2.31 5.77-4.09 11.75-5.18 17.83a83.369 83.369 0 0 0-1.18 18.46c.03 1.55.24 3.08.35 4.63.14 1.54.42 3.07.61 4.6.25 1.52.57 3 .85 4.5.04.18.08.41.1.46l.14.46c.1.34.21.69.33 1.06.22.71.48 1.44.74 2.16a93.879 93.879 0 0 0 8.13 16.8c3.27 5.33 7.02 10.36 11.27 14.92 4.25 4.56 7.25 8.48 12.31 12.1 0 0-1.68 8.9-3.84 7.41-11.42-7.86-21.15-17.32-28.38-29.1-3.66-5.86-6.72-12.06-9.18-18.6-.3-.82-.6-1.65-.88-2.5-.14-.42-.28-.85-.41-1.3l-.21-.72c-.1-.35-.13-.53-.17-.75-.35-1.73-.74-3.46-1.03-5.18-.24-1.72-.55-3.44-.73-5.17-.14-1.74-.39-3.46-.44-5.21-.39-6.96 0-13.97 1.16-20.85 1.17-6.87 3.11-13.59 5.7-20.03 2.6-6.43 5.81-12.59 9.55-18.39 1.87-2.91 3.87-5.72 6.01-8.45 1.06-1.37 2.18-2.7 3.32-4.02.58-.66 1.16-1.31 1.77-1.96.63-.67 1.17-1.25 1.96-2.01l-.4.42c1.29-1.47 2.41-2.63 3.65-3.88 1.21-1.22 2.45-2.4 3.7-3.57 2.51-2.32 5.1-4.53 7.76-6.66 5.31-4.26 10.97-8.1 16.87-11.55 11.84-6.8 24.89-11.92 38.63-14.01a92.636 92.636 0 0 1 20.78-.89c1.73.17 3.47.29 5.19.5l5.16.78c3.73.67 7.4 1.91 10.8 3.71 6.82 3.58 12.32 9.36 15.82 15.93 3.53 6.58 5.35 13.89 5.53 21.22.15 7.32-1.29 14.79-4.75 21.5l-.68 1.24-.34.62-.33.54-1.31 2.16c-.86 1.45-1.77 2.86-2.68 4.27-.9 1.42-1.83 2.81-2.78 4.2-.93 1.4-1.89 2.77-2.87 4.13-7.75 10.92-16.35 21.1-25.17 30.92-8.82 9.83-17.92 19.29-26.46 29-8.52 9.71-17.05 19.18-22.79 30.26l-.04.08a2.436 2.436 0 0 1-3.28 1.04 2.441 2.441 0 0 1-1.27-2.63\"\n                    class=\"cls-9\" />\n                <g class=\"cls-20\">\n                    <path\n                        d=\"M211.98 349.04c-8.63 8.62-21.18 10.05-28.04 3.19-6.86-6.86-5.43-19.41 3.2-28.03 8.62-8.62 21.18-10.05 28.03-3.2 6.86 6.86 5.43 19.41-3.2 28.04\"\n                        class=\"cls-3\" />\n                </g>\n                <g class=\"cls-20\">\n                    <path\n                        d=\"M214.43 309.1c-3.66 3.66-9 4.27-11.91 1.36-2.91-2.91-2.31-8.25 1.36-11.91 3.66-3.66 9-4.27 11.91-1.36 2.91 2.91 2.31 8.25-1.36 11.91\"\n                        class=\"cls-3\" />\n                </g>\n                <g class=\"cls-20\">\n                    <path\n                        d=\"M230.61 315.92c-3.74 3.75-9.19 4.37-12.17 1.39-2.98-2.98-2.36-8.43 1.39-12.17 3.74-3.74 9.19-4.37 12.17-1.39 2.98 2.98 2.36 8.43-1.39 12.17\"\n                        class=\"cls-3\" />\n                </g>\n                <g class=\"cls-20\">\n                    <path\n                        d=\"M234.52 329.19c-3.03 3.03-7.45 3.54-9.86 1.12-2.41-2.41-1.91-6.83 1.12-9.86 3.03-3.03 7.45-3.54 9.86-1.12 2.41 2.41 1.91 6.83-1.12 9.86\"\n                        class=\"cls-3\" />\n                </g>\n                <path\n                    d=\"M232.2 483.14c-10.58-.05-29.68-6.12-56.76-18.06-19.9-8.77-37.07-17.65-37.24-17.74l3.84-7.41c.17.09 17.14 8.86 36.8 17.53 32.39 14.27 42.22 15.99 48.7 16.01l4.65 9.66Z\"\n                    class=\"cls-9\" />\n            </svg>\n        </div>\n        <div class=\"right\">\n            <div class=\"content\">\n                <section class=\"general\">\n                    <h2>If you are a member of the general public:</h2>\n\n                    <p>The fact that you are seeing this page indicates that the website you just visited is either\n                        experiencing problems, or is undergoing routine maintenance.</p>\n\n                    <p>\n                        If you would like to let the administrators of this website know that you've seen this page\n                        instead of the page you expected, you should send them e-mail.\n                        In general, mail sent to the name \"webmaster\" and directed to the website's domain should reach\n                        the appropriate person.\n                    </p>\n\n                    <p>For example, try contacting <a\n                            href=\"mailto:webmaster@<?php echo $_SERVER['SERVER_NAME'] ?? 'example.com'; ?>\">webmaster@<?php echo $_SERVER['SERVER_NAME'] ?? 'example.com'; ?></a>.\n                    </p>\n\n                    <p>Learn more about FrankenPHP at the <a href=\"https://frankenphp.dev/\">official website</a>.</p>\n                </section>\n\n                <section class=\"administrator\">\n                    <h2>If you are the website administrator:</h2>\n\n                    <p>Your server is running and serving requests using FrankenPHP</p>\n\n                    <p>To replace this page, deploy your application files to <code><?php echo getcwd(); ?></code>.</p>\n\n                    <p>Configuration is handled in your <code>Caddyfile</code>.</p>\n\n                    <div class=\"runtime-info\">\n                        <strong>Served by PHP SAPI: </strong> <?php echo php_sapi_name(); ?><br />\n                    </div>\n\n                </section>\n            </div>\n            <footer class=\"footer\">\n                <p><a href=\"https://frankenphp.dev\">FrankenPHP</a> is a free and open-source web server for PHP built on top\n                    of <a href=\"https://caddyserver.com\">Caddy</a>.</p>\n                <div class=\"logos\">\n                    <a href=\"https://frankenphp.dev/\"><svg xmlns=\"http://www.w3.org/2000/svg\" id=\"a\"\n                            viewBox=\"0 0 699.37 92.29\">\n                            <defs fill=\"#000000\">\n                                <style>\n                                    .b {\n                                        fill: #390075;\n                                    }\n\n                                    .c {\n                                        fill: #b3d133;\n                                    }\n                                </style>\n                            </defs>\n                            <path class=\"c\"\n                                d=\"M22.65,38.71h20.32c3.02,0,5.65,1.1,7.89,3.3,2.24,2.2,3.36,4.85,3.36,7.96s-1.12,5.67-3.36,7.96c-2.24,2.29-4.96,3.43-8.15,3.43H22.65v19.8c0,3.45-1.12,6.17-3.37,8.15-2.24,1.99-4.88,2.98-7.89,2.98s-5.78-.99-8.02-2.98c-2.24-1.98-3.37-4.7-3.37-8.15V20.2c0-3.45,.86-6.64,2.59-9.58,1.72-2.93,4.03-5.26,6.92-6.99,2.89-1.72,6.06-2.59,9.51-2.59h28.86c3.19,0,5.89,1.1,8.09,3.3,2.2,2.2,3.3,4.81,3.3,7.83s-1.14,5.8-3.43,8.09c-2.29,2.29-4.94,3.43-7.96,3.43H22.65v15.01Z\"\n                                fill=\"#000000\" />\n                            <path class=\"c\"\n                                d=\"M118.29,54.76c3.88,2.42,6.66,5.18,8.35,8.28,1.68,3.11,2.52,6.77,2.52,11v6.6c0,3.11-1.08,5.76-3.24,7.96-2.16,2.2-4.75,3.3-7.77,3.3s-5.74-1.12-7.89-3.36c-2.16-2.24-3.24-4.87-3.24-7.89v-6.34l-22.13-26.92v33.26c0,3.11-1.08,5.76-3.24,7.96-2.16,2.2-4.79,3.3-7.89,3.3s-5.89-1.1-8.09-3.3c-2.2-2.2-3.3-4.85-3.3-7.96V12.44c0-3.19,1.1-5.89,3.3-8.09s4.9-3.3,8.09-3.3h25.24c5.09,0,9.62,1.86,13.59,5.57l11,10.61c4.05,4.06,6.08,8.72,6.08,13.98v5.05c0,8.54-3.8,14.71-11.39,18.51Zm-33.39-9.32h22.13V23.7h-22.13v21.74Z\"\n                                fill=\"#000000\" />\n                            <path class=\"c\"\n                                d=\"M270.74,88.54c-2.16,2.24-4.83,3.36-8.02,3.36s-6-1.12-8.15-3.36c-2.16-2.24-3.24-4.96-3.24-8.15V23.57h-20.84v57.07c0,3.19-1.12,5.87-3.37,8.02-2.24,2.16-4.96,3.24-8.15,3.24s-5.76-1.1-7.96-3.3c-2.2-2.2-3.3-4.85-3.3-7.96V12.05c0-3.19,1.12-5.89,3.37-8.09,2.24-2.2,4.87-3.3,7.89-3.3,3.19,0,5.91,1.1,8.15,3.3,2.24,2.2,3.37,4.9,3.37,8.09v3.75c0,.43,.08,.65,.26,.65s.26-.08,.26-.26c1.55-5,4.12-8.8,7.7-11.39,3.58-2.59,7.87-3.88,12.88-3.88,6.47,0,11.82,1.92,16.05,5.76,4.23,3.84,6.34,8.99,6.34,15.47v58.24c0,3.19-1.08,5.91-3.24,8.15Z\"\n                                fill=\"#000000\" />\n                            <path class=\"c\"\n                                d=\"M347.1,80.64c0,3.11-1.14,5.78-3.43,8.02-2.29,2.24-4.98,3.37-8.09,3.37s-5.8-1.12-8.09-3.37c-2.29-2.24-3.43-4.92-3.43-8.02v-24.72l-22.13-.26v24.98c0,3.19-1.1,5.89-3.3,8.09s-4.9,3.3-8.09,3.3-5.89-1.1-8.09-3.3-3.3-4.9-3.3-8.09V11.66c0-3.02,1.1-5.63,3.3-7.83,2.2-2.2,4.9-3.3,8.09-3.3s5.89,1.08,8.09,3.24c2.2,2.16,3.3,4.79,3.3,7.89V53.72l22.13-27.31V11.66c0-3.02,1.14-5.63,3.43-7.83,2.29-2.2,4.94-3.3,7.96-3.3s5.63,1.1,7.83,3.3c2.2,2.2,3.3,4.81,3.3,7.83v15.14c0,4.32-.84,8.05-2.52,11.19-1.68,3.15-4.51,5.98-8.48,8.48,7.68,3.62,11.52,9.79,11.52,18.51v15.66Z\"\n                                fill=\"#000000\" />\n                            <path class=\"c\"\n                                d=\"M374.66,35.6h25.24c3.11,0,5.76,1.1,7.96,3.3,2.2,2.2,3.3,4.9,3.3,8.09,0,3.02-1.12,5.65-3.37,7.89-2.24,2.24-4.88,3.36-7.89,3.36h-25.24v10.87h27.44c3.11,0,5.76,1.1,7.96,3.3,2.2,2.2,3.3,4.9,3.3,8.09,0,3.02-1.1,5.61-3.3,7.77-2.2,2.16-4.85,3.32-7.96,3.49h-31.19c-3.37,0-6.49-.86-9.38-2.59-2.89-1.73-5.2-4.05-6.92-6.99-1.73-2.93-2.59-6.12-2.59-9.58v-6.99c0-5.26,.73-9.55,2.2-12.88,1.47-3.32,4.49-5.76,9.06-7.31-4.32-1.47-7.27-3.77-8.87-6.92-1.6-3.15-2.39-7.23-2.39-12.23v-6.08c0-3.45,.86-6.64,2.59-9.58,1.73-2.93,4.03-5.26,6.92-6.99,2.89-1.72,6.02-2.59,9.38-2.59h31.19c3.11,0,5.76,1.1,7.96,3.3s3.3,4.81,3.3,7.83-1.12,5.8-3.37,8.09c-2.24,2.29-4.88,3.43-7.89,3.43h-27.44v11.91Z\"\n                                fill=\"#000000\" />\n                            <path class=\"c\"\n                                d=\"M480.4,88.54c-2.16,2.24-4.83,3.36-8.02,3.36s-6-1.12-8.15-3.36c-2.16-2.24-3.24-4.96-3.24-8.15V23.57h-20.84v57.07c0,3.19-1.12,5.87-3.37,8.02-2.24,2.16-4.96,3.24-8.15,3.24s-5.76-1.1-7.96-3.3c-2.2-2.2-3.3-4.85-3.3-7.96V12.05c0-3.19,1.12-5.89,3.37-8.09,2.24-2.2,4.87-3.3,7.89-3.3,3.19,0,5.91,1.1,8.15,3.3,2.24,2.2,3.37,4.9,3.37,8.09v3.75c0,.43,.08,.65,.26,.65s.26-.08,.26-.26c1.55-5,4.12-8.8,7.7-11.39,3.58-2.59,7.87-3.88,12.88-3.88,6.47,0,11.82,1.92,16.05,5.76,4.23,3.84,6.34,8.99,6.34,15.47v58.24c0,3.19-1.08,5.91-3.24,8.15Z\"\n                                fill=\"#000000\" />\n                            <path class=\"b\"\n                                d=\"M511.07,80.25c0,3.19-1.06,5.91-3.17,8.15-2.12,2.24-4.72,3.37-7.83,3.37s-5.78-1.12-8.02-3.37c-2.24-2.24-3.37-4.96-3.37-8.15V20.07c0-3.45,.86-6.64,2.59-9.58,1.73-2.93,4.05-5.26,6.99-6.99,2.93-1.72,6.12-2.59,9.58-2.59h17.21c5.09,0,9.53,1.77,13.33,5.31l12.42,12.42c1.81,1.73,3.17,3.97,4.08,6.73,.91,2.76,1.36,5.74,1.36,8.93,0,3.71-.58,7.29-1.75,10.74-1.16,3.45-2.78,6.17-4.85,8.15l-11.26,10.74c-3.62,3.62-8.07,5.44-13.33,5.44h-13.98v10.87Zm21.74-56.69l-21.48-.26v23.42h21.48V23.57Z\"\n                                fill=\"#000000\" />\n                            <path class=\"b\"\n                                d=\"M560.11,11.92c0-3.45,1.12-6.17,3.37-8.15,2.24-1.98,4.92-2.98,8.02-2.98s5.76,.97,7.96,2.91c2.2,1.94,3.3,4.64,3.3,8.09v28.47h21.35V11.92c0-3.45,1.14-6.17,3.43-8.15,2.29-1.98,4.98-2.98,8.09-2.98s5.76,.97,7.96,2.91,3.3,4.64,3.3,8.09V81.16c0,3.45-1.12,6.17-3.37,8.15-2.24,1.99-4.92,2.98-8.02,2.98s-5.78-.99-8.02-2.98c-2.24-1.98-3.37-4.7-3.37-8.15V43.5l-21.35,19.28v18.38c0,3.45-1.12,6.17-3.36,8.15-2.24,1.99-4.88,2.98-7.89,2.98s-5.78-.99-8.02-2.98c-2.24-1.98-3.37-4.7-3.37-8.15V11.92Z\"\n                                fill=\"#000000\" />\n                            <path class=\"b\"\n                                d=\"M654.2,80.25c0,3.19-1.06,5.91-3.17,8.15-2.12,2.24-4.72,3.37-7.83,3.37s-5.78-1.12-8.02-3.37c-2.24-2.24-3.37-4.96-3.37-8.15V20.07c0-3.45,.86-6.64,2.59-9.58,1.73-2.93,4.05-5.26,6.99-6.99,2.93-1.72,6.12-2.59,9.58-2.59h17.21c5.09,0,9.53,1.77,13.33,5.31l12.42,12.42c1.81,1.73,3.17,3.97,4.08,6.73,.91,2.76,1.36,5.74,1.36,8.93,0,3.71-.58,7.29-1.75,10.74-1.16,3.45-2.78,6.17-4.85,8.15l-11.26,10.74c-3.62,3.62-8.07,5.44-13.33,5.44h-13.98v10.87Zm21.74-56.69l-21.48-.26v23.42h21.48V23.57Z\"\n                                fill=\"#000000\" />\n                            <g>\n                                <path class=\"c\"\n                                    d=\"M142.76,32.66v-3.51c0-1.32,1.04-2.4,2.32-2.4,1.29,0,2.33,1.07,2.33,2.39v3.51l7.92-.02v-3.51c0-1.32,1.04-2.4,2.32-2.4,1.29,0,2.33,1.07,2.33,2.39v3.51h6.64v-3.53c0-1.32,1.03-2.4,2.32-2.4s2.33,1.07,2.33,2.39v3.51h6.83v-3.53c0-1.32,1.03-2.4,2.32-2.4,1.29,0,2.33,1.07,2.33,2.39v3.51l7.69-.02v-3.51c0-1.32,1.03-2.4,2.32-2.4s2.33,1.07,2.33,2.39v3.51h6.89v-.85c-.17-3.02-.89-5.52-2.14-7.51-1.25-1.98-3.22-4.1-5.89-6.34l-16.95-13.85c-3.19-2.67-6.13-4.01-8.8-4.01s-5.61,1.34-9.06,4.01l-16.57,13.85c-2.68,2.33-4.66,4.49-5.95,6.47-1.29,1.99-2.07,4.44-2.33,7.38v.97l8.46-.02Z\"\n                                    fill=\"#000000\" />\n                                <path class=\"c\"\n                                    d=\"M195.11,37.2v3.5c0,1.32-1.03,2.4-2.32,2.4s-2.33-1.07-2.33-2.39v-3.5l-7.69,.02v3.5c0,1.32-1.03,2.4-2.32,2.4s-2.33-1.07-2.33-2.39v-3.5h-6.83v3.51c0,1.32-1.04,2.4-2.32,2.4-1.29,0-2.33-1.07-2.33-2.39v-3.5h-6.64v3.51c0,1.32-1.03,2.4-2.32,2.4-1.29,0-2.33-1.07-2.33-2.39v-3.5l-7.92,.02v3.5c0,1.32-1.03,2.4-2.32,2.4-1.29,0-2.33-1.07-2.33-2.39v-3.5l-8.47,.02v43.43c0,3.11,1.12,5.74,3.37,7.89,2.24,2.16,4.92,3.24,8.02,3.24s5.76-1.08,7.96-3.24c2.2-2.16,3.3-4.79,3.3-7.89v-31.84l22.52,20.84v11c0,3.11,1.1,5.74,3.3,7.89,2.2,2.16,4.9,3.24,8.09,3.24s5.74-1.08,7.9-3.24,3.24-4.79,3.24-7.89V37.19h-6.88Z\"\n                                    fill=\"#000000\" />\n                            </g>\n                        </svg></a>\n                    <a href=\"https://caddyserver.com/\"><svg width=\"100%\" height=\"100%\" viewBox=\"0 0 379 114\"\n                            version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\"\n                            xml:space=\"preserve\" xmlns:serif=\"http://www.serif.com/\"\n                            style=\"fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;\">\n                            <g transform=\"matrix(1,0,0,1,-35.5985,-67.948)\">\n                                <g transform=\"matrix(1.16548,0,0,1.10195,-124,-68.27)\">\n                                    <g id=\"Light-Logo\" serif:id=\"Light Logo\"\n                                        transform=\"matrix(0.858013,0,0,0.907485,-3243.42,-1469.17)\">\n                                        <g id=\"Logo\" transform=\"matrix(1,0,0,1,21.4759,36.7359)\">\n                                            <g id=\"Icon\">\n                                                <g>\n                                                    <g\n                                                        transform=\"matrix(-0.191794,-0.715786,0.715786,-0.191794,4329.14,4673.64)\">\n                                                        <path\n                                                            d=\"M3901.56,610.734C3893.53,610.261 3886.06,608.1 3879.2,604.877C3872.24,601.608 3866.04,597.093 3860.8,591.633C3858.71,589.457 3856.76,587.149 3854.97,584.709C3853.2,582.281 3851.57,579.733 3850.13,577.066C3845.89,569.224 3843.21,560.381 3842.89,550.868C3842.57,543.321 3843.64,536.055 3845.94,529.307C3848.37,522.203 3852.08,515.696 3856.83,510.049L3855.79,509.095C3850.39,514.54 3846.02,520.981 3842.9,528.125C3839.84,535.125 3838.03,542.781 3837.68,550.868C3837.34,561.391 3839.51,571.425 3843.79,580.306C3845.27,583.38 3847.03,586.304 3849.01,589.049C3851.01,591.806 3853.24,594.39 3855.69,596.742C3861.75,602.568 3869,607.19 3877.03,610.1C3884.66,612.867 3892.96,614.059 3901.56,613.552L3901.56,610.734Z\"\n                                                            style=\"fill:rgb(0,144,221);\" />\n                                                    </g>\n                                                    <g\n                                                        transform=\"matrix(-0.191794,-0.715786,0.715786,-0.191794,4329.14,4673.64)\">\n                                                        <path\n                                                            d=\"M3875.69,496.573C3879.62,494.538 3883.8,492.897 3888.2,491.786C3892.49,490.704 3896.96,490.124 3901.56,490.032C3903.82,490.13 3906.03,490.332 3908.21,490.688C3917.13,492.147 3925.19,495.814 3932.31,500.683C3936.13,503.294 3939.59,506.335 3942.81,509.619C3947.09,513.98 3950.89,518.816 3953.85,524.232C3958.2,532.197 3960.96,541.186 3961.32,550.868C3961.61,558.748 3960.46,566.345 3957.88,573.322C3956.09,578.169 3953.7,582.753 3950.66,586.838C3947.22,591.461 3942.96,595.427 3938.27,598.769C3933.66,602.055 3928.53,604.619 3923.09,606.478C3922.37,606.721 3921.6,606.805 3920.93,607.167C3920.42,607.448 3920.14,607.854 3919.69,608.224L3920.37,610.389C3920.98,610.432 3921.47,610.573 3922.07,610.474C3922.86,610.344 3923.55,609.883 3924.28,609.566C3931.99,606.216 3938.82,601.355 3944.57,595.428C3947.02,592.903 3949.25,590.174 3951.31,587.319C3953.59,584.168 3955.66,580.853 3957.43,577.348C3961.47,569.34 3964.01,560.422 3964.36,550.868C3964.74,540.511 3962.66,530.628 3958.48,521.868C3955.57,515.775 3951.72,510.163 3946.95,505.478C3943.37,501.962 3939.26,498.99 3934.84,496.562C3926.88,492.192 3917.87,489.76 3908.37,489.229C3906.12,489.104 3903.86,489.054 3901.56,489.154C3896.87,489.06 3892.3,489.519 3887.89,490.397C3883.3,491.309 3878.89,492.683 3874.71,494.525L3875.69,496.573Z\"\n                                                            style=\"fill:rgb(0,144,221);\" />\n                                                    </g>\n                                                </g>\n                                                <g>\n                                                    <g\n                                                        transform=\"matrix(-3.37109,-0.514565,0.514565,-3.37109,4078.07,1806.88)\">\n                                                        <path\n                                                            d=\"M22,12C22,10.903 21.097,10 20,10C19.421,10 18.897,10.251 18.53,10.649C18.202,11.006 18,11.481 18,12C18,13.097 18.903,14 20,14C21.097,14 22,13.097 22,12Z\"\n                                                            style=\"fill:none;fill-rule:nonzero;stroke:rgb(0,144,221);stroke-width:1.05px;\" />\n                                                    </g>\n                                                    <g\n                                                        transform=\"matrix(-5.33921,-5.26159,-3.12106,-6.96393,4073.87,1861.55)\">\n                                                        <path\n                                                            d=\"M10.315,5.333C10.315,5.333 9.748,5.921 9.03,6.673C7.768,7.995 6.054,9.805 6.054,9.805L6.237,9.86C6.237,9.86 8.045,8.077 9.36,6.771C10.107,6.028 10.689,5.444 10.689,5.444L10.315,5.333Z\"\n                                                            style=\"fill:rgb(0,144,221);\" />\n                                                    </g>\n                                                </g>\n                                                <g id=\"Padlock\" transform=\"matrix(3.11426,0,0,3.11426,3938.31,1737.25)\">\n                                                    <g>\n                                                        <path\n                                                            d=\"M9.876,21L18.162,21C18.625,21 19,20.625 19,20.162L19,11.838C19,11.375 18.625,11 18.162,11L5.838,11C5.375,11 5,11.375 5,11.838L5,16.758\"\n                                                            style=\"fill:none;stroke:rgb(34,182,56);stroke-width:1.89px;stroke-linecap:butt;stroke-linejoin:miter;\" />\n                                                        <path\n                                                            d=\"M8,11L8,7C8,4.806 9.806,3 12,3C14.194,3 16,4.806 16,7L16,11\"\n                                                            style=\"fill:none;fill-rule:nonzero;stroke:rgb(34,182,56);stroke-width:1.89px;\" />\n                                                    </g>\n                                                </g>\n                                                <g>\n                                                    <g\n                                                        transform=\"matrix(5.30977,0.697415,-0.697415,5.30977,3852.72,1727.97)\">\n                                                        <path\n                                                            d=\"M22,12C22,11.659 21.913,11.337 21.76,11.055C21.421,10.429 20.756,10 20,10C18.903,10 18,10.903 18,12C18,13.097 18.903,14 20,14C21.097,14 22,13.097 22,12Z\"\n                                                            style=\"fill:none;fill-rule:nonzero;stroke:rgb(0,144,221);stroke-width:0.98px;\" />\n                                                    </g>\n                                                    <g\n                                                        transform=\"matrix(4.93114,2.49604,1.11018,5.44847,3921.41,1726.72)\">\n                                                        <path\n                                                            d=\"M8.902,6.77C8.902,6.77 7.235,8.253 6.027,9.366C5.343,9.996 4.819,10.502 4.819,10.502L5.52,11.164C5.52,11.164 6.021,10.637 6.646,9.951C7.749,8.739 9.219,7.068 9.219,7.068L8.902,6.77Z\"\n                                                            style=\"fill:rgb(0,144,221);\" />\n                                                    </g>\n                                                </g>\n                                            </g>\n                                            <g id=\"Wordmark\" transform=\"matrix(1.54159,0,0,2.8744,2710.6,709.804)\">\n                                                <g id=\"y\" transform=\"matrix(0.50291,0,0,0.281607,905.533,304.987)\">\n                                                    <path\n                                                        d=\"M192.152,286.875L202.629,268.64C187.804,270.106 183.397,265.779 180.143,263.391C176.888,261.004 174.362,257.99 172.563,254.347C170.765,250.705 169.866,246.691 169.866,242.305L169.866,208.107L183.21,208.107L183.21,242.213C183.21,245.188 183.896,247.822 185.268,250.116C186.64,252.41 188.465,254.197 190.743,255.475C193.022,256.754 195.501,257.393 198.182,257.393C200.894,257.393 203.393,256.75 205.68,255.463C207.966,254.177 209.799,252.391 211.178,250.105C212.558,247.818 213.248,245.188 213.248,242.213L213.248,208.107L226.545,208.107L226.545,242.305C226.545,246.707 225.378,258.46 218.079,268.64C215.735,271.909 207.835,286.875 207.835,286.875L192.152,286.875Z\"\n                                                        style=\"fill:#2e2e2e;fill-rule:nonzero;\" />\n                                                </g>\n                                                <g id=\"add\" transform=\"matrix(0.525075,0,0,0.281607,801.871,304.987)\">\n                                                    <g transform=\"matrix(116.242,0,0,116.242,161.846,267.39)\">\n                                                        <path\n                                                            d=\"M0.276,0.012C0.227,0.012 0.186,0 0.15,-0.024C0.115,-0.048 0.088,-0.08 0.069,-0.12C0.05,-0.161 0.04,-0.205 0.04,-0.254C0.04,-0.305 0.051,-0.35 0.072,-0.39C0.094,-0.431 0.125,-0.463 0.165,-0.487C0.205,-0.51 0.254,-0.522 0.31,-0.522C0.366,-0.522 0.413,-0.51 0.452,-0.486C0.491,-0.463 0.521,-0.431 0.542,-0.39C0.562,-0.35 0.573,-0.305 0.573,-0.256L0.573,-0L0.458,-0L0.458,-0.095L0.456,-0.095C0.446,-0.076 0.433,-0.058 0.417,-0.042C0.401,-0.026 0.381,-0.013 0.358,-0.003C0.335,0.007 0.307,0.012 0.276,0.012ZM0.307,-0.086C0.337,-0.086 0.363,-0.093 0.386,-0.108C0.408,-0.123 0.426,-0.144 0.438,-0.17C0.45,-0.195 0.456,-0.224 0.456,-0.256C0.456,-0.288 0.45,-0.317 0.438,-0.342C0.426,-0.367 0.409,-0.387 0.387,-0.402C0.365,-0.417 0.338,-0.424 0.308,-0.424C0.276,-0.424 0.249,-0.417 0.226,-0.402C0.204,-0.387 0.186,-0.366 0.174,-0.341C0.162,-0.315 0.156,-0.287 0.156,-0.255C0.156,-0.224 0.162,-0.195 0.174,-0.169C0.186,-0.144 0.203,-0.123 0.226,-0.108C0.248,-0.093 0.275,-0.086 0.307,-0.086Z\"\n                                                            style=\"fill:#2e2e2e;fill-rule:nonzero;\" />\n                                                    </g>\n                                                    <g transform=\"matrix(116.242,0,0,116.242,226.592,267.39)\">\n                                                        <path\n                                                            d=\"M0.306,0.012C0.265,0.012 0.229,0.006 0.196,-0.008C0.163,-0.021 0.135,-0.039 0.112,-0.064C0.089,-0.088 0.071,-0.117 0.059,-0.151C0.046,-0.185 0.04,-0.222 0.04,-0.263C0.04,-0.315 0.051,-0.36 0.072,-0.399C0.093,-0.437 0.122,-0.468 0.159,-0.489C0.196,-0.511 0.239,-0.522 0.287,-0.522C0.311,-0.522 0.333,-0.518 0.355,-0.511C0.377,-0.504 0.396,-0.493 0.413,-0.48C0.431,-0.466 0.445,-0.451 0.455,-0.433L0.456,-0.433L0.456,-0.73L0.571,-0.73L0.571,-0.261C0.571,-0.205 0.56,-0.156 0.537,-0.115C0.515,-0.074 0.484,-0.043 0.444,-0.021C0.405,0.001 0.358,0.012 0.306,0.012ZM0.306,-0.086C0.335,-0.086 0.361,-0.093 0.384,-0.107C0.406,-0.122 0.423,-0.141 0.436,-0.167C0.448,-0.192 0.455,-0.221 0.455,-0.255C0.455,-0.288 0.448,-0.317 0.436,-0.343C0.423,-0.368 0.406,-0.388 0.383,-0.402C0.361,-0.417 0.335,-0.424 0.305,-0.424C0.276,-0.424 0.251,-0.417 0.228,-0.402C0.206,-0.387 0.188,-0.368 0.175,-0.342C0.163,-0.317 0.156,-0.288 0.156,-0.255C0.156,-0.222 0.163,-0.193 0.175,-0.167C0.188,-0.142 0.206,-0.122 0.229,-0.108C0.251,-0.093 0.277,-0.086 0.306,-0.086Z\"\n                                                            style=\"fill:#2e2e2e;fill-rule:nonzero;\" />\n                                                    </g>\n                                                    <g transform=\"matrix(116.242,0,0,116.242,290.293,267.39)\">\n                                                        <path\n                                                            d=\"M0.306,0.012C0.265,0.012 0.229,0.006 0.196,-0.008C0.163,-0.021 0.135,-0.039 0.112,-0.064C0.089,-0.088 0.071,-0.117 0.059,-0.151C0.046,-0.185 0.04,-0.222 0.04,-0.263C0.04,-0.315 0.051,-0.36 0.072,-0.399C0.093,-0.437 0.122,-0.468 0.159,-0.489C0.196,-0.511 0.239,-0.522 0.287,-0.522C0.311,-0.522 0.333,-0.518 0.355,-0.511C0.377,-0.504 0.396,-0.493 0.413,-0.48C0.431,-0.466 0.445,-0.451 0.455,-0.433L0.456,-0.433L0.456,-0.73L0.571,-0.73L0.571,-0.261C0.571,-0.205 0.56,-0.156 0.537,-0.115C0.515,-0.074 0.484,-0.043 0.444,-0.021C0.405,0.001 0.358,0.012 0.306,0.012ZM0.306,-0.086C0.335,-0.086 0.361,-0.093 0.384,-0.107C0.406,-0.122 0.423,-0.141 0.436,-0.167C0.448,-0.192 0.455,-0.221 0.455,-0.255C0.455,-0.288 0.448,-0.317 0.436,-0.343C0.423,-0.368 0.406,-0.388 0.383,-0.402C0.361,-0.417 0.335,-0.424 0.305,-0.424C0.276,-0.424 0.251,-0.417 0.228,-0.402C0.206,-0.387 0.188,-0.368 0.175,-0.342C0.163,-0.317 0.156,-0.288 0.156,-0.255C0.156,-0.222 0.163,-0.193 0.175,-0.167C0.188,-0.142 0.206,-0.122 0.229,-0.108C0.251,-0.093 0.277,-0.086 0.306,-0.086Z\"\n                                                            style=\"fill:#2e2e2e;fill-rule:nonzero;\" />\n                                                    </g>\n                                                </g>\n                                                <g id=\"c\"\n                                                    transform=\"matrix(-0.0716462,0.31304,-0.583685,-0.0384251,1489.76,-444.051)\">\n                                                    <path\n                                                        d=\"M2668.11,700.4C2666.79,703.699 2666.12,707.216 2666.12,710.766C2666.12,726.268 2678.71,738.854 2694.21,738.854C2709.71,738.854 2722.3,726.268 2722.3,710.766C2722.3,704.111 2719.93,697.672 2715.63,692.597L2707.63,699.378C2710.33,702.559 2711.57,706.602 2711.81,710.766C2712.2,717.38 2706.61,724.52 2697.27,726.637C2683.9,728.581 2676.61,720.482 2676.61,710.766C2676.61,708.541 2677.03,706.336 2677.85,704.269L2668.11,700.4Z\"\n                                                        style=\"fill:#2e2e2e;\" />\n                                                </g>\n                                            </g>\n                                        </g>\n                                        <g id=\"R\" transform=\"matrix(0.497016,0,0,0.497016,2390.38,823.152)\">\n                                            <g transform=\"matrix(1,0,0,1,-0.10786,0.450801)\">\n                                                <g transform=\"matrix(12.1247,0,0,12.1247,3862.61,1929.9)\">\n                                                    <path\n                                                        d=\"M0.073,-0L0.073,-0.7L0.383,-0.7C0.428,-0.7 0.469,-0.69 0.506,-0.67C0.543,-0.651 0.572,-0.623 0.594,-0.588C0.616,-0.553 0.627,-0.512 0.627,-0.465C0.627,-0.418 0.615,-0.377 0.592,-0.342C0.569,-0.306 0.539,-0.279 0.501,-0.259L0.57,-0.128C0.574,-0.12 0.579,-0.115 0.584,-0.111C0.59,-0.107 0.596,-0.106 0.605,-0.106L0.664,-0.106L0.664,-0L0.587,-0C0.56,-0 0.535,-0.007 0.514,-0.02C0.493,-0.034 0.476,-0.052 0.463,-0.075L0.381,-0.232C0.375,-0.231 0.368,-0.231 0.361,-0.231C0.354,-0.231 0.347,-0.231 0.34,-0.231L0.192,-0.231L0.192,-0L0.073,-0ZM0.192,-0.336L0.368,-0.336C0.394,-0.336 0.417,-0.341 0.438,-0.351C0.459,-0.361 0.476,-0.376 0.489,-0.396C0.501,-0.415 0.507,-0.438 0.507,-0.465C0.507,-0.492 0.501,-0.516 0.488,-0.535C0.475,-0.554 0.459,-0.569 0.438,-0.579C0.417,-0.59 0.394,-0.595 0.369,-0.595L0.192,-0.595L0.192,-0.336Z\"\n                                                        style=\"fill:#2e2e2e;fill-rule:nonzero;\" />\n                                                </g>\n                                            </g>\n                                            <g transform=\"matrix(1,0,0,1,0.278569,0.101881)\">\n                                                <circle cx=\"3866.43\" cy=\"1926.14\" r=\"8.923\"\n                                                    style=\"fill:none;stroke:#2e2e2e;stroke-width:2px;stroke-linecap:butt;stroke-linejoin:miter;\" />\n                                            </g>\n                                        </g>\n                                    </g>\n                                </g>\n                            </g>\n                        </svg>\n                    </a>\n                </div>\n            </footer>\n        </div>\n    </main>\n</body>\n\n</html>\n"
  },
  {
    "path": "package/debian/frankenphp.service",
    "content": "[Unit]\nDescription=FrankenPHP - The modern PHP app server\nDocumentation=https://frankenphp.dev/docs/\nAfter=network.target network-online.target\nRequires=network-online.target\n\n[Service]\nType=notify\nUser=frankenphp\nGroup=frankenphp\nExecStartPre=/usr/bin/frankenphp validate --config /etc/frankenphp/Caddyfile\nExecStart=/usr/bin/frankenphp run --environ --config /etc/frankenphp/Caddyfile\nExecReload=/usr/bin/frankenphp reload --config /etc/frankenphp/Caddyfile\nWorkingDirectory=/var/lib/frankenphp\nRestart=on-failure\nRestartSec=3s\nTimeoutStopSec=5s\nLimitNOFILE=1048576\nLimitNPROC=512\nPrivateTmp=true\nProtectHome=true\nProtectSystem=full\nAmbientCapabilities=CAP_NET_BIND_SERVICE\n\n[Install]\nWantedBy=multi-user.target\n"
  },
  {
    "path": "package/debian/postinst.sh",
    "content": "#!/bin/sh\nset -e\n\nif [ \"$1\" = \"configure\" ]; then\n\t# Add user and group\n\tif ! getent group frankenphp >/dev/null; then\n\t\tgroupadd --system frankenphp\n\tfi\n\tif ! getent passwd frankenphp >/dev/null; then\n\t\tuseradd --system \\\n\t\t\t--gid frankenphp \\\n\t\t\t--create-home \\\n\t\t\t--home-dir /var/lib/frankenphp \\\n\t\t\t--shell /usr/sbin/nologin \\\n\t\t\t--comment \"FrankenPHP web server\" \\\n\t\t\tfrankenphp\n\tfi\n\tif getent group www-data >/dev/null; then\n\t\tusermod -aG www-data frankenphp\n\tfi\n\n\t# trust frankenphp certificates before starting the systemd service\n\tif [ -z \"$2\" ] && [ -x /usr/bin/frankenphp ]; then\n\t\tHOME=/var/lib/frankenphp /usr/bin/frankenphp run --config /dev/null &\n\t\tFRANKENPHP_PID=$!\n\t\tsleep 2\n\t\tHOME=/var/lib/frankenphp /usr/bin/frankenphp trust || true\n\t\tkill \"$FRANKENPHP_PID\" || true\n\t\twait \"$FRANKENPHP_PID\" 2>/dev/null || true\n\n\t\tchown -R frankenphp:frankenphp /var/lib/frankenphp\n\tfi\n\n\t# Handle cases where package was installed and then purged;\n\t# user and group will still exist but with no home dir\n\tif [ ! -d /var/lib/frankenphp ]; then\n\t\tmkdir -p /var/lib/frankenphp\n\t\tchown -R frankenphp:frankenphp /var/lib/frankenphp\n\tfi\n\n\t# Add log directory with correct permissions\n\tif [ ! -d /var/log/frankenphp ]; then\n\t\tmkdir -p /var/log/frankenphp\n\t\tchown -R frankenphp:frankenphp /var/log/frankenphp\n\tfi\nfi\n\nif [ \"$1\" = \"configure\" ] || [ \"$1\" = \"abort-upgrade\" ] || [ \"$1\" = \"abort-deconfigure\" ] || [ \"$1\" = \"abort-remove\" ]; then\n\t# This will only remove masks created by d-s-h on package removal.\n\tdeb-systemd-helper unmask frankenphp.service >/dev/null || true\n\n\t# was-enabled defaults to true, so new installations run enable.\n\tif deb-systemd-helper --quiet was-enabled frankenphp.service; then\n\t\t# Enables the unit on first installation, creates new\n\t\t# symlinks on upgrades if the unit file has changed.\n\t\tdeb-systemd-helper enable frankenphp.service >/dev/null || true\n\t\tdeb-systemd-invoke start frankenphp.service >/dev/null || true\n\telse\n\t\t# Update the statefile to add new symlinks (if any), which need to be\n\t\t# cleaned up on purge. Also remove old symlinks.\n\t\tdeb-systemd-helper update-state frankenphp.service >/dev/null || true\n\tfi\n\n\t# Restart only if it was already started\n\tif [ -d /run/systemd/system ]; then\n\t\tsystemctl --system daemon-reload >/dev/null || true\n\t\tif [ -n \"$2\" ]; then\n\t\t\tdeb-systemd-invoke try-restart frankenphp.service >/dev/null || true\n\t\tfi\n\tfi\nfi\n\nif command -v setcap >/dev/null 2>&1; then\n\tsetcap cap_net_bind_service=+ep /usr/bin/frankenphp || true\nfi\n"
  },
  {
    "path": "package/debian/postrm.sh",
    "content": "#!/bin/sh\nset -e\n\nif [ -d /run/systemd/system ]; then\n\tsystemctl --system daemon-reload >/dev/null || true\nfi\n\nif [ \"$1\" = \"remove\" ]; then\n\tif [ -x \"/usr/bin/deb-systemd-helper\" ]; then\n\t\tdeb-systemd-helper mask frankenphp.service >/dev/null || true\n\tfi\nfi\n\nif [ \"$1\" = \"purge\" ]; then\n\tif [ -x \"/usr/bin/deb-systemd-helper\" ]; then\n\t\tdeb-systemd-helper purge frankenphp.service >/dev/null || true\n\t\tdeb-systemd-helper unmask frankenphp.service >/dev/null || true\n\tfi\n\trm -rf /var/lib/frankenphp /var/log/frankenphp /etc/frankenphp\nfi\n"
  },
  {
    "path": "package/debian/prerm.sh",
    "content": "#!/bin/sh\nset -e\n\nif [ -d /run/systemd/system ] && [ \"$1\" = remove ]; then\n\tdeb-systemd-invoke stop frankenphp.service >/dev/null || true\nfi\n"
  },
  {
    "path": "package/rhel/frankenphp.service",
    "content": "[Unit]\nDescription=FrankenPHP - The modern PHP app server\nDocumentation=https://frankenphp.dev/docs/\nAfter=network.target network-online.target\nRequires=network-online.target\n\n[Service]\nType=notify\nUser=frankenphp\nGroup=frankenphp\nExecStartPre=/usr/bin/frankenphp validate --config /etc/frankenphp/Caddyfile\nExecStart=/usr/bin/frankenphp run --environ --config /etc/frankenphp/Caddyfile\nExecReload=/usr/bin/frankenphp reload --config /etc/frankenphp/Caddyfile\nWorkingDirectory=/var/lib/frankenphp\nRestart=on-failure\nRestartSec=3s\nTimeoutStopSec=5s\nLimitNOFILE=1048576\nLimitNPROC=512\nPrivateTmp=true\nProtectHome=true\nProtectSystem=full\nAmbientCapabilities=CAP_NET_BIND_SERVICE\n\n[Install]\nWantedBy=multi-user.target\n"
  },
  {
    "path": "package/rhel/postinstall.sh",
    "content": "#!/bin/bash\n\nif [ \"$1\" -eq 1 ] && [ -x \"/usr/lib/systemd/systemd-update-helper\" ]; then\n\t# Initial installation\n\t/usr/lib/systemd/systemd-update-helper install-system-units frankenphp.service || :\nfi\n\nif [ -x /usr/sbin/getsebool ]; then\n\t# Connect to ACME endpoint to request certificates\n\tsetsebool -P httpd_can_network_connect on\nfi\n\nif [ -x /usr/sbin/semanage ] && [ -x /usr/sbin/restorecon ]; then\n\t# file contexts\n\tsemanage fcontext --add --type httpd_exec_t '/usr/bin/frankenphp' 2>/dev/null || :\n\tsemanage fcontext --add --type httpd_sys_content_t '/usr/share/frankenphp(/.*)?' 2>/dev/null || :\n\tsemanage fcontext --add --type httpd_config_t '/etc/frankenphp(/.*)?' 2>/dev/null || :\n\tsemanage fcontext --add --type httpd_var_lib_t '/var/lib/frankenphp(/.*)?' 2>/dev/null || :\n\tsemanage fcontext --add --type httpd_sys_rw_content_t \"/var/lib/frankenphp(/.*\\.db)\" 2>/dev/null || :\n\trestorecon -r /usr/bin/frankenphp /usr/share/frankenphp /etc/frankenphp /var/lib/frankenphp || :\nfi\n\nif [ -x /usr/sbin/semanage ]; then\n\t# QUIC\n\tsemanage port --add --type http_port_t --proto udp 80 2>/dev/null || :\n\tsemanage port --add --type http_port_t --proto udp 443 2>/dev/null || :\n\t# admin endpoint\n\tsemanage port --add --type http_port_t --proto tcp 2019 2>/dev/null || :\nfi\n\nif command -v setcap >/dev/null 2>&1; then\n\tsetcap cap_net_bind_service=+ep /usr/bin/frankenphp || :\nfi\n\n# check if 0.0.0.0:2019 or 127.0.0.1:2019 are in use\nport_in_use() {\n\tport_hex=$(printf '%04X' \"$1\")\n\tgrep -qE \"(00000000|0100007F):${port_hex}\" /proc/net/tcp 2>/dev/null\n}\n\n# trust frankenphp certificates if the admin api can start\nif [ \"$1\" -eq 1 ] && [ -x /usr/bin/frankenphp ]; then\n\tif ! port_in_use 2019; then\n\t\tHOME=/var/lib/frankenphp /usr/bin/frankenphp run --config /dev/null &\n\t\tFRANKENPHP_PID=$!\n\t\tsleep 2\n\t\tHOME=/var/lib/frankenphp /usr/bin/frankenphp trust || :\n\t\tkill \"$FRANKENPHP_PID\" || :\n\t\twait \"$FRANKENPHP_PID\" 2>/dev/null || :\n\n\t\tchown -R frankenphp:frankenphp /var/lib/frankenphp\n\tfi\nfi\n"
  },
  {
    "path": "package/rhel/postuninstall.sh",
    "content": "#!/bin/bash\n\nif [ \"$1\" -ge 1 ] && [ -x \"/usr/lib/systemd/systemd-update-helper\" ]; then\n\t# Package upgrade, not uninstall\n\t/usr/lib/systemd/systemd-update-helper mark-restart-system-units frankenphp.service || :\nfi\n\nif [ \"$1\" -eq 0 ]; then\n\tif [ -x /usr/sbin/getsebool ]; then\n\t\t# connect to ACME endpoint to request certificates\n\t\tsetsebool -P httpd_can_network_connect off\n\tfi\n\tif [ -x /usr/sbin/semanage ]; then\n\t\t# file contexts\n\t\tsemanage fcontext --delete --type httpd_exec_t '/usr/bin/frankenphp' 2>/dev/null || :\n\t\tsemanage fcontext --delete --type httpd_sys_content_t '/usr/share/frankenphp(/.*)?' 2>/dev/null || :\n\t\tsemanage fcontext --delete --type httpd_config_t '/etc/frankenphp(/.*)?' 2>/dev/null || :\n\t\tsemanage fcontext --delete --type httpd_var_lib_t '/var/lib/frankenphp(/.*)?' 2>/dev/null || :\n\t\tsemanage fcontext --delete --type httpd_sys_rw_content_t '/var/lib/frankenphp(/.*\\.db)' 2>/dev/null || :\n\t\t# QUIC\n\t\tsemanage port --delete --type http_port_t --proto udp 80 2>/dev/null || :\n\t\tsemanage port --delete --type http_port_t --proto udp 443 2>/dev/null || :\n\t\t# admin endpoint\n\t\tsemanage port --delete --type http_port_t --proto tcp 2019 2>/dev/null || :\n\tfi\nfi\n"
  },
  {
    "path": "package/rhel/preinstall.sh",
    "content": "#!/bin/bash\n\ngetent group frankenphp &>/dev/null ||\n\tgroupadd -r frankenphp &>/dev/null\ngetent passwd frankenphp &>/dev/null ||\n\tuseradd -r -g frankenphp -d /var/lib/frankenphp -s /sbin/nologin -c 'FrankenPHP web server' frankenphp &>/dev/null\nexit 0\n"
  },
  {
    "path": "package/rhel/preuninstall.sh",
    "content": "#!/bin/bash\n\nif [ \"$1\" -eq 0 ] && [ -x \"/usr/lib/systemd/systemd-update-helper\" ]; then\n\t# Package removal, not upgrade\n\t/usr/lib/systemd/systemd-update-helper remove-system-units frankenphp.service || :\nfi\n"
  },
  {
    "path": "phpmainthread.go",
    "content": "package frankenphp\n\n// #cgo nocallback frankenphp_new_main_thread\n// #cgo noescape frankenphp_new_main_thread\n// #include \"frankenphp.h\"\n// #include <php_variables.h>\nimport \"C\"\nimport (\n\t\"log/slog\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/dunglas/frankenphp/internal/memory\"\n\t\"github.com/dunglas/frankenphp/internal/phpheaders\"\n\t\"github.com/dunglas/frankenphp/internal/state\"\n)\n\n// represents the main PHP thread\n// the thread needs to keep running as long as all other threads are running\ntype phpMainThread struct {\n\tstate      *state.ThreadState\n\tdone       chan struct{}\n\tnumThreads int\n\tmaxThreads int\n\tphpIni     map[string]string\n}\n\nvar (\n\tphpThreads    []*phpThread\n\tmainThread    *phpMainThread\n\tcommonHeaders map[string]*C.zend_string\n)\n\n// initPHPThreads starts the main PHP thread,\n// a fixed number of inactive PHP threads\n// and reserves a fixed number of possible PHP threads\nfunc initPHPThreads(numThreads int, numMaxThreads int, phpIni map[string]string) (*phpMainThread, error) {\n\tmainThread = &phpMainThread{\n\t\tstate:      state.NewThreadState(),\n\t\tdone:       make(chan struct{}),\n\t\tnumThreads: numThreads,\n\t\tmaxThreads: numMaxThreads,\n\t\tphpIni:     phpIni,\n\t}\n\n\t// initialize the first thread\n\t// this needs to happen before starting the main thread\n\t// since some extensions access environment variables on startup\n\t// the threadIndex on the main thread defaults to 0 -> phpThreads[0].Pin(...)\n\tinitialThread := newPHPThread(0)\n\tphpThreads = []*phpThread{initialThread}\n\n\tif err := mainThread.start(); err != nil {\n\t\treturn nil, err\n\t}\n\n\t// initialize all other threads\n\tphpThreads = make([]*phpThread, mainThread.maxThreads)\n\tphpThreads[0] = initialThread\n\tfor i := 1; i < mainThread.maxThreads; i++ {\n\t\tphpThreads[i] = newPHPThread(i)\n\t}\n\n\t// start the underlying C threads\n\tvar ready sync.WaitGroup\n\n\tfor i := 0; i < numThreads; i++ {\n\t\tready.Go(phpThreads[i].boot)\n\t}\n\n\tready.Wait()\n\n\treturn mainThread, nil\n}\n\nfunc drainPHPThreads() {\n\tif mainThread == nil {\n\t\treturn // mainThread was never initialized\n\t}\n\tdoneWG := sync.WaitGroup{}\n\tdoneWG.Add(len(phpThreads))\n\tmainThread.state.Set(state.ShuttingDown)\n\tclose(mainThread.done)\n\tfor _, thread := range phpThreads {\n\t\t// shut down all reserved threads\n\t\tif thread.state.CompareAndSwap(state.Reserved, state.Done) {\n\t\t\tdoneWG.Done()\n\t\t\tcontinue\n\t\t}\n\t\t// shut down all active threads\n\t\tgo func(thread *phpThread) {\n\t\t\tthread.shutdown()\n\t\t\tdoneWG.Done()\n\t\t}(thread)\n\t}\n\n\tdoneWG.Wait()\n\tmainThread.state.Set(state.Done)\n\tmainThread.state.WaitFor(state.Reserved)\n\tphpThreads = nil\n}\n\nfunc (mainThread *phpMainThread) start() error {\n\tif C.frankenphp_new_main_thread(C.int(mainThread.numThreads)) != 0 {\n\t\treturn ErrMainThreadCreation\n\t}\n\n\tmainThread.state.WaitFor(state.Ready)\n\n\t// cache common request headers as zend_strings (HTTP_ACCEPT, HTTP_USER_AGENT, etc.)\n\tif commonHeaders == nil {\n\t\tcommonHeaders = make(map[string]*C.zend_string, len(phpheaders.CommonRequestHeaders))\n\t\tfor key, phpKey := range phpheaders.CommonRequestHeaders {\n\t\t\tcommonHeaders[key] = newPersistentZendString(phpKey)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc getInactivePHPThread() *phpThread {\n\tfor _, thread := range phpThreads {\n\t\tif thread.state.Is(state.Inactive) {\n\t\t\treturn thread\n\t\t}\n\t}\n\n\tfor _, thread := range phpThreads {\n\t\tif thread.state.CompareAndSwap(state.Reserved, state.BootRequested) {\n\t\t\tthread.boot()\n\t\t\treturn thread\n\t\t}\n\t}\n\n\treturn nil\n}\n\n//export go_frankenphp_main_thread_is_ready\nfunc go_frankenphp_main_thread_is_ready() {\n\tmainThread.setAutomaticMaxThreads()\n\tif mainThread.maxThreads < mainThread.numThreads {\n\t\tmainThread.maxThreads = mainThread.numThreads\n\t}\n\n\tmainThread.state.Set(state.Ready)\n\tmainThread.state.WaitFor(state.Done)\n}\n\n// max_threads = auto\n// setAutomaticMaxThreads estimates the amount of threads based on php.ini and system memory_limit\n// If unable to get the system's memory limit, simply double num_threads\nfunc (mainThread *phpMainThread) setAutomaticMaxThreads() {\n\tif mainThread.maxThreads >= 0 {\n\t\treturn\n\t}\n\tperThreadMemoryLimit := int64(C.frankenphp_get_current_memory_limit())\n\ttotalSysMemory := memory.TotalSysMemory()\n\tif perThreadMemoryLimit <= 0 || totalSysMemory == 0 {\n\t\tmainThread.maxThreads = mainThread.numThreads * 2\n\t\treturn\n\t}\n\tmaxAllowedThreads := totalSysMemory / uint64(perThreadMemoryLimit)\n\tmainThread.maxThreads = int(maxAllowedThreads)\n\n\tif globalLogger.Enabled(globalCtx, slog.LevelDebug) {\n\t\tglobalLogger.LogAttrs(globalCtx, slog.LevelDebug, \"Automatic thread limit\", slog.Int(\"perThreadMemoryLimitMB\", int(perThreadMemoryLimit/1024/1024)), slog.Int(\"maxThreads\", mainThread.maxThreads))\n\t}\n}\n\n//export go_frankenphp_shutdown_main_thread\nfunc go_frankenphp_shutdown_main_thread() {\n\tmainThread.state.Set(state.Reserved)\n}\n\n//export go_get_custom_php_ini\nfunc go_get_custom_php_ini(disableTimeouts C.bool) *C.char {\n\tif mainThread.phpIni == nil {\n\t\tmainThread.phpIni = make(map[string]string)\n\t}\n\n\t// Timeouts are currently fundamentally broken\n\t// with ZTS except on Linux and FreeBSD: https://bugs.php.net/bug.php?id=79464\n\t// Disable timeouts if ZEND_MAX_EXECUTION_TIMERS is not supported\n\tif disableTimeouts {\n\t\tmainThread.phpIni[\"max_execution_time\"] = \"0\"\n\t\tmainThread.phpIni[\"max_input_time\"] = \"-1\"\n\t}\n\n\t// Pass the php.ini overrides to PHP before startup\n\t// TODO: if needed this would also be possible on a per-thread basis\n\tvar overrides strings.Builder\n\n\t// 32 is an over-estimate for php.ini settings\n\toverrides.Grow(len(mainThread.phpIni) * 32)\n\tfor k, v := range mainThread.phpIni {\n\t\toverrides.WriteString(k)\n\t\toverrides.WriteByte('=')\n\t\toverrides.WriteString(v)\n\t\toverrides.WriteByte('\\n')\n\t}\n\n\treturn C.CString(overrides.String())\n}\n"
  },
  {
    "path": "phpmainthread_test.go",
    "content": "package frankenphp\n\nimport (\n\t\"io\"\n\t\"math/rand/v2\"\n\t\"net/http/httptest\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/dunglas/frankenphp/internal/state\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nvar testDataPath, _ = filepath.Abs(\"./testdata\")\n\nfunc setupGlobals(t *testing.T) {\n\tt.Helper()\n\n\tt.Cleanup(Shutdown)\n\n\tresetGlobals()\n}\n\nfunc TestStartAndStopTheMainThreadWithOneInactiveThread(t *testing.T) {\n\t_, err := initPHPThreads(1, 1, nil) // boot 1 thread\n\tassert.NoError(t, err)\n\n\tassert.Len(t, phpThreads, 1)\n\tassert.Equal(t, 0, phpThreads[0].threadIndex)\n\tassert.True(t, phpThreads[0].state.Is(state.Inactive))\n\n\tdrainPHPThreads()\n\n\tassert.Nil(t, phpThreads)\n}\n\nfunc TestTransitionRegularThreadToWorkerThread(t *testing.T) {\n\tsetupGlobals(t)\n\n\t_, err := initPHPThreads(1, 1, nil)\n\tassert.NoError(t, err)\n\n\t// transition to regular thread\n\tconvertToRegularThread(phpThreads[0])\n\tassert.IsType(t, &regularThread{}, phpThreads[0].handler)\n\n\t// transition to worker thread\n\tworker := getDummyWorker(t, \"transition-worker-1.php\")\n\tconvertToWorkerThread(phpThreads[0], worker)\n\tassert.IsType(t, &workerThread{}, phpThreads[0].handler)\n\tassert.Len(t, worker.threads, 1)\n\n\t// transition back to inactive thread\n\tconvertToInactiveThread(phpThreads[0])\n\tassert.IsType(t, &inactiveThread{}, phpThreads[0].handler)\n\tassert.Len(t, worker.threads, 0)\n\n\tdrainPHPThreads()\n\tassert.Nil(t, phpThreads)\n}\n\nfunc TestTransitionAThreadBetween2DifferentWorkers(t *testing.T) {\n\tsetupGlobals(t)\n\n\t_, err := initPHPThreads(1, 1, nil)\n\tassert.NoError(t, err)\n\tfirstWorker := getDummyWorker(t, \"transition-worker-1.php\")\n\tsecondWorker := getDummyWorker(t, \"transition-worker-2.php\")\n\n\t// convert to first worker thread\n\tconvertToWorkerThread(phpThreads[0], firstWorker)\n\tfirstHandler := phpThreads[0].handler.(*workerThread)\n\tassert.Same(t, firstWorker, firstHandler.worker)\n\tassert.Len(t, firstWorker.threads, 1)\n\tassert.Len(t, secondWorker.threads, 0)\n\n\t// convert to second worker thread\n\tconvertToWorkerThread(phpThreads[0], secondWorker)\n\tsecondHandler := phpThreads[0].handler.(*workerThread)\n\tassert.Same(t, secondWorker, secondHandler.worker)\n\tassert.Len(t, firstWorker.threads, 0)\n\tassert.Len(t, secondWorker.threads, 1)\n\n\tdrainPHPThreads()\n\tassert.Nil(t, phpThreads)\n}\n\n// try all possible handler transitions\n// takes around 200ms and is supposed to force race conditions\nfunc TestTransitionThreadsWhileDoingRequests(t *testing.T) {\n\tt.Cleanup(Shutdown)\n\n\tvar (\n\t\tisDone atomic.Bool\n\t\twg     sync.WaitGroup\n\t)\n\n\tnumThreads := 10\n\tnumRequestsPerThread := 100\n\tworker1Path := filepath.Join(testDataPath, \"transition-worker-1.php\")\n\tworker1Name := \"worker-1\"\n\tworker2Path := filepath.Join(testDataPath, \"transition-worker-2.php\")\n\tworker2Name := \"worker-2\"\n\n\tassert.NoError(t, Init(\n\t\tWithNumThreads(numThreads),\n\t\tWithWorkers(worker1Name, worker1Path, 1,\n\t\t\tWithWorkerEnv(map[string]string{\"ENV1\": \"foo\"}),\n\t\t\tWithWorkerWatchMode([]string{}),\n\t\t\tWithWorkerMaxFailures(0),\n\t\t),\n\t\tWithWorkers(worker2Name, worker2Path, 1,\n\t\t\tWithWorkerEnv(map[string]string{\"ENV1\": \"foo\"}),\n\t\t\tWithWorkerWatchMode([]string{}),\n\t\t\tWithWorkerMaxFailures(0),\n\t\t),\n\t))\n\n\t// try all possible permutations of transition, transition every ms\n\ttransitions := allPossibleTransitions(worker1Path, worker2Path)\n\tfor i := range numThreads {\n\t\tgo func(thread *phpThread, start int) {\n\t\t\tfor {\n\t\t\t\tfor j := start; j < len(transitions); j++ {\n\t\t\t\t\tif isDone.Load() {\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\ttransitions[j](thread)\n\t\t\t\t\ttime.Sleep(time.Millisecond)\n\t\t\t\t}\n\t\t\t\tstart = 0\n\t\t\t}\n\t\t}(phpThreads[i], i)\n\t}\n\n\t// randomly do requests to the 3 endpoints\n\twg.Add(numThreads)\n\tfor i := range numThreads {\n\t\tgo func(i int) {\n\t\t\tfor range numRequestsPerThread {\n\t\t\t\tswitch rand.IntN(3) {\n\t\t\t\tcase 0:\n\t\t\t\t\tassertRequestBody(t, \"http://localhost/transition-worker-1.php\", \"Hello from worker 1\")\n\t\t\t\tcase 1:\n\t\t\t\t\tassertRequestBody(t, \"http://localhost/transition-worker-2.php\", \"Hello from worker 2\")\n\t\t\t\tcase 2:\n\t\t\t\t\tassertRequestBody(t, \"http://localhost/transition-regular.php\", \"Hello from regular thread\")\n\t\t\t\t}\n\t\t\t}\n\t\t\twg.Done()\n\t\t}(i)\n\t}\n\n\t// we are finished as soon as all 1000 requests are done\n\twg.Wait()\n\tisDone.Store(true)\n}\n\nfunc TestFinishBootingAWorkerScript(t *testing.T) {\n\tsetupGlobals(t)\n\n\t_, err := initPHPThreads(1, 1, nil)\n\tassert.NoError(t, err)\n\n\t// boot the worker\n\tworker := getDummyWorker(t, \"transition-worker-1.php\")\n\tconvertToWorkerThread(phpThreads[0], worker)\n\tphpThreads[0].state.WaitFor(state.Ready)\n\n\tassert.NotNil(t, phpThreads[0].handler.(*workerThread).dummyContext)\n\tassert.Nil(t, phpThreads[0].handler.(*workerThread).workerContext)\n\tassert.False(\n\t\tt,\n\t\tphpThreads[0].handler.(*workerThread).isBootingScript,\n\t\t\"isBootingScript should be false after the worker thread is ready\",\n\t)\n\n\tdrainPHPThreads()\n\tassert.Nil(t, phpThreads)\n}\n\nfunc TestReturnAnErrorIf2WorkersHaveTheSameFileName(t *testing.T) {\n\tworkers = []*worker{}\n\tworkersByName = map[string]*worker{}\n\tworkersByPath = map[string]*worker{}\n\tw, err1 := newWorker(workerOpt{fileName: testDataPath + \"/index.php\"})\n\tassert.NoError(t, err1)\n\tworkers = append(workers, w)\n\tworkersByName[w.name] = w\n\tworkersByPath[w.fileName] = w\n\t_, err2 := newWorker(workerOpt{fileName: testDataPath + \"/index.php\"})\n\tassert.Error(t, err2, \"two workers cannot have the same filename\")\n}\n\nfunc TestReturnAnErrorIf2ModuleWorkersHaveTheSameName(t *testing.T) {\n\tworkers = []*worker{}\n\tworkersByName = map[string]*worker{}\n\tworkersByPath = map[string]*worker{}\n\tw, err1 := newWorker(workerOpt{fileName: testDataPath + \"/index.php\", name: \"workername\"})\n\tassert.NoError(t, err1)\n\tworkers = append(workers, w)\n\tworkersByName[w.name] = w\n\tworkersByPath[w.fileName] = w\n\t_, err2 := newWorker(workerOpt{fileName: testDataPath + \"/hello.php\", name: \"workername\"})\n\tassert.Error(t, err2, \"two workers cannot have the same name\")\n}\n\nfunc getDummyWorker(t *testing.T, fileName string) *worker {\n\tt.Helper()\n\n\tif workers == nil {\n\t\tworkers = []*worker{}\n\t}\n\n\tworker, _ := newWorker(workerOpt{\n\t\tfileName: testDataPath + \"/\" + fileName,\n\t\tnum:      1,\n\t})\n\tworkers = append(workers, worker)\n\n\treturn worker\n}\n\nfunc assertRequestBody(t *testing.T, url string, expected string) {\n\tr := httptest.NewRequest(\"GET\", url, nil)\n\tw := httptest.NewRecorder()\n\n\treq, err := NewRequestWithContext(r, WithRequestDocumentRoot(testDataPath, false))\n\tassert.NoError(t, err)\n\terr = ServeHTTP(w, req)\n\tassert.NoError(t, err)\n\tresp := w.Result()\n\tbody, _ := io.ReadAll(resp.Body)\n\tassert.Equal(t, expected, string(body))\n}\n\n// create a mix of possible transitions of workers and regular threads\nfunc allPossibleTransitions(worker1Path string, worker2Path string) []func(*phpThread) {\n\treturn []func(*phpThread){\n\t\tconvertToRegularThread,\n\t\tfunc(thread *phpThread) { thread.shutdown() },\n\t\tfunc(thread *phpThread) {\n\t\t\tif thread.state.Is(state.Reserved) {\n\t\t\t\tthread.boot()\n\t\t\t}\n\t\t},\n\t\tfunc(thread *phpThread) { convertToWorkerThread(thread, workersByPath[worker1Path]) },\n\t\tconvertToInactiveThread,\n\t\tfunc(thread *phpThread) { convertToWorkerThread(thread, workersByPath[worker2Path]) },\n\t\tconvertToInactiveThread,\n\t}\n}\n\nfunc TestCorrectThreadCalculation(t *testing.T) {\n\tmaxProcs := runtime.GOMAXPROCS(0) * 2\n\toneWorkerThread := []workerOpt{{num: 1}}\n\n\t// default values\n\ttestThreadCalculation(t, maxProcs, maxProcs, &opt{})\n\ttestThreadCalculation(t, maxProcs, maxProcs, &opt{workers: oneWorkerThread})\n\n\t// num_threads is set\n\ttestThreadCalculation(t, 1, 1, &opt{numThreads: 1})\n\ttestThreadCalculation(t, 2, 2, &opt{numThreads: 2, workers: oneWorkerThread})\n\n\t// max_threads is set\n\ttestThreadCalculation(t, 1, 10, &opt{maxThreads: 10})\n\ttestThreadCalculation(t, 2, 10, &opt{maxThreads: 10, workers: oneWorkerThread})\n\ttestThreadCalculation(t, 5, 10, &opt{numThreads: 5, maxThreads: 10, workers: oneWorkerThread})\n\n\t// automatic max_threads\n\ttestThreadCalculation(t, 1, -1, &opt{maxThreads: -1})\n\ttestThreadCalculation(t, 2, -1, &opt{maxThreads: -1, workers: oneWorkerThread})\n\ttestThreadCalculation(t, 2, -1, &opt{numThreads: 2, maxThreads: -1})\n\n\t// max_threads should be thread minimum + sum of worker max_threads\n\ttestThreadCalculation(t, 2, 6, &opt{workers: []workerOpt{{num: 1, maxThreads: 5}}})\n\ttestThreadCalculation(t, 6, 9, &opt{workers: []workerOpt{{num: 1, maxThreads: 4}, {num: 4, maxThreads: 4}}})\n\ttestThreadCalculation(t, 10, 14, &opt{numThreads: 10, workers: []workerOpt{{num: 1, maxThreads: 4}, {num: 3, maxThreads: 4}}})\n\n\t// max_threads should remain equal to overall max_threads\n\ttestThreadCalculation(t, 2, 5, &opt{maxThreads: 5, workers: []workerOpt{{num: 1, maxThreads: 3}}})\n\ttestThreadCalculation(t, 3, 5, &opt{maxThreads: 5, workers: []workerOpt{{num: 1, maxThreads: 4}, {num: 1, maxThreads: 4}}})\n\n\t// not enough num threads\n\ttestThreadCalculationError(t, &opt{numThreads: 1, workers: oneWorkerThread})\n\ttestThreadCalculationError(t, &opt{numThreads: 1, maxThreads: 1, workers: oneWorkerThread})\n\n\t// not enough max_threads\n\ttestThreadCalculationError(t, &opt{numThreads: 2, maxThreads: 1})\n\ttestThreadCalculationError(t, &opt{maxThreads: 1, workers: oneWorkerThread})\n\n\t// worker max_threads is bigger than overall max_threads\n\ttestThreadCalculationError(t, &opt{maxThreads: 5, workers: []workerOpt{{num: 1, maxThreads: 10}}})\n\n\t// worker max_threads is smaller than num_threads\n\ttestThreadCalculationError(t, &opt{workers: []workerOpt{{num: 3, maxThreads: 2}}})\n}\n\nfunc testThreadCalculation(t *testing.T, expectedNumThreads int, expectedMaxThreads int, o *opt) {\n\tt.Helper()\n\n\t_, err := calculateMaxThreads(o)\n\tassert.NoError(t, err, \"no error should be returned\")\n\tassert.Equal(t, expectedNumThreads, o.numThreads, \"num_threads must be correct\")\n\tassert.Equal(t, expectedMaxThreads, o.maxThreads, \"max_threads must be correct\")\n}\n\nfunc testThreadCalculationError(t *testing.T, o *opt) {\n\tt.Helper()\n\n\t_, err := calculateMaxThreads(o)\n\tassert.Error(t, err, \"configuration must error\")\n}\n"
  },
  {
    "path": "phpthread.go",
    "content": "package frankenphp\n\n// #cgo nocallback frankenphp_new_php_thread\n// #include \"frankenphp.h\"\nimport \"C\"\nimport (\n\t\"context\"\n\t\"runtime\"\n\t\"sync\"\n\t\"unsafe\"\n\n\t\"github.com/dunglas/frankenphp/internal/state\"\n)\n\n// representation of the actual underlying PHP thread\n// identified by the index in the phpThreads slice\ntype phpThread struct {\n\truntime.Pinner\n\tthreadIndex int\n\trequestChan chan contextHolder\n\tdrainChan   chan struct{}\n\thandlerMu   sync.RWMutex\n\thandler     threadHandler\n\tstate       *state.ThreadState\n}\n\n// threadHandler defines how the callbacks from the C thread should be handled\ntype threadHandler interface {\n\tname() string\n\tbeforeScriptExecution() string\n\tafterScriptExecution(exitStatus int)\n\tcontext() context.Context\n\tfrankenPHPContext() *frankenPHPContext\n}\n\nfunc newPHPThread(threadIndex int) *phpThread {\n\treturn &phpThread{\n\t\tthreadIndex: threadIndex,\n\t\trequestChan: make(chan contextHolder),\n\t\tstate:       state.NewThreadState(),\n\t}\n}\n\n// boot starts the underlying PHP thread\nfunc (thread *phpThread) boot() {\n\t// thread must be in reserved state to boot\n\tif !thread.state.CompareAndSwap(state.Reserved, state.Booting) && !thread.state.CompareAndSwap(state.BootRequested, state.Booting) {\n\t\tpanic(\"thread is not in reserved state: \" + thread.state.Name())\n\t}\n\n\t// boot threads as inactive\n\tthread.handlerMu.Lock()\n\tthread.handler = &inactiveThread{thread: thread}\n\tthread.drainChan = make(chan struct{})\n\tthread.handlerMu.Unlock()\n\n\t// start the actual posix thread - TODO: try this with go threads instead\n\tif !C.frankenphp_new_php_thread(C.uintptr_t(thread.threadIndex)) {\n\t\tpanic(\"unable to create thread\")\n\t}\n\n\tthread.state.WaitFor(state.Inactive)\n}\n\n// shutdown the underlying PHP thread\nfunc (thread *phpThread) shutdown() {\n\tif !thread.state.RequestSafeStateChange(state.ShuttingDown) {\n\t\t// already shutting down or done, wait for the C thread to finish\n\t\tthread.state.WaitFor(state.Done, state.Reserved)\n\n\t\treturn\n\t}\n\n\tclose(thread.drainChan)\n\tthread.state.WaitFor(state.Done)\n\tthread.drainChan = make(chan struct{})\n\n\t// threads go back to the reserved state from which they can be booted again\n\tif mainThread.state.Is(state.Ready) {\n\t\tthread.state.Set(state.Reserved)\n\t}\n}\n\n// setHandler changes the thread handler safely\n// must be called from outside the PHP thread\nfunc (thread *phpThread) setHandler(handler threadHandler) {\n\tthread.handlerMu.Lock()\n\tdefer thread.handlerMu.Unlock()\n\n\tif !thread.state.RequestSafeStateChange(state.TransitionRequested) {\n\t\t// no state change allowed == shutdown or done\n\t\treturn\n\t}\n\n\tclose(thread.drainChan)\n\n\tthread.state.WaitFor(state.TransitionInProgress)\n\tthread.handler = handler\n\tthread.drainChan = make(chan struct{})\n\tthread.state.Set(state.TransitionComplete)\n}\n\n// transition to a new handler safely\n// is triggered by setHandler and executed on the PHP thread\nfunc (thread *phpThread) transitionToNewHandler() string {\n\tthread.state.Set(state.TransitionInProgress)\n\tthread.state.WaitFor(state.TransitionComplete)\n\n\t// execute beforeScriptExecution of the new handler\n\treturn thread.handler.beforeScriptExecution()\n}\n\nfunc (thread *phpThread) frankenPHPContext() *frankenPHPContext {\n\treturn thread.handler.frankenPHPContext()\n}\n\nfunc (thread *phpThread) context() context.Context {\n\tif thread.handler == nil {\n\t\t// handler can be nil when using opcache.preload\n\t\treturn globalCtx\n\t}\n\n\treturn thread.handler.context()\n}\n\nfunc (thread *phpThread) name() string {\n\tthread.handlerMu.RLock()\n\tname := thread.handler.name()\n\tthread.handlerMu.RUnlock()\n\n\treturn name\n}\n\n// Pin a string that is not null-terminated\n// PHP's zend_string may contain null-bytes\nfunc (thread *phpThread) pinString(s string) *C.char {\n\tsData := unsafe.StringData(s)\n\tif sData == nil {\n\t\treturn nil\n\t}\n\n\tthread.Pin(sData)\n\n\treturn (*C.char)(unsafe.Pointer(sData))\n}\n\n// C strings must be null-terminated\nfunc (thread *phpThread) pinCString(s string) *C.char {\n\treturn thread.pinString(s + \"\\x00\")\n}\n\nfunc (*phpThread) updateContext(isWorker bool) {\n\tC.frankenphp_update_local_thread_context(C.bool(isWorker))\n}\n\n//export go_frankenphp_before_script_execution\nfunc go_frankenphp_before_script_execution(threadIndex C.uintptr_t) *C.char {\n\tthread := phpThreads[threadIndex]\n\tscriptName := thread.handler.beforeScriptExecution()\n\n\t// if no scriptName is passed, shut down\n\tif scriptName == \"\" {\n\t\treturn nil\n\t}\n\n\t// return the name of the PHP script that should be executed\n\treturn thread.pinCString(scriptName)\n}\n\n//export go_frankenphp_after_script_execution\nfunc go_frankenphp_after_script_execution(threadIndex C.uintptr_t, exitStatus C.int) {\n\tthread := phpThreads[threadIndex]\n\tif exitStatus < 0 {\n\t\tpanic(ErrScriptExecution)\n\t}\n\tthread.handler.afterScriptExecution(int(exitStatus))\n\n\t// unpin all memory used during script execution\n\tthread.Unpin()\n}\n\n//export go_frankenphp_on_thread_shutdown\nfunc go_frankenphp_on_thread_shutdown(threadIndex C.uintptr_t) {\n\tthread := phpThreads[threadIndex]\n\tthread.Unpin()\n\tthread.state.Set(state.Done)\n}\n"
  },
  {
    "path": "recorder_test.go",
    "content": "// Remove me when https://github.com/golang/go/pull/56151 will be merged\n\n// Copyright 2011 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage frankenphp_test\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptrace\"\n\t\"net/textproto\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"golang.org/x/net/http/httpguts\"\n)\n\n// ResponseRecorder is an implementation of http.ResponseWriter that\n// records its mutations for later inspection in tests.\ntype ResponseRecorder struct {\n\t// Code is the HTTP response code set by WriteHeader.\n\t//\n\t// Note that if a Handler never calls WriteHeader or Write,\n\t// this might end up being 0, rather than the implicit\n\t// http.StatusOK. To get the implicit value, use the Result\n\t// method.\n\tCode int\n\n\t// HeaderMap contains the headers explicitly set by the Handler.\n\t// It is an internal detail.\n\t//\n\t// Deprecated: HeaderMap exists for historical compatibility\n\t// and should not be used. To access the headers returned by a handler,\n\t// use the Response.Header map as returned by the Result method.\n\tHeaderMap http.Header\n\n\t// Body is the buffer to which the Handler's Write calls are sent.\n\t// If nil, the Writes are silently discarded.\n\tBody *bytes.Buffer\n\n\t// Flushed is whether the Handler called Flush.\n\tFlushed bool\n\n\t// ClientTrace is used to trace 1XX responses\n\tClientTrace *httptrace.ClientTrace\n\n\tresult      *http.Response // cache of Result's return value\n\tsnapHeader  http.Header    // snapshot of HeaderMap at first Write\n\twroteHeader bool\n}\n\n// NewRecorder returns an initialized ResponseRecorder.\nfunc NewRecorder() *ResponseRecorder {\n\treturn &ResponseRecorder{\n\t\tHeaderMap: make(http.Header),\n\t\tBody:      new(bytes.Buffer),\n\t\tCode:      200,\n\t}\n}\n\n// DefaultRemoteAddr is the default remote address to return in RemoteAddr if\n// an explicit DefaultRemoteAddr isn't set on ResponseRecorder.\nconst DefaultRemoteAddr = \"1.2.3.4\"\n\n// Header implements http.ResponseWriter. It returns the response\n// headers to mutate within a handler. To test the headers that were\n// written after a handler completes, use the Result method and see\n// the returned Response value's Header.\nfunc (rw *ResponseRecorder) Header() http.Header {\n\tm := rw.HeaderMap\n\tif m == nil {\n\t\tm = make(http.Header)\n\t\trw.HeaderMap = m\n\t}\n\treturn m\n}\n\n// writeHeader writes a header if it was not written yet and\n// detects Content-Type if needed.\n//\n// bytes or str are the beginning of the response body.\n// We pass both to avoid unnecessarily generate garbage\n// in rw.WriteString which was created for performance reasons.\n// Non-nil bytes win.\nfunc (rw *ResponseRecorder) writeHeader(b []byte, str string) {\n\tif rw.wroteHeader {\n\t\treturn\n\t}\n\tif len(str) > 512 {\n\t\tstr = str[:512]\n\t}\n\n\tm := rw.Header()\n\n\t_, hasType := m[\"Content-Type\"]\n\thasTE := m.Get(\"Transfer-Encoding\") != \"\"\n\tif !hasType && !hasTE {\n\t\tif b == nil {\n\t\t\tb = []byte(str)\n\t\t}\n\t\tm.Set(\"Content-Type\", http.DetectContentType(b))\n\t}\n\n\trw.WriteHeader(200)\n}\n\n// Write implements http.ResponseWriter. The data in buf is written to\n// rw.Body, if not nil.\nfunc (rw *ResponseRecorder) Write(buf []byte) (int, error) {\n\trw.writeHeader(buf, \"\")\n\tif rw.Body != nil {\n\t\trw.Body.Write(buf)\n\t}\n\treturn len(buf), nil\n}\n\n// WriteString implements io.StringWriter. The data in str is written\n// to rw.Body, if not nil.\nfunc (rw *ResponseRecorder) WriteString(str string) (int, error) {\n\trw.writeHeader(nil, str)\n\tif rw.Body != nil {\n\t\trw.Body.WriteString(str)\n\t}\n\treturn len(str), nil\n}\n\nfunc checkWriteHeaderCode(code int) {\n\t// Issue 22880: require valid WriteHeader status codes.\n\t// For now, we only enforce that it's three digits.\n\t// In the future we might block things over 599 (600 and above aren't defined\n\t// at https://httpwg.org/specs/rfc7231.html#status.codes)\n\t// and we might block under 200 (once we have more mature 1xx support).\n\t// But for now any three digits.\n\t//\n\t// We used to send \"HTTP/1.1 000 0\" on the wire in responses but there's\n\t// no equivalent bogus thing we can realistically send in HTTP/2,\n\t// so we'll consistently panic instead and help people find their bugs\n\t// early. (We can't return an error from WriteHeader even if we wanted to.)\n\tif code < 100 || code > 999 {\n\t\tpanic(fmt.Sprintf(\"invalid WriteHeader code %v\", code))\n\t}\n}\n\n// WriteHeader implements http.ResponseWriter.\nfunc (rw *ResponseRecorder) WriteHeader(code int) {\n\tif rw.wroteHeader {\n\t\treturn\n\t}\n\n\tcheckWriteHeaderCode(code)\n\n\tif rw.ClientTrace != nil && code >= 100 && code < 200 {\n\t\tif code == 100 {\n\t\t\trw.ClientTrace.Got100Continue()\n\t\t}\n\t\t// treat 101 as a terminal status, see issue 26161\n\t\tif code != http.StatusSwitchingProtocols {\n\t\t\tif err := rw.ClientTrace.Got1xxResponse(code, textproto.MIMEHeader(rw.HeaderMap)); err != nil {\n\t\t\t\tpanic(err)\n\t\t\t}\n\t\t\treturn\n\t\t}\n\t}\n\n\trw.Code = code\n\trw.wroteHeader = true\n\tif rw.HeaderMap == nil {\n\t\trw.HeaderMap = make(http.Header)\n\t}\n\trw.snapHeader = rw.HeaderMap.Clone()\n}\n\n// Flush implements http.Flusher. To test whether Flush was\n// called, see rw.Flushed.\nfunc (rw *ResponseRecorder) Flush() {\n\tif !rw.wroteHeader {\n\t\trw.WriteHeader(200)\n\t}\n\trw.Flushed = true\n}\n\n// Result returns the response generated by the handler.\n//\n// The returned Response will have at least its StatusCode,\n// Header, Body, and optionally Trailer populated.\n// More fields may be populated in the future, so callers should\n// not DeepEqual the result in tests.\n//\n// The Response.Header is a snapshot of the headers at the time of the\n// first write call, or at the time of this call, if the handler never\n// did a write.\n//\n// The Response.Body is guaranteed to be non-nil and Body.Read call is\n// guaranteed to not return any error other than io.EOF.\n//\n// Result must only be called after the handler has finished running.\nfunc (rw *ResponseRecorder) Result() *http.Response {\n\tif rw.result != nil {\n\t\treturn rw.result\n\t}\n\tif rw.snapHeader == nil {\n\t\trw.snapHeader = rw.HeaderMap.Clone()\n\t}\n\tres := &http.Response{\n\t\tProto:      \"HTTP/1.1\",\n\t\tProtoMajor: 1,\n\t\tProtoMinor: 1,\n\t\tStatusCode: rw.Code,\n\t\tHeader:     rw.snapHeader,\n\t}\n\trw.result = res\n\tif res.StatusCode == 0 {\n\t\tres.StatusCode = 200\n\t}\n\tres.Status = fmt.Sprintf(\"%03d %s\", res.StatusCode, http.StatusText(res.StatusCode))\n\tif rw.Body != nil {\n\t\tres.Body = io.NopCloser(bytes.NewReader(rw.Body.Bytes()))\n\t} else {\n\t\tres.Body = http.NoBody\n\t}\n\tres.ContentLength = parseContentLength(res.Header.Get(\"Content-Length\"))\n\n\tif trailers, ok := rw.snapHeader[\"Trailer\"]; ok {\n\t\tres.Trailer = make(http.Header, len(trailers))\n\t\tfor _, k := range trailers {\n\t\t\tfor k := range strings.SplitSeq(k, \",\") {\n\t\t\t\tk = http.CanonicalHeaderKey(textproto.TrimString(k))\n\t\t\t\tif !httpguts.ValidTrailerHeader(k) {\n\t\t\t\t\t// Ignore since forbidden by RFC 7230, section 4.1.2.\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tvv, ok := rw.HeaderMap[k]\n\t\t\t\tif !ok {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tvv2 := make([]string, len(vv))\n\t\t\t\tcopy(vv2, vv)\n\t\t\t\tres.Trailer[k] = vv2\n\t\t\t}\n\t\t}\n\t}\n\tfor k, vv := range rw.HeaderMap {\n\t\tif !strings.HasPrefix(k, http.TrailerPrefix) {\n\t\t\tcontinue\n\t\t}\n\t\tif res.Trailer == nil {\n\t\t\tres.Trailer = make(http.Header)\n\t\t}\n\t\tfor _, v := range vv {\n\t\t\tres.Trailer.Add(strings.TrimPrefix(k, http.TrailerPrefix), v)\n\t\t}\n\t}\n\treturn res\n}\n\n// parseContentLength trims whitespace from s and returns -1 if no value\n// is set, or the value if it's >= 0.\n//\n// This a modified version of same function found in net/http/transfer.go. This\n// one just ignores an invalid header.\nfunc parseContentLength(cl string) int64 {\n\tcl = textproto.TrimString(cl)\n\tif cl == \"\" {\n\t\treturn -1\n\t}\n\tn, err := strconv.ParseUint(cl, 10, 63)\n\tif err != nil {\n\t\treturn -1\n\t}\n\treturn int64(n)\n}\n"
  },
  {
    "path": "release.sh",
    "content": "#!/usr/bin/env bash\n\n# Creates the tags for the library and the Caddy module.\n\nset -o nounset\nset -o errexit\ntrap 'echo \"Aborting due to errexit on line $LINENO. Exit code: $?\" >&2' ERR\nset -o errtrace\nset -o pipefail\nset -o xtrace\n\nif ! type \"git\" >/dev/null; then\n\techo \"The \\\"git\\\" command must be installed.\"\n\texit 1\nfi\n\nif ! type \"gh\" >/dev/null; then\n\techo \"The \\\"gh\\\" command must be installed.\"\n\texit 1\nfi\n\nif ! type \"brew\" >/dev/null; then\n\techo \"The \\\"brew\\\" command must be installed.\"\n\texit 1\nfi\n\nif [[ $# -ne 1 ]]; then\n\techo \"Usage: ./release.sh version\" >&2\n\texit 1\nfi\n\n# Adapted from https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string\nif [[ ! $1 =~ ^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(-((0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*)(\\.(0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*))*))?(\\+([0-9a-zA-Z-]+(\\.[0-9a-zA-Z-]+)*))?$ ]]; then\n\techo \"Invalid version number: $1\" >&2\n\texit 1\nfi\n\ngit checkout main\ngit pull\n\ncd caddy/\ngo get \"github.com/dunglas/frankenphp@v$1\"\ncd -\n\ngit commit -S -a -m \"chore: prepare release $1\" || echo \"skip\"\n\ngit tag -s -m \"Version $1\" \"v$1\"\ngit tag -s -m \"Version $1\" \"caddy/v$1\"\ngit push --follow-tags\n\ntags=$(git tag --list --sort=-version:refname 'v*')\nprevious_tag=$(awk 'NR==2 {print;exit}' <<<\"${tags}\")\n\ngh release create --draft --generate-notes --latest --notes-start-tag \"${previous_tag}\" --verify-tag \"v$1\"\nbrew bump-formula-pr dunglas/frankenphp/frankenphp --version \"$1\"\n"
  },
  {
    "path": "reload_test.sh",
    "content": "#!/bin/bash\nfor ((i = 0; i < 100; i++)); do\n\tcurl --no-progress-meter -o /dev/null http://localhost:2019/config/apps/frankenphp -: --no-progress-meter -o /dev/null -H 'Cache-Control: must-revalidate' -H 'Content-Type: application/json' --data-binary '{\"workers\":[{\"file_name\":\"./index.php\"}]}' -X PATCH http://localhost:2019/config/apps/frankenphp\ndone\n"
  },
  {
    "path": "requestoptions.go",
    "content": "package frankenphp\n\nimport (\n\t\"errors\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"unicode/utf8\"\n\n\t\"github.com/dunglas/frankenphp/internal/fastabs\"\n)\n\n// RequestOption instances allow to configure a FrankenPHP Request.\ntype RequestOption func(h *frankenPHPContext) error\n\nvar (\n\tErrInvalidSplitPath = errors.New(\"split path contains non-ASCII characters\")\n\n\tdocumentRootCache    sync.Map\n\tdocumentRootCacheLen atomic.Uint32\n)\n\n// WithRequestDocumentRoot sets the root directory of the PHP application.\n// if resolveSymlink is true, oath declared as root directory will be resolved\n// to its absolute value after the evaluation of any symbolic links.\n// Due to the nature of PHP opcache, root directory path is cached: when\n// using a symlinked directory as root this could generate errors when\n// symlink is changed without PHP being restarted; enabling this\n// directive will set $_SERVER['DOCUMENT_ROOT'] to the real directory path.\nfunc WithRequestDocumentRoot(documentRoot string, resolveSymlink bool) RequestOption {\n\treturn func(o *frankenPHPContext) (err error) {\n\t\tv, ok := documentRootCache.Load(documentRoot)\n\t\tif !ok {\n\t\t\t// make sure file root is absolute\n\t\t\tv, err = fastabs.FastAbs(documentRoot)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\t// prevent the cache to grow forever, this is a totally arbitrary value\n\t\t\tif documentRootCacheLen.Load() < 1024 {\n\t\t\t\tdocumentRootCache.LoadOrStore(documentRoot, v)\n\t\t\t\tdocumentRootCacheLen.Add(1)\n\t\t\t}\n\t\t}\n\n\t\tif resolveSymlink {\n\t\t\tif v, err = filepath.EvalSymlinks(v.(string)); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\to.documentRoot = v.(string)\n\n\t\treturn nil\n\t}\n}\n\n// WithRequestResolvedDocumentRoot is similar to WithRequestDocumentRoot\n// but doesn't do any checks or resolving on the path to improve performance.\nfunc WithRequestResolvedDocumentRoot(documentRoot string) RequestOption {\n\treturn func(o *frankenPHPContext) error {\n\t\to.documentRoot = documentRoot\n\n\t\treturn nil\n\t}\n}\n\n// WithRequestSplitPath contains a list of split path strings.\n//\n// The path in the URL will be split into two, with the first piece ending\n// with the value of splitPath. The first piece will be assumed as the\n// actual resource (CGI script) name, and the second piece will be set to\n// PATH_INFO for the CGI script to use.\n//\n// Split paths can only contain ASCII characters.\n// Comparison is case-insensitive.\n//\n// Future enhancements should be careful to avoid CVE-2019-11043,\n// which can be mitigated with use of a try_files-like behavior\n// that 404s if the FastCGI path info is not found.\nfunc WithRequestSplitPath(splitPath []string) (RequestOption, error) {\n\tvar b strings.Builder\n\n\tfor i, split := range splitPath {\n\t\tb.Grow(len(split))\n\n\t\tfor j := 0; j < len(split); j++ {\n\t\t\tc := split[j]\n\t\t\tif c >= utf8.RuneSelf {\n\t\t\t\treturn nil, ErrInvalidSplitPath\n\t\t\t}\n\n\t\t\tif 'A' <= c && c <= 'Z' {\n\t\t\t\tb.WriteByte(c + 'a' - 'A')\n\t\t\t} else {\n\t\t\t\tb.WriteByte(c)\n\t\t\t}\n\t\t}\n\n\t\tsplitPath[i] = b.String()\n\t\tb.Reset()\n\t}\n\n\treturn func(o *frankenPHPContext) error {\n\t\to.splitPath = splitPath\n\n\t\treturn nil\n\t}, nil\n}\n\ntype PreparedEnv = map[string]string\n\nfunc PrepareEnv(env map[string]string) PreparedEnv {\n\tpreparedEnv := make(PreparedEnv, len(env))\n\tfor k, v := range env {\n\t\tpreparedEnv[k+\"\\x00\"] = v\n\t}\n\n\treturn preparedEnv\n}\n\n// WithRequestEnv set CGI-like environment variables that will be available in $_SERVER.\n// Values set with WithEnv always have priority over automatically populated values.\nfunc WithRequestEnv(env map[string]string) RequestOption {\n\treturn WithRequestPreparedEnv(PrepareEnv(env))\n}\n\nfunc WithRequestPreparedEnv(env PreparedEnv) RequestOption {\n\treturn func(o *frankenPHPContext) error {\n\t\to.env = env\n\n\t\treturn nil\n\t}\n}\n\nfunc WithOriginalRequest(r *http.Request) RequestOption {\n\treturn func(o *frankenPHPContext) error {\n\t\to.originalRequest = r\n\n\t\treturn nil\n\t}\n}\n\n// WithRequestLogger sets the logger associated with the current request\nfunc WithRequestLogger(logger *slog.Logger) RequestOption {\n\treturn func(o *frankenPHPContext) error {\n\t\to.logger = logger\n\n\t\treturn nil\n\t}\n}\n\n// WithWorkerName sets the worker that should handle the request\nfunc WithWorkerName(name string) RequestOption {\n\treturn func(o *frankenPHPContext) error {\n\t\tif name != \"\" {\n\t\t\to.worker = workersByName[name]\n\t\t}\n\n\t\treturn nil\n\t}\n}\n"
  },
  {
    "path": "requestoptions_test.go",
    "content": "package frankenphp\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestWithRequestSplitPath(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname          string\n\t\tsplitPath     []string\n\t\twantErr       error\n\t\twantSplitPath []string\n\t}{\n\t\t{\n\t\t\tname:          \"valid lowercase split path\",\n\t\t\tsplitPath:     []string{\".php\"},\n\t\t\twantErr:       nil,\n\t\t\twantSplitPath: []string{\".php\"},\n\t\t},\n\t\t{\n\t\t\tname:          \"valid uppercase split path normalized\",\n\t\t\tsplitPath:     []string{\".PHP\"},\n\t\t\twantErr:       nil,\n\t\t\twantSplitPath: []string{\".php\"},\n\t\t},\n\t\t{\n\t\t\tname:          \"valid mixed case split path normalized\",\n\t\t\tsplitPath:     []string{\".PhP\", \".PHTML\"},\n\t\t\twantErr:       nil,\n\t\t\twantSplitPath: []string{\".php\", \".phtml\"},\n\t\t},\n\t\t{\n\t\t\tname:          \"empty split path\",\n\t\t\tsplitPath:     []string{},\n\t\t\twantErr:       nil,\n\t\t\twantSplitPath: []string{},\n\t\t},\n\t\t{\n\t\t\tname:      \"non-ASCII character in split path rejected\",\n\t\t\tsplitPath: []string{\".php\", \".Ⱥphp\"},\n\t\t\twantErr:   ErrInvalidSplitPath,\n\t\t},\n\t\t{\n\t\t\tname:      \"unicode character in split path rejected\",\n\t\t\tsplitPath: []string{\".phpⱥ\"},\n\t\t\twantErr:   ErrInvalidSplitPath,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tctx := &frankenPHPContext{}\n\t\t\topt, err := WithRequestSplitPath(tt.splitPath)\n\n\t\t\tif tt.wantErr != nil {\n\t\t\t\trequire.ErrorIs(t, err, tt.wantErr)\n\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.NoError(t, opt(ctx))\n\t\t\tassert.Equal(t, tt.wantSplitPath, ctx.splitPath)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "scaling.go",
    "content": "package frankenphp\n\nimport (\n\t\"errors\"\n\t\"log/slog\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/dunglas/frankenphp/internal/cpu\"\n\t\"github.com/dunglas/frankenphp/internal/state\"\n)\n\nconst (\n\t// requests have to be stalled for at least this amount of time before scaling\n\tminStallTime = 5 * time.Millisecond\n\t// time to check for CPU usage before scaling a single thread\n\tcpuProbeTime = 120 * time.Millisecond\n\t// do not scale over this amount of CPU usage\n\tmaxCpuUsageForScaling = 0.8\n\t// downscale idle threads every x seconds\n\tdownScaleCheckTime = 5 * time.Second\n\t// max amount of threads stopped in one iteration of downScaleCheckTime\n\tmaxTerminationCount = 10\n\t// default time an autoscaled thread may be idle before being deactivated\n\tdefaultMaxIdleTime = 5 * time.Second\n)\n\nvar (\n\tErrMaxThreadsReached = errors.New(\"max amount of overall threads reached\")\n\n\tmaxIdleTime       = defaultMaxIdleTime\n\tscaleChan         chan *frankenPHPContext\n\tautoScaledThreads = []*phpThread{}\n\tscalingMu         = new(sync.RWMutex)\n)\n\nfunc initAutoScaling(mainThread *phpMainThread) {\n\tif mainThread.maxThreads <= mainThread.numThreads {\n\t\tscaleChan = nil\n\t\treturn\n\t}\n\n\tscalingMu.Lock()\n\tscaleChan = make(chan *frankenPHPContext)\n\tmaxScaledThreads := mainThread.maxThreads - mainThread.numThreads\n\tautoScaledThreads = make([]*phpThread, 0, maxScaledThreads)\n\tscalingMu.Unlock()\n\n\tgo startUpscalingThreads(maxScaledThreads, scaleChan, mainThread.done)\n\tgo startDownScalingThreads(mainThread.done)\n}\n\nfunc drainAutoScaling() {\n\tscalingMu.Lock()\n\n\tif globalLogger.Enabled(globalCtx, slog.LevelDebug) {\n\t\tglobalLogger.LogAttrs(globalCtx, slog.LevelDebug, \"shutting down autoscaling\", slog.Int(\"autoScaledThreads\", len(autoScaledThreads)))\n\t}\n\n\tscalingMu.Unlock()\n}\n\nfunc addRegularThread() (*phpThread, error) {\n\tthread := getInactivePHPThread()\n\tif thread == nil {\n\t\treturn nil, ErrMaxThreadsReached\n\t}\n\tconvertToRegularThread(thread)\n\tthread.state.WaitFor(state.Ready, state.ShuttingDown, state.Reserved)\n\treturn thread, nil\n}\n\nfunc addWorkerThread(worker *worker) (*phpThread, error) {\n\tthread := getInactivePHPThread()\n\tif thread == nil {\n\t\treturn nil, ErrMaxThreadsReached\n\t}\n\tconvertToWorkerThread(thread, worker)\n\tthread.state.WaitFor(state.Ready, state.ShuttingDown, state.Reserved)\n\treturn thread, nil\n}\n\n// scaleWorkerThread adds a worker PHP thread automatically\nfunc scaleWorkerThread(worker *worker) {\n\t// probe CPU usage before acquiring the lock (avoids holding lock during 120ms sleep)\n\tif !cpu.ProbeCPUs(cpuProbeTime, maxCpuUsageForScaling, mainThread.done) {\n\t\treturn\n\t}\n\n\tscalingMu.Lock()\n\tdefer scalingMu.Unlock()\n\n\tif !mainThread.state.Is(state.Ready) {\n\t\treturn\n\t}\n\n\tthread, err := addWorkerThread(worker)\n\tif err != nil {\n\t\tif globalLogger.Enabled(globalCtx, slog.LevelWarn) {\n\t\t\tglobalLogger.LogAttrs(globalCtx, slog.LevelWarn, \"could not increase max_threads, consider raising this limit\", slog.String(\"worker\", worker.name), slog.Any(\"error\", err))\n\t\t}\n\n\t\treturn\n\t}\n\n\tautoScaledThreads = append(autoScaledThreads, thread)\n\n\tif globalLogger.Enabled(globalCtx, slog.LevelInfo) {\n\t\tglobalLogger.LogAttrs(globalCtx, slog.LevelInfo, \"upscaling worker thread\", slog.String(\"worker\", worker.name), slog.Int(\"thread\", thread.threadIndex), slog.Int(\"num_threads\", len(autoScaledThreads)))\n\t}\n}\n\n// scaleRegularThread adds a regular PHP thread automatically\nfunc scaleRegularThread() {\n\t// probe CPU usage before acquiring the lock (avoids holding lock during 120ms sleep)\n\tif !cpu.ProbeCPUs(cpuProbeTime, maxCpuUsageForScaling, mainThread.done) {\n\t\treturn\n\t}\n\n\tscalingMu.Lock()\n\tdefer scalingMu.Unlock()\n\n\tif !mainThread.state.Is(state.Ready) {\n\t\treturn\n\t}\n\n\tthread, err := addRegularThread()\n\tif err != nil {\n\t\tif globalLogger.Enabled(globalCtx, slog.LevelWarn) {\n\t\t\tglobalLogger.LogAttrs(globalCtx, slog.LevelWarn, \"could not increase max_threads, consider raising this limit\", slog.Any(\"error\", err))\n\t\t}\n\n\t\treturn\n\t}\n\n\tautoScaledThreads = append(autoScaledThreads, thread)\n\n\tif globalLogger.Enabled(globalCtx, slog.LevelInfo) {\n\t\tglobalLogger.LogAttrs(globalCtx, slog.LevelInfo, \"upscaling regular thread\", slog.Int(\"thread\", thread.threadIndex), slog.Int(\"num_threads\", len(autoScaledThreads)))\n\t}\n}\n\nfunc startUpscalingThreads(maxScaledThreads int, scale chan *frankenPHPContext, done chan struct{}) {\n\tfor {\n\t\tscalingMu.Lock()\n\t\tscaledThreadCount := len(autoScaledThreads)\n\t\tscalingMu.Unlock()\n\t\tif scaledThreadCount >= maxScaledThreads {\n\t\t\t// we have reached max_threads, check again later\n\t\t\tselect {\n\t\t\tcase <-done:\n\t\t\t\treturn\n\t\t\tcase <-time.After(downScaleCheckTime):\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\tselect {\n\t\tcase fc := <-scale:\n\t\t\ttimeSinceStalled := time.Since(fc.startedAt)\n\n\t\t\t// if the request has not been stalled long enough, wait and repeat\n\t\t\tif timeSinceStalled < minStallTime {\n\t\t\t\tselect {\n\t\t\t\tcase <-done:\n\t\t\t\t\treturn\n\t\t\t\tcase <-time.After(minStallTime - timeSinceStalled):\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// if the request has been stalled long enough, scale\n\t\t\tif fc.worker == nil {\n\t\t\t\tscaleRegularThread()\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// check for max worker threads here again in case requests overflowed while waiting\n\t\t\tif fc.worker.isAtThreadLimit() {\n\t\t\t\tif globalLogger.Enabled(globalCtx, slog.LevelInfo) {\n\t\t\t\t\tglobalLogger.LogAttrs(globalCtx, slog.LevelInfo, \"cannot scale worker thread, max threads reached for worker\", slog.String(\"worker\", fc.worker.name))\n\t\t\t\t}\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tscaleWorkerThread(fc.worker)\n\t\tcase <-done:\n\t\t\treturn\n\t\t}\n\t}\n}\n\nfunc startDownScalingThreads(done chan struct{}) {\n\tfor {\n\t\tselect {\n\t\tcase <-done:\n\t\t\treturn\n\t\tcase <-time.After(downScaleCheckTime):\n\t\t\tdeactivateThreads()\n\t\t}\n\t}\n}\n\n// deactivateThreads checks all threads and removes those that have been inactive for too long\nfunc deactivateThreads() {\n\tstoppedThreadCount := 0\n\tscalingMu.Lock()\n\tdefer scalingMu.Unlock()\n\tfor i := len(autoScaledThreads) - 1; i >= 0; i-- {\n\t\tthread := autoScaledThreads[i]\n\n\t\t// the thread might have been stopped otherwise, remove it\n\t\tif thread.state.Is(state.Reserved) {\n\t\t\tautoScaledThreads = append(autoScaledThreads[:i], autoScaledThreads[i+1:]...)\n\t\t\tcontinue\n\t\t}\n\n\t\twaitTime := thread.state.WaitTime()\n\t\tif stoppedThreadCount > maxTerminationCount || waitTime == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\t// convert threads to inactive if they have been idle for too long\n\t\tif thread.state.Is(state.Ready) && waitTime > maxIdleTime.Milliseconds() {\n\t\t\tconvertToInactiveThread(thread)\n\t\t\tstoppedThreadCount++\n\t\t\tautoScaledThreads = append(autoScaledThreads[:i], autoScaledThreads[i+1:]...)\n\n\t\t\tif globalLogger.Enabled(globalCtx, slog.LevelInfo) {\n\t\t\t\tglobalLogger.LogAttrs(globalCtx, slog.LevelInfo, \"downscaling thread\", slog.Int(\"thread\", thread.threadIndex), slog.Int64(\"wait_time\", waitTime), slog.Int(\"num_threads\", len(autoScaledThreads)))\n\t\t\t}\n\n\t\t\tcontinue\n\t\t}\n\n\t\t// TODO: Completely stopping threads is more memory efficient\n\t\t// Some PECL extensions like #1296 will prevent threads from fully stopping (they leak memory)\n\t\t// Reactivate this if there is a better solution or workaround\n\t\t// if thread.state.Is(state.Inactive) && waitTime > maxThreadIdleTime.Milliseconds() {\n\t\t// \tlogger.LogAttrs(nil, slog.LevelDebug, \"auto-stopping thread\", slog.Int(\"thread\", thread.threadIndex))\n\t\t// \tthread.shutdown()\n\t\t// \tstoppedThreadCount++\n\t\t// \tautoScaledThreads = append(autoScaledThreads[:i], autoScaledThreads[i+1:]...)\n\t\t// \tcontinue\n\t\t// }\n\t}\n}\n"
  },
  {
    "path": "scaling_test.go",
    "content": "package frankenphp\n\nimport (\n\t\"path/filepath\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/dunglas/frankenphp/internal/state\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestScaleARegularThreadUpAndDown(t *testing.T) {\n\tt.Cleanup(Shutdown)\n\n\tassert.NoError(t, Init(\n\t\tWithNumThreads(1),\n\t\tWithMaxThreads(2),\n\t))\n\n\tautoScaledThread := phpThreads[1]\n\n\t// scale up\n\tscaleRegularThread()\n\tassert.Equal(t, state.Ready, autoScaledThread.state.Get())\n\tassert.IsType(t, &regularThread{}, autoScaledThread.handler)\n\n\t// on down-scale, the thread will be marked as inactive\n\tsetLongWaitTime(t, autoScaledThread)\n\tdeactivateThreads()\n\tassert.IsType(t, &inactiveThread{}, autoScaledThread.handler)\n}\n\nfunc TestScaleAWorkerThreadUpAndDown(t *testing.T) {\n\tt.Cleanup(Shutdown)\n\n\tworkerName := \"worker1\"\n\tworkerPath := filepath.Join(testDataPath, \"transition-worker-1.php\")\n\tassert.NoError(t, Init(\n\t\tWithNumThreads(2),\n\t\tWithMaxThreads(3),\n\t\tWithWorkers(workerName, workerPath, 1,\n\t\t\tWithWorkerEnv(map[string]string{}),\n\t\t\tWithWorkerWatchMode([]string{}),\n\t\t\tWithWorkerMaxFailures(0),\n\t\t),\n\t))\n\n\tautoScaledThread := phpThreads[2]\n\n\t// scale up\n\tscaleWorkerThread(workersByPath[workerPath])\n\tassert.Equal(t, state.Ready, autoScaledThread.state.Get())\n\n\t// on down-scale, the thread will be marked as inactive\n\tsetLongWaitTime(t, autoScaledThread)\n\tdeactivateThreads()\n\tassert.IsType(t, &inactiveThread{}, autoScaledThread.handler)\n}\n\nfunc TestMaxIdleTimePreventsEarlyDeactivation(t *testing.T) {\n\tt.Cleanup(Shutdown)\n\n\tassert.NoError(t, Init(\n\t\tWithNumThreads(1),\n\t\tWithMaxThreads(2),\n\t\tWithMaxIdleTime(time.Hour),\n\t))\n\n\tautoScaledThread := phpThreads[1]\n\n\t// scale up\n\tscaleRegularThread()\n\tassert.Equal(t, state.Ready, autoScaledThread.state.Get())\n\n\t// set wait time to 30 minutes (less than 1 hour max idle time)\n\tautoScaledThread.state.SetWaitTime(time.Now().Add(-30 * time.Minute))\n\tdeactivateThreads()\n\tassert.IsType(t, &regularThread{}, autoScaledThread.handler, \"thread should still be active after 30min with 1h max idle time\")\n\n\t// set wait time to over 1 hour (exceeds max idle time)\n\tautoScaledThread.state.SetWaitTime(time.Now().Add(-time.Hour - time.Minute))\n\tdeactivateThreads()\n\tassert.IsType(t, &inactiveThread{}, autoScaledThread.handler, \"thread should be deactivated after exceeding max idle time\")\n}\n\nfunc setLongWaitTime(t *testing.T, thread *phpThread) {\n\tt.Helper()\n\n\tthread.state.SetWaitTime(time.Now().Add(-time.Hour))\n}\n"
  },
  {
    "path": "static-builder-gnu.Dockerfile",
    "content": "# syntax=docker/dockerfile:1\n#checkov:skip=CKV_DOCKER_2\n#checkov:skip=CKV_DOCKER_3\nFROM centos:7\n\nARG FRANKENPHP_VERSION=''\nENV FRANKENPHP_VERSION=${FRANKENPHP_VERSION}\n\nARG PHP_VERSION=''\nENV PHP_VERSION=${PHP_VERSION}\n\n# args passed to static-php-cli\nARG PHP_EXTENSIONS=''\nARG PHP_EXTENSION_LIBS=''\nARG SPC_OPT_BUILD_ARGS\n\n# args passed to xcaddy\nARG XCADDY_ARGS='--with github.com/dunglas/caddy-cbrotli --with github.com/dunglas/mercure/caddy --with github.com/dunglas/vulcain/caddy'\nENV SPC_CMD_VAR_FRANKENPHP_XCADDY_MODULES=\"${XCADDY_ARGS}\"\nARG CLEAN=''\nARG EMBED=''\nARG DEBUG_SYMBOLS=''\nARG MIMALLOC=''\nARG NO_COMPRESS=''\n\n# Go\nARG GO_VERSION\nENV GOTOOLCHAIN=local\n\nSHELL [\"/bin/bash\", \"-o\", \"pipefail\", \"-c\"]\n\n# Pass through CI environment flag so build-static.sh can detect CI context\nARG CI\nENV CI=${CI}\n\n# labels, same as static-builder.Dockerfile\n\nLABEL org.opencontainers.image.title=FrankenPHP\nLABEL org.opencontainers.image.description=\"The modern PHP app server\"\nLABEL org.opencontainers.image.url=https://frankenphp.dev\nLABEL org.opencontainers.image.source=https://github.com/php/frankenphp\nLABEL org.opencontainers.image.licenses=MIT\nLABEL org.opencontainers.image.vendor=\"Kévin Dunglas\"\n\n# yum update\nRUN sed -i 's/mirror.centos.org/vault.centos.org/g' /etc/yum.repos.d/*.repo && \\\n\tsed -i 's/^#.*baseurl=http/baseurl=http/g' /etc/yum.repos.d/*.repo && \\\n\tsed -i 's/^mirrorlist=http/#mirrorlist=http/g' /etc/yum.repos.d/*.repo && \\\n\tyum clean all && \\\n\tyum makecache && \\\n\tyum update -y && \\\n\tyum install -y centos-release-scl\n\n# different arch for different scl repo\nRUN if [ \"$(uname -m)\" = \"aarch64\" ]; then \\\n\t\tsed -i 's|mirror.centos.org/centos|vault.centos.org/altarch|g' /etc/yum.repos.d/CentOS-SCLo-scl-rh.repo ; \\\n\t\tsed -i 's|mirror.centos.org/centos|vault.centos.org/altarch|g' /etc/yum.repos.d/CentOS-SCLo-scl.repo ; \\\n\t\tsed -i 's/^#.*baseurl=http/baseurl=http/g' /etc/yum.repos.d/*.repo ; \\\n\t\tsed -i 's/^mirrorlist=http/#mirrorlist=http/g' /etc/yum.repos.d/*.repo ; \\\n\telse \\\n\t\tsed -i 's/mirror.centos.org/vault.centos.org/g' /etc/yum.repos.d/*.repo ; \\\n\t\tsed -i 's/^#.*baseurl=http/baseurl=http/g' /etc/yum.repos.d/*.repo ; \\\n\t\tsed -i 's/^mirrorlist=http/#mirrorlist=http/g' /etc/yum.repos.d/*.repo ; \\\n\tfi; \\\n\tyum update -y && \\\n\tyum install -y devtoolset-10-gcc-* && \\\n\techo \"source scl_source enable devtoolset-10\" >> /etc/bashrc && \\\n\tsource /etc/bashrc\n\n# install build essentials\nRUN yum install -y \\\n\t\tperl \\\n\t\tmake \\\n\t\tbison \\\n\t\tflex \\\n\t\tgit \\\n\t\tautoconf \\\n\t\tautomake \\\n\t\ttar \\\n\t\tunzip \\\n\t\tgzip \\\n\t\tgcc \\\n\t\tbzip2 \\\n\t\tpatch \\\n\t\txz \\\n\t\tlibtool \\\n\t\tperl-IPC-Cmd ; \\\n\tcurl -o make.tar.gz -fsSL https://ftp.gnu.org/gnu/make/make-4.4.tar.gz && \\\n\ttar -zxvf make.tar.gz && \\\n\tcd make-* && \\\n\t./configure && \\\n\tmake && \\\n\tmake install && \\\n\tln -sf /usr/local/bin/make /usr/bin/make && \\\n\tcd .. && \\\n\trm -Rf make* && \\\n\tcurl -o cmake.tar.gz -fsSL https://github.com/Kitware/CMake/releases/download/v4.1.2/cmake-4.1.2-linux-$(uname -m).tar.gz && \\\n\tmkdir /cmake && \\\n\ttar -xzf cmake.tar.gz -C /cmake --strip-components 1 && \\\n\trm cmake.tar.gz && \\\n\tcurl -fsSL -o patchelf.tar.gz https://github.com/NixOS/patchelf/releases/download/0.18.0/patchelf-0.18.0-$(uname -m).tar.gz && \\\n\tmkdir -p /patchelf && \\\n\ttar -xzf patchelf.tar.gz -C /patchelf --strip-components=1 && \\\n\tcp /patchelf/bin/patchelf /usr/bin/ && \\\n\trm patchelf.tar.gz && \\\n\tif [ \"$(uname -m)\" = \"aarch64\" ]; then \\\n\t\tGO_ARCH=\"arm64\" ; \\\n\telse \\\n\t\tGO_ARCH=\"amd64\" ; \\\n\tfi; \\\n\tcurl -o /usr/local/bin/jq -fsSL https://github.com/jqlang/jq/releases/download/jq-1.7.1/jq-linux-${GO_ARCH} && \\\n\tchmod +x /usr/local/bin/jq && \\\n\tcurl -o go.tar.gz -fsSL https://go.dev/dl/$(curl -fsS https://go.dev/dl/?mode=json | jq -r \"first(first(.[] | select(.stable and (.version | startswith(\\\"go${GO_VERSION}\\\")))).files[] | select(.os == \\\"linux\\\" and (.kind == \\\"archive\\\") and (.arch == \\\"${GO_ARCH}\\\"))).filename\") && \\\n\trm -rf /usr/local/go && \\\n\ttar -C /usr/local -xzf go.tar.gz && \\\n\trm go.tar.gz && \\\n\t/usr/local/go/bin/go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest\n\nENV PATH=\"/opt/rh/devtoolset-10/root/usr/bin:/cmake/bin:/usr/local/go/bin:$PATH\"\n\n# Apply GNU mode\nENV SPC_DEFAULT_C_FLAGS='-fPIE -fPIC -O3'\nENV SPC_LIBC='glibc'\nENV SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS_PROGRAM='-Wl,-O3 -pie'\nENV SPC_CMD_VAR_PHP_MAKE_EXTRA_LIBS='-ldl -lpthread -lm -lresolv -lutil -lrt'\n# Keep default config paths and append any externally provided SPC_OPT_BUILD_ARGS (e.g., from CI)\nENV SPC_OPT_BUILD_ARGS=\"--with-config-file-path=/etc/frankenphp --with-config-file-scan-dir=/etc/frankenphp/php.d ${SPC_OPT_BUILD_ARGS}\"\nENV SPC_REL_TYPE='binary'\nENV EXTENSION_DIR='/usr/lib/frankenphp/modules'\n\n# not sure if this is needed\nENV COMPOSER_ALLOW_SUPERUSER=1\n\nWORKDIR /go/src/app\nCOPY go.mod go.sum ./\nRUN go mod download\n\nWORKDIR /go/src/app/caddy\nCOPY caddy/go.mod caddy/go.sum ./\nRUN go mod download\n\nWORKDIR /go/src/app\nCOPY --link *.* ./\nCOPY --link caddy caddy\nCOPY --link internal internal\nCOPY --link package package\n\nRUN --mount=type=secret,id=github-token GITHUB_TOKEN=$(cat /run/secrets/github-token) ./build-static.sh\n"
  },
  {
    "path": "static-builder-musl.Dockerfile",
    "content": "# syntax=docker/dockerfile:1\n#checkov:skip=CKV_DOCKER_2\n#checkov:skip=CKV_DOCKER_3\n#checkov:skip=CKV_DOCKER_7\nFROM golang-base\n\nARG TARGETARCH\n\nARG FRANKENPHP_VERSION=''\nENV FRANKENPHP_VERSION=${FRANKENPHP_VERSION}\n\nARG PHP_VERSION=''\nENV PHP_VERSION=${PHP_VERSION}\n\n# args passed to static-php-cli\nARG PHP_EXTENSIONS=''\nARG PHP_EXTENSION_LIBS=''\nARG SPC_OPT_BUILD_ARGS\n\n# args passed to xcaddy\nARG XCADDY_ARGS='--with github.com/dunglas/caddy-cbrotli --with github.com/dunglas/mercure/caddy --with github.com/dunglas/vulcain/caddy'\nENV SPC_CMD_VAR_FRANKENPHP_XCADDY_MODULES=\"${XCADDY_ARGS}\"\nARG CLEAN=''\nARG EMBED=''\nARG DEBUG_SYMBOLS=''\nARG MIMALLOC=''\nARG NO_COMPRESS=''\n\nENV GOTOOLCHAIN=local\n\nSHELL [\"/bin/ash\", \"-eo\", \"pipefail\", \"-c\"]\n\nARG CI\nENV CI=${CI}\n\nLABEL org.opencontainers.image.title=FrankenPHP\nLABEL org.opencontainers.image.description=\"The modern PHP app server\"\nLABEL org.opencontainers.image.url=https://frankenphp.dev\nLABEL org.opencontainers.image.source=https://github.com/php/frankenphp\nLABEL org.opencontainers.image.licenses=MIT\nLABEL org.opencontainers.image.vendor=\"Kévin Dunglas\"\n\nRUN apk update; \\\n\tapk add --no-cache \\\n\t\talpine-sdk \\\n\t\tautoconf \\\n\t\tautomake \\\n\t\tbash \\\n\t\tbinutils \\\n\t\tbison \\\n\t\tbuild-base \\\n\t\tcmake \\\n\t\tcurl \\\n\t\tfile \\\n\t\tflex \\\n\t\tg++ \\\n\t\tgcc \\\n\t\tgit \\\n\t\tjq \\\n\t\tlibgcc \\\n\t\tlibstdc++ \\\n\t\tlibtool \\\n\t\tlinux-headers \\\n\t\tm4 \\\n\t\tmake \\\n\t\tpkgconfig \\\n\t\tphp84 \\\n\t\tphp84-common \\\n\t\tphp84-ctype \\\n\t\tphp84-curl \\\n\t\tphp84-dom \\\n\t\tphp84-iconv \\\n\t\tphp84-mbstring \\\n\t\tphp84-openssl \\\n\t\tphp84-pcntl \\\n\t\tphp84-phar \\\n\t\tphp84-posix \\\n\t\tphp84-session \\\n\t\tphp84-sodium \\\n\t\tphp84-tokenizer \\\n\t\tphp84-xml \\\n\t\tphp84-xmlwriter \\\n\t\tupx \\\n\t\twget \\\n\t\txz ; \\\n\tln -sf /usr/bin/php84 /usr/bin/php && \\\n\tgo install github.com/caddyserver/xcaddy/cmd/xcaddy@latest\n\n# https://getcomposer.org/doc/03-cli.md#composer-allow-superuser\nENV COMPOSER_ALLOW_SUPERUSER=1\nCOPY --from=composer/composer:2-bin /composer /usr/bin/composer\n\nWORKDIR /go/src/app\nCOPY go.mod go.sum ./\nRUN go mod download\n\nWORKDIR /go/src/app/caddy\nCOPY caddy/go.mod caddy/go.sum ./\nRUN go mod download\n\nWORKDIR /go/src/app\nCOPY --link . ./\n\nENV SPC_DEFAULT_C_FLAGS='-fPIE -fPIC -O3'\nENV SPC_LIBC='musl'\nENV SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS_PROGRAM='-Wl,-O3 -pie'\n# Keep default config paths and append any externally provided SPC_OPT_BUILD_ARGS (e.g., from CI)\nENV SPC_OPT_BUILD_ARGS=\"--with-config-file-path=/etc/frankenphp --with-config-file-scan-dir=/etc/frankenphp/php.d ${SPC_OPT_BUILD_ARGS}\"\nENV SPC_REL_TYPE='binary'\nENV EXTENSION_DIR='/usr/lib/frankenphp/modules'\n\nRUN --mount=type=secret,id=github-token GITHUB_TOKEN=$(cat /run/secrets/github-token) ./build-static.sh\n"
  },
  {
    "path": "testdata/Caddyfile",
    "content": "{\n\tdebug\n\tfrankenphp {\n\t\t#worker ./index.php\n\t}\n}\n\nhttp:// {\n\tlog\n\troute {\n\t\troot .\n\t\t# Add trailing slash for directory requests\n\t\t@canonicalPath {\n\t\t\tfile {path}/index.php\n\t\t\tnot path */\n\t\t}\n\t\tredir @canonicalPath {path}/ 308\n\n\t\t# If the requested file does not exist, try index files\n\t\t@indexFiles file {\n\t\t\ttry_files {path} {path}/index.php index.php\n\t\t\tsplit_path .php\n\t\t}\n\t\trewrite @indexFiles {http.matchers.file.relative}\n\n\t\tencode zstd br gzip\n\n\t\t# FrankenPHP!\n\t\t@phpFiles path *.php\n\t\tphp @phpFiles\n\t\tfile_server\n\n\t\trespond 404\n\t}\n}\n"
  },
  {
    "path": "testdata/_executor.php",
    "content": "<?php\n\n$fn = require $_SERVER['SCRIPT_FILENAME'];\nif ('1' !== ($_SERVER['FRANKENPHP_WORKER'] ?? null)) {\n    $fn();\n    exit(0);\n}\n\nwhile (frankenphp_handle_request($fn)) {}\n\nexit(0);\n"
  },
  {
    "path": "testdata/autoloader-require.php",
    "content": "<?php\n\nfunction my_autoloader(string $class) {\n}\nspl_autoload_register('my_autoloader');\n"
  },
  {
    "path": "testdata/autoloader.php",
    "content": "<?php\n\nrequire_once __DIR__.'/_executor.php';\nrequire_once __DIR__.'/autoloader-require.php';\n\nreturn function () {\n    echo \"request {$_GET['i']}\\n\";\n    echo implode(',', spl_autoload_functions());\n};\n"
  },
  {
    "path": "testdata/benchmark.Caddyfile",
    "content": "{\n\tfrankenphp\n}\n\nhttp:// {\n\troute {\n\t\troot .\n\n\t\tencode zstd br gzip\n\n\t\tphp {\n\t\t\tfile_server off\n\t\t\tresolve_root_symlink false\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "testdata/command.php",
    "content": "<?php\n\nvar_dump($argv, $_SERVER);\necho \"From the CLI\\n\";\n\nexit(3);\n"
  },
  {
    "path": "testdata/connection_status.php",
    "content": "<?php\n\nignore_user_abort(true);\n\nrequire_once __DIR__.'/_executor.php';\n\nreturn function () {\n\tif($_GET['finish'] ?? false) {\n\t\tfrankenphp_finish_request();\n\t}\n\n\techo 'hi';\n\tflush();\n\t$status = (string) connection_status();\n\terror_log(\"request {$_GET['i']}: \" . $status);\n};\n"
  },
  {
    "path": "testdata/cookies.php",
    "content": "<?php\n\nrequire_once __DIR__.'/_executor.php';\n\nreturn function () {\n    echo var_export($_COOKIE);\n};\n"
  },
  {
    "path": "testdata/dd.php",
    "content": "<?php\n\n// simulate Symfony's dd() behavior\n// see https://github.com/symfony/http-kernel/blob/7.3/DataCollector/DumpDataCollector.php#L216\nclass Dumper\n{\n    private string $message;\n\n    public function dump(string $message): void\n    {\n        http_response_code(500);\n        $this->message = $message;\n    }\n\n    public function __destruct()\n    {\n        if (isset($this->message)) {\n            echo $this->message;\n        }\n    }\n}\n\n$dumper = new Dumper();\n\nwhile (frankenphp_handle_request(function () use ($dumper) {\n    $dumper->dump($_GET['output'] ?? '');\n    exit(1);\n})) {\n    // keep handling requests\n}\n\necho \"we should never reach here\\n\";\n"
  },
  {
    "path": "testdata/die.php",
    "content": "<?php\n\ndo {\n    $ok = frankenphp_handle_request(function (): void {\n        echo 'Hello, world';\n    });\n\n    die();\n} while ($ok);\n"
  },
  {
    "path": "testdata/dirindex/index.php",
    "content": "<?php\n\necho \"Hello from directory index.php\";\n"
  },
  {
    "path": "testdata/early-hints.php",
    "content": "<?php\n\nrequire_once __DIR__.'/_executor.php';\n\nreturn function () {\n    header('Link: </style.css>; rel=preload; as=style');\n    header(\"Request: {$_GET['i']}\");\n    headers_send(103);\n\n    header_remove('Link');\n\n    echo 'Hello';\n};\n"
  },
  {
    "path": "testdata/echo.php",
    "content": "<?php\n\nheader('Content-Type: text/plain');\n\necho file_get_contents('php://input');\n"
  },
  {
    "path": "testdata/env/env.php",
    "content": "<?php\n\nrequire_once __DIR__ . '/../_executor.php';\n\nreturn function () use (&$rememberedKey) {\n    $keys = $_GET['keys'];\n\n    // echoes ENV1=value1,ENV2=value2\n    echo join(',', array_map(fn($key) => \"$key=\" . $_ENV[$key], $keys));\n};\n"
  },
  {
    "path": "testdata/env/import-env.php",
    "content": "<?php\n\nreturn $_ENV['custom_key'];\n"
  },
  {
    "path": "testdata/env/overwrite-env.php",
    "content": "<?php\n\nrequire_once __DIR__.'/../_executor.php';\n\n// modify $_ENV in the global symbol table\n// the modification should persist through the worker's lifetime\n$_ENV['custom_key'] = 'custom_value';\n\nreturn function () use (&$rememberedIndex) {\n    $custom_key = require __DIR__.'/import-env.php';\n    echo $custom_key;\n};\n"
  },
  {
    "path": "testdata/env/putenv.php",
    "content": "<?php\n\nrequire_once __DIR__.'/../_executor.php';\n\nreturn function () use (&$rememberedKey) {\n    $key = $_GET['key'];\n    $put = $_GET['put'] ?? null;\n\n    if(isset($put)){\n        putenv(\"$key=$put\");\n    }\n\n    $get = getenv($key);\n    $asStr = $get === false ? '' : $get;\n\n    echo \"$key=$asStr\";\n};\n"
  },
  {
    "path": "testdata/env/remember-env.php",
    "content": "<?php\n\nrequire_once __DIR__.'/../_executor.php';\n\n$rememberedIndex = 0;\n\nreturn function () use (&$rememberedIndex) {\n    $indexFromRequest = $_GET['index'] ?? null;\n    if(isset($indexFromRequest)){\n        $rememberedIndex = (int)$indexFromRequest;\n        putenv(\"index=$rememberedIndex\");\n    }\n\n    $indexInEnv = (int)getenv('index');\n    if($indexInEnv === $rememberedIndex){\n        echo 'success';\n        return;\n    }\n    echo \"failure: '$indexInEnv' is not '$rememberedIndex'\";\n};\n"
  },
  {
    "path": "testdata/env/test-env.php",
    "content": "<?php\n\nrequire_once __DIR__ . '/../_executor.php';\n\nreturn function () {\n    $var = 'MY_VAR_' . ($_GET['var'] ?? '');\n    // Setting an environment variable\n    $result = putenv(\"$var=HelloWorld\");\n    echo $result ? \"Set MY_VAR successfully.\\nMY_VAR = \" . getenv($var) . \"\\n\" : \"Failed to set MY_VAR.\\n\";\n\n    // putenv should not affect $_ENV\n    $result = $_ENV[$var] ?? null;\n    echo $result === null ? \"MY_VAR not found in \\$_ENV.\\n\" : \"MY_VAR is in \\$_ENV (not expected)\\n\";\n\n    // putenv should not affect $_SERVER\n    $result = $_SERVER[$var] ?? null;\n    echo $result === null ? \"MY_VAR not found in \\$_SERVER.\\n\" : \"MY_VAR is in \\$_SERVER (not expected)\\n\";\n\n    // Unsetting the environment variable\n    $result = putenv($var);\n    if ($result) {\n        echo \"Unset MY_VAR successfully.\\n\";\n        $value = getenv($var);\n        echo $value === false ? \"MY_VAR is unset.\\n\" : \"MY_VAR = $value\\n\";\n    } else {\n        echo \"Failed to unset MY_VAR.\\n\";\n    }\n\n    $result = putenv(\"$var=\");\n    if ($result) {\n        echo \"MY_VAR set to empty successfully.\\n\";\n        $value = getenv($var);\n        echo $value === false ? \"MY_VAR is unset.\\n\" : \"MY_VAR = $value\\n\";\n    } else {\n        echo \"Failed to set MY_VAR.\\n\";\n    }\n\n    // Attempt to unset a non-existing variable\n    $result = putenv('NON_EXISTING_VAR' . ($_GET['var'] ?? ''));\n    echo $result ? \"Unset NON_EXISTING_VAR successfully.\\n\" : \"Failed to unset NON_EXISTING_VAR.\\n\";\n\n    // Inserting an invalid variable should fail (null byte in key)\n    $result = putenv(\"INVALID\\x0_VAR=value\");\n    if (getenv(\"INVALID\\x0_VAR\")) {\n        echo \"Invalid value was inserted (unexpected).\\n\";\n    } else if ($result) {\n        echo \"Invalid value was not inserted.\\n\";\n    } else {\n        echo \"Invalid value was not inserted, but regular PHP should still return 'true' here.\\n\";\n    }\n\n    getenv();\n};\n"
  },
  {
    "path": "testdata/exception.php",
    "content": "<?php\n\nrequire_once __DIR__.'/_executor.php';\n\nreturn function () {\n    echo 'hello';\n    throw new Exception(\"request {$_GET['i']}\");\n};\n"
  },
  {
    "path": "testdata/failing-worker.php",
    "content": "<?php\n\nif (rand(1, 100) <= 50) {\n    throw new Exception('this exception is expected to fail the worker');\n}\n\n// frankenphp_handle_request() has not been reached (also a failure)\n"
  },
  {
    "path": "testdata/fiber-basic.php",
    "content": "<?php\nrequire_once __DIR__.'/_executor.php';\n\nreturn function() {\n    $fiber = new Fiber(function() {\n        echo 'Fiber '.($_GET['i'] ?? '');\n    });\n    $fiber->start();\n};\n"
  },
  {
    "path": "testdata/fiber-no-cgo.php",
    "content": "<?php\nrequire_once __DIR__.'/_executor.php';\n\nreturn function() {\n    $fiber = new Fiber(function() {\n        Fiber::suspend('Fiber '.($_GET['i'] ?? ''));\n    });\n    echo $fiber->start();\n\n    $fiber->resume();\n};\n\n"
  },
  {
    "path": "testdata/file-stream.php",
    "content": "<?php\n\n$fileStream = fopen(__DIR__ . '/file-stream.txt', 'r');\n$input = fopen('php://input', 'r');\n\nwhile (frankenphp_handle_request(function () use ($fileStream, $input) {\n    echo fread($fileStream, 5);\n\n    // this line will lead to a zend_mm_heap corrupted error if the input stream was destroyed\n    stream_is_local($input);\n})) ;\n\nfclose($fileStream);\n"
  },
  {
    "path": "testdata/file-stream.txt",
    "content": "word1word2word3\n"
  },
  {
    "path": "testdata/file-upload.php",
    "content": "<?php\nrequire_once __DIR__.'/_executor.php';\n\nreturn function()\n{\n    $uploaded = ($_FILES['file']['tmp_name'] ?? null) ? file_get_contents($_FILES['file']['tmp_name']) : null;\n    if ($uploaded) {\n        echo 'Upload OK'; \n        return;\n    }\n\n    echo <<<'HTML'\n    <form method=\"POST\" enctype=\"multipart/form-data\">\n        <input type=\"file\" name=\"file\">\n        <input type=\"submit\">\n    </form>\n    HTML;\n};\n"
  },
  {
    "path": "testdata/files/.gitignore",
    "content": "test.txt\n"
  },
  {
    "path": "testdata/files/static.txt",
    "content": "Hello from file\n"
  },
  {
    "path": "testdata/finish-request.php",
    "content": "<?php\n\nrequire_once __DIR__.'/_executor.php';\n\nreturn function () {\n    echo 'This is output '.($_GET['i'] ?? '').\"\\n\";\n\n    frankenphp_finish_request();\n\n    echo 'This is not';\n};\n"
  },
  {
    "path": "testdata/flush.php",
    "content": "<?php\n\nrequire_once __DIR__.'/_executor.php';\n\nreturn function () {\n    if (ini_get(\"output_buffering\") !== \"0\") {\n        // Disable output buffering if not already done\n        while (@ob_end_flush());\n    }\n\n    echo 'He';\n\n    flush();\n\n    echo 'llo '.($_GET['i'] ?? '');\n};\n"
  },
  {
    "path": "testdata/headers.php",
    "content": "<?php\n\nrequire_once __DIR__.'/_executor.php';\n\nreturn function () {\n    header('Foo: bar');\n    header('Foo2: bar2');\n    header('Foo3:bar3'); // no space after colon (also valid, not recommended)\n    header('Invalid');\n    header('I: ' . ($_GET['i'] ?? 'i not set'));\n    http_response_code(201);\n    \n    echo 'Hello';\n};\n"
  },
  {
    "path": "testdata/hello.php",
    "content": "<?php\n\necho \"Hello from PHP\";\n"
  },
  {
    "path": "testdata/hello.txt",
    "content": "Hello\n"
  },
  {
    "path": "testdata/index.php",
    "content": "<?php\n\nrequire_once __DIR__.'/_executor.php';\n\nreturn function () {\n    echo sprintf(\"I am by birth a Genevese (%s)\", $_GET['i'] ?? 'i not set');\n};\n"
  },
  {
    "path": "testdata/ini.php",
    "content": "<?php\n\nrequire_once __DIR__.'/_executor.php';\n\nreturn function () {\n    echo $_GET['key'] . ':' . ini_get($_GET['key']);\n};\n"
  },
  {
    "path": "testdata/input.php",
    "content": "<?php\n\nrequire_once __DIR__.'/_executor.php';\n\nreturn function () {\n    header('Foo: bar');\n\n    echo file_get_contents('php://input');\n};\n"
  },
  {
    "path": "testdata/integration/basic_function.go",
    "content": "package testintegration\n\n// #include <Zend/zend_types.h>\nimport \"C\"\nimport (\n\t\"strings\"\n\t\"unsafe\"\n\n\t\"github.com/dunglas/frankenphp\"\n)\n\n// export_php:function test_uppercase(string $str): string\nfunc test_uppercase(s *C.zend_string) unsafe.Pointer {\n\tstr := frankenphp.GoString(unsafe.Pointer(s))\n\tupper := strings.ToUpper(str)\n\treturn frankenphp.PHPString(upper, false)\n}\n\n// export_php:function test_add_numbers(int $a, int $b): int\nfunc test_add_numbers(a int64, b int64) int64 {\n\treturn a + b\n}\n\n// export_php:function test_multiply(float $a, float $b): float\nfunc test_multiply(a float64, b float64) float64 {\n\treturn a * b\n}\n\n// export_php:function test_is_enabled(bool $flag): bool\nfunc test_is_enabled(flag bool) bool {\n\treturn !flag\n}\n"
  },
  {
    "path": "testdata/integration/callable.go",
    "content": "package testintegration\n\n// #include <Zend/zend_types.h>\nimport \"C\"\nimport (\n\t\"unsafe\"\n\n\t\"github.com/dunglas/frankenphp\"\n)\n\n// export_php:function my_array_map(array $data, callable $callback): array\nfunc my_array_map(arr *C.zend_array, callback *C.zval) unsafe.Pointer {\n\tgoArray, err := frankenphp.GoPackedArray[any](unsafe.Pointer(arr))\n\tif err != nil {\n\t\treturn nil\n\t}\n\n\tresult := make([]any, len(goArray))\n\tfor i, item := range goArray {\n\t\tcallResult := frankenphp.CallPHPCallable(unsafe.Pointer(callback), []any{item})\n\t\tresult[i] = callResult\n\t}\n\n\treturn frankenphp.PHPPackedArray[any](result)\n}\n\n// export_php:function my_filter(array $data, ?callable $callback): array\nfunc my_filter(arr *C.zend_array, callback *C.zval) unsafe.Pointer {\n\tgoArray, err := frankenphp.GoPackedArray[any](unsafe.Pointer(arr))\n\tif err != nil {\n\t\treturn nil\n\t}\n\n\tif callback == nil {\n\t\treturn unsafe.Pointer(arr)\n\t}\n\n\tresult := make([]any, 0)\n\tfor _, item := range goArray {\n\t\tcallResult := frankenphp.CallPHPCallable(unsafe.Pointer(callback), []any{item})\n\t\tif boolResult, ok := callResult.(bool); ok && boolResult {\n\t\t\tresult = append(result, item)\n\t\t}\n\t}\n\n\treturn frankenphp.PHPPackedArray[any](result)\n}\n\n// export_php:class Processor\ntype Processor struct{}\n\n// export_php:method Processor::transform(string $input, callable $transformer): string\nfunc (p *Processor) Transform(input *C.zend_string, callback *C.zval) unsafe.Pointer {\n\tgoInput := frankenphp.GoString(unsafe.Pointer(input))\n\n\tcallResult := frankenphp.CallPHPCallable(unsafe.Pointer(callback), []any{goInput})\n\n\tresultStr, ok := callResult.(string)\n\tif !ok {\n\t\treturn unsafe.Pointer(input)\n\t}\n\n\treturn frankenphp.PHPString(resultStr, false)\n}\n"
  },
  {
    "path": "testdata/integration/class_methods.go",
    "content": "package testintegration\n\n// #include <Zend/zend_types.h>\nimport \"C\"\nimport (\n\t\"unsafe\"\n\n\t\"github.com/dunglas/frankenphp\"\n)\n\n// export_php:class Counter\ntype CounterStruct struct {\n\tValue int\n}\n\n// export_php:method Counter::increment(): void\nfunc (c *CounterStruct) Increment() {\n\tc.Value++\n}\n\n// export_php:method Counter::decrement(): void\nfunc (c *CounterStruct) Decrement() {\n\tc.Value--\n}\n\n// export_php:method Counter::getValue(): int\nfunc (c *CounterStruct) GetValue() int64 {\n\treturn int64(c.Value)\n}\n\n// export_php:method Counter::setValue(int $value): void\nfunc (c *CounterStruct) SetValue(value int64) {\n\tc.Value = int(value)\n}\n\n// export_php:method Counter::reset(): void\nfunc (c *CounterStruct) Reset() {\n\tc.Value = 0\n}\n\n// export_php:method Counter::addValue(int $amount): int\nfunc (c *CounterStruct) AddValue(amount int64) int64 {\n\tc.Value += int(amount)\n\treturn int64(c.Value)\n}\n\n// export_php:method Counter::updateWithNullable(?int $newValue): void\nfunc (c *CounterStruct) UpdateWithNullable(newValue *int64) {\n\tif newValue != nil {\n\t\tc.Value = int(*newValue)\n\t}\n}\n\n// export_php:class StringHolder\ntype StringHolderStruct struct {\n\tData string\n}\n\n// export_php:method StringHolder::setData(string $data): void\nfunc (sh *StringHolderStruct) SetData(data *C.zend_string) {\n\tsh.Data = frankenphp.GoString(unsafe.Pointer(data))\n}\n\n// export_php:method StringHolder::getData(): string\nfunc (sh *StringHolderStruct) GetData() unsafe.Pointer {\n\treturn frankenphp.PHPString(sh.Data, false)\n}\n\n// export_php:method StringHolder::getLength(): int\nfunc (sh *StringHolderStruct) GetLength() int64 {\n\treturn int64(len(sh.Data))\n}\n"
  },
  {
    "path": "testdata/integration/constants.go",
    "content": "package testintegration\n\n// #include <Zend/zend_types.h>\nimport \"C\"\nimport (\n\t\"unsafe\"\n\n\t\"github.com/dunglas/frankenphp\"\n)\n\n// export_php:const\nconst TEST_MAX_RETRIES = 100\n\n// export_php:const\nconst TEST_API_VERSION = \"2.0.0\"\n\n// export_php:const\nconst TEST_ENABLED = true\n\n// export_php:const\nconst TEST_PI = 3.14159\n\n// export_php:const\nconst (\n\tSTATUS_PENDING = iota\n\tSTATUS_PROCESSING\n\tSTATUS_COMPLETED\n)\n\nconst (\n\t// export_php:const\n\tONE = 1\n\t// export_php:const\n\tTWO = 2\n)\n\n// export_php:class Config\ntype ConfigStruct struct {\n\tMode int\n}\n\n// export_php:classconst Config\nconst MODE_DEBUG = 1\n\n// export_php:classconst Config\nconst MODE_PRODUCTION = 2\n\n// export_php:classconst Config\nconst DEFAULT_TIMEOUT = 30\n\n// export_php:method Config::setMode(int $mode): void\nfunc (c *ConfigStruct) SetMode(mode int64) {\n\tc.Mode = int(mode)\n}\n\n// export_php:method Config::getMode(): int\nfunc (c *ConfigStruct) GetMode() int64 {\n\treturn int64(c.Mode)\n}\n\n// export_php:function test_with_constants(int $status): string\nfunc test_with_constants(status int64) unsafe.Pointer {\n\tvar result string\n\tswitch status {\n\tcase STATUS_PENDING:\n\t\tresult = \"pending\"\n\tcase STATUS_PROCESSING:\n\t\tresult = \"processing\"\n\tcase STATUS_COMPLETED:\n\t\tresult = \"completed\"\n\tdefault:\n\t\tresult = \"unknown\"\n\t}\n\treturn frankenphp.PHPString(result, false)\n}\n"
  },
  {
    "path": "testdata/integration/invalid_signature.go",
    "content": "package testintegration\n\n// #include <Zend/zend_types.h>\nimport \"C\"\n\n// export_php:function invalid_return_type(string $str): unsupported_type\nfunc invalid_return_type(s *C.zend_string) int {\n\treturn 42\n}\n"
  },
  {
    "path": "testdata/integration/namespace.go",
    "content": "package testintegration\n\n// export_php:namespace TestIntegration\\Extension\n\n// #include <Zend/zend_types.h>\nimport \"C\"\nimport (\n\t\"unsafe\"\n\n\t\"github.com/dunglas/frankenphp\"\n)\n\n// export_php:const\nconst NAMESPACE_VERSION = \"1.0.0\"\n\n// export_php:function greet(string $name): string\nfunc greet(name *C.zend_string) unsafe.Pointer {\n\tstr := frankenphp.GoString(unsafe.Pointer(name))\n\tresult := \"Hello, \" + str + \"!\"\n\treturn frankenphp.PHPString(result, false)\n}\n\n// export_php:class Person\ntype PersonStruct struct {\n\tName string\n\tAge  int\n}\n\n// export_php:method Person::setName(string $name): void\nfunc (p *PersonStruct) SetName(name *C.zend_string) {\n\tp.Name = frankenphp.GoString(unsafe.Pointer(name))\n}\n\n// export_php:method Person::getName(): string\nfunc (p *PersonStruct) GetName() unsafe.Pointer {\n\treturn frankenphp.PHPString(p.Name, false)\n}\n\n// export_php:method Person::setAge(int $age): void\nfunc (p *PersonStruct) SetAge(age int64) {\n\tp.Age = int(age)\n}\n\n// export_php:method Person::getAge(): int\nfunc (p *PersonStruct) GetAge() int64 {\n\treturn int64(p.Age)\n}\n\n// export_php:classconst Person\nconst DEFAULT_AGE = 18\n"
  },
  {
    "path": "testdata/integration/type_mismatch.go",
    "content": "package testintegration\n\n// #include <Zend/zend_types.h>\nimport \"C\"\n\n// export_php:function mismatched_param_type(int $value): int\nfunc mismatched_param_type(value string) int64 {\n\treturn 0\n}\n\n// export_php:class BadClass\ntype BadClassStruct struct {\n\tValue int\n}\n\n// export_php:method BadClass::wrongReturnType(): string\nfunc (bc *BadClassStruct) WrongReturnType() int {\n\treturn bc.Value\n}\n"
  },
  {
    "path": "testdata/large-request.php",
    "content": "<?php\n\nrequire_once __DIR__.'/_executor.php';\n\nreturn function () {\n    printf(\n        'Request body size: %d (%s)',\n        strlen(file_get_contents('php://input')),\n        $_GET['i'] ?? 'unknown',\n    );\n};\n"
  },
  {
    "path": "testdata/large-response.php",
    "content": "<?php\n\nrequire_once __DIR__.'/_executor.php';\n\nreturn function () {\n    echo str_repeat(\"Hey\\n\", 1024);\n};\n"
  },
  {
    "path": "testdata/load-test.js",
    "content": "import http from \"k6/http\";\nimport { check } from \"k6\";\n\nexport const options = {\n  // A number specifying the number of VUs to run concurrently.\n  vus: 100,\n  // A string specifying the total duration of the test run.\n  duration: \"30s\",\n\n  // The following section contains configuration options for execution of this\n  // test script in Grafana Cloud.\n  //\n  // See https://grafana.com/docs/grafana-cloud/k6/get-started/run-cloud-tests-from-the-cli/\n  // to learn about authoring and running k6 test scripts in Grafana k6 Cloud.\n  //\n  // ext: {\n  //   loadimpact: {\n  //     // The ID of the project to which the test is assigned in the k6 Cloud UI.\n  //     // By default tests are executed in default project.\n  //     projectID: \"\",\n  //     // The name of the test in the k6 Cloud UI.\n  //     // Test runs with the same name will be grouped.\n  //     name: \"script.js\"\n  //   }\n  // },\n\n  // Uncomment this section to enable the use of Browser API in your tests.\n  //\n  // See https://grafana.com/docs/k6/latest/using-k6-browser/running-browser-tests/ to learn more\n  // about using Browser API in your test scripts.\n  //\n  // scenarios: {\n  //   // The scenario name appears in the result summary, tags, and so on.\n  //   // You can give the scenario any name, as long as each name in the script is unique.\n  //   ui: {\n  //     // Executor is a mandatory parameter for browser-based tests.\n  //     // Shared iterations in this case tells k6 to reuse VUs to execute iterations.\n  //     //\n  //     // See https://grafana.com/docs/k6/latest/using-k6/scenarios/executors/ for other executor types.\n  //     executor: 'shared-iterations',\n  //     options: {\n  //       browser: {\n  //         // This is a mandatory parameter that instructs k6 to launch and\n  //         // connect to a Chromium-based browser, and use it to run UI-based\n  //         // tests.\n  //         type: 'chromium',\n  //       },\n  //     },\n  //   },\n  // }\n};\n\nconst payload = \"foo\\n\".repeat(1000);\n\n// The function that defines VU logic.\n//\n// See https://grafana.com/docs/k6/latest/examples/get-started-with-k6/ to learn more\n// about authoring k6 scripts.\n//\nexport default function () {\n  const params = {\n    headers: {\n      Accept:\n        \"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8\",\n      // 'Accept-Encoding': 'br',\n      \"Accept-Language\": \"fr,fr-FR;q=0.8,en-US;q=0.5,en;q=0.3\",\n      \"Cache-Control\": \"no-cache\",\n      Connection: \"keep-alive\",\n      Cookie:\n        \"user_session=myrandomuuid; __Host-user_session_same_site=myotherrandomuuid; dotcom_user=dunglas; logged_in=yes; _foo=barbarbarbarbarbar; _device_id=anotherrandomuuid; color_mode=foobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobar; preferred_color_mode=light; tz=Europe%2FParis; has_recent_activity=1\",\n      DNT: \"1\",\n      Host: \"example.com\",\n      Pragma: \"no-cache\",\n      \"Sec-Fetch-Dest\": \"document\",\n      \"Sec-Fetch-Mode\": \"navigate\",\n      \"Sec-Fetch-Site\": \"cross-site\",\n      \"Sec-GPC\": \"1\",\n      \"Upgrade-Insecure-Requests\": \"1\",\n      \"User-Agent\":\n        \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:122.0) Gecko/20100101 Firefox/122.0\",\n    },\n  };\n\n  const res = http.post(\"http://localhost/echo.php\", payload, params);\n  check(res, {\n    \"is status 200\": (r) => r.status === 200,\n    \"is echoed\": (r) => r.body === payload,\n  });\n}\n"
  },
  {
    "path": "testdata/log-error_log.php",
    "content": "<?php\n\nrequire_once __DIR__.'/_executor.php';\n\nreturn function () {\n    error_log(\"request {$_GET['i']}\");\n};\n"
  },
  {
    "path": "testdata/log-frankenphp_log.php",
    "content": "<?php\n\nrequire_once __DIR__.'/_executor.php';\n\nfrankenphp_log(\"default level message\");\n\nreturn function () {\n\tfrankenphp_log(\"some debug message {$_GET['i']}\", FRANKENPHP_LOG_LEVEL_DEBUG, [\n\t\t\"key int\"    => 1,\n\t]);\n\n\tfrankenphp_log(\"some info message {$_GET['i']}\", FRANKENPHP_LOG_LEVEL_INFO, [\n\t\t\"key string\" => \"string\",\n\t]);\n\n\tfrankenphp_log(\"some warn message {$_GET['i']}\", FRANKENPHP_LOG_LEVEL_WARN);\n\n\tfrankenphp_log(\"some error message {$_GET['i']}\", FRANKENPHP_LOG_LEVEL_ERROR, [\n\t\t\"err\" => [\"a\", \"v\"],\n\t]);\n};\n"
  },
  {
    "path": "testdata/mercure-publish.php",
    "content": "<?php\n\nrequire_once __DIR__.'/_executor.php';\n\nreturn function () {\n\techo \"update 1: \" . mercure_publish('foo', 'bar', true, 'myid', 'mytype', 10) . \"\\n\";\n\techo \"update 2: \" . mercure_publish(['baz', 'bar']) . \"\\n\";\n};\n"
  },
  {
    "path": "testdata/message-worker.php",
    "content": "<?php\n\nwhile (frankenphp_handle_request(function ($message) {\n    echo $message;\n    return \"received message: $message\";\n})) {\n    // continue handling requests\n}\n"
  },
  {
    "path": "testdata/non-worker.php",
    "content": "<?php\n\nfrankenphp_handle_request(function () {});\n"
  },
  {
    "path": "testdata/only-headers.php",
    "content": "<?php\n\nrequire_once __DIR__.'/_executor.php';\n\nreturn function () {\n    header('Content-Type: application/json');\n    header('HTTP/1.1 204 No Content', true, 204);\n\n    echo '{\"status\": \"test\"}';\n    flush();\n};\n"
  },
  {
    "path": "testdata/performance/api.js",
    "content": "import http from \"k6/http\";\n\n/**\n * Many applications communicate with external APIs or microservices.\n * Latencies tend to be much higher than with databases in these cases.\n * We'll consider 10ms-150ms\n */\nexport const options = {\n  stages: [\n    { duration: \"20s\", target: 150 },\n    { duration: \"20s\", target: 1000 },\n    { duration: \"10s\", target: 0 },\n  ],\n  thresholds: {\n    http_req_failed: [\"rate<0.01\"],\n  },\n};\n\n/* global __ENV */\nexport default function () {\n  // 10-150ms latency\n  const latency = Math.floor(Math.random() * 141) + 10;\n  // 1-30000 work units\n  const work = Math.ceil(Math.random() * 30000);\n  // 1-40 output units\n  const output = Math.ceil(Math.random() * 40);\n\n  http.get(\n    http.url`${__ENV.CADDY_HOSTNAME}/sleep.php?sleep=${latency}&work=${work}&output=${output}`,\n  );\n}\n"
  },
  {
    "path": "testdata/performance/computation.js",
    "content": "import http from \"k6/http\";\n\n/**\n * Simulate an application that does very little IO, but a lot of computation\n */\nexport const options = {\n  stages: [\n    { duration: \"20s\", target: 80 },\n    { duration: \"20s\", target: 150 },\n    { duration: \"5s\", target: 0 },\n  ],\n  thresholds: {\n    http_req_failed: [\"rate<0.01\"],\n  },\n};\n\n/* global __ENV */\nexport default function () {\n  // do 1-1,000,000 work units\n  const work = Math.ceil(Math.random() * 1_000_000);\n  // output 1-500 units\n  const output = Math.ceil(Math.random() * 500);\n  // simulate 0-2ms latency\n  const latency = Math.floor(Math.random() * 3);\n\n  http.get(\n    http.url`${__ENV.CADDY_HOSTNAME}/sleep.php?sleep=${latency}&work=${work}&output=${output}`,\n  );\n}\n"
  },
  {
    "path": "testdata/performance/database.js",
    "content": "import http from \"k6/http\";\n\n/**\n * Modern databases tend to have latencies in the single-digit milliseconds.\n * We'll simulate 1-10ms latencies and 1-2 queries per request.\n */\nexport const options = {\n  stages: [\n    { duration: \"20s\", target: 100 },\n    { duration: \"30s\", target: 200 },\n    { duration: \"10s\", target: 0 },\n  ],\n  thresholds: {\n    http_req_failed: [\"rate<0.01\"],\n  },\n};\n\n/* global __ENV */\nexport default function () {\n  // 1-10ms latency\n  const latency = Math.floor(Math.random() * 10) + 1;\n  // 1-2 iterations per request\n  const iterations = Math.floor(Math.random() * 2) + 1;\n  // 1-30000 work units per iteration\n  const work = Math.ceil(Math.random() * 30000);\n  // 1-40 output units\n  const output = Math.ceil(Math.random() * 40);\n\n  http.get(\n    http.url`${__ENV.CADDY_HOSTNAME}/sleep.php?sleep=${latency}&work=${work}&output=${output}&iterations=${iterations}`,\n  );\n}\n"
  },
  {
    "path": "testdata/performance/flamegraph.sh",
    "content": "#!/bin/bash\n\n# install brendangregg's FlameGraph\nif [ ! -d \"/usr/local/src/flamegraph\" ]; then\n\tmkdir /usr/local/src/flamegraph &&\n\t\tcd /usr/local/src/flamegraph &&\n\t\tgit clone https://github.com/brendangregg/FlameGraph.git\nfi\n\n# let the test warm up\nsleep 10\n\n# run a 30 second profile on the Caddy admin port\ncd /usr/local/src/flamegraph/FlameGraph &&\n\tgo tool pprof -raw -output=cpu.txt 'http://localhost:2019/debug/pprof/profile?seconds=30' &&\n\t./stackcollapse-go.pl cpu.txt | ./flamegraph.pl >/go/src/app/testdata/performance/flamegraph.svg\n"
  },
  {
    "path": "testdata/performance/hanging-requests.js",
    "content": "import http from \"k6/http\";\n\n/**\n * It is not uncommon for external services to hang for a long time.\n * Make sure the server is resilient in such cases and doesn't hang as well.\n */\nexport const options = {\n  stages: [\n    { duration: \"20s\", target: 100 },\n    { duration: \"20s\", target: 500 },\n    { duration: \"20s\", target: 0 },\n  ],\n  thresholds: {\n    http_req_failed: [\"rate<0.01\"],\n  },\n};\n\n/* global __ENV */\nexport default function () {\n  // 2% chance for a request that hangs for 15s\n  if (Math.random() < 0.02) {\n    http.get(\n      `${__ENV.CADDY_HOSTNAME}/sleep.php?sleep=15000&work=10000&output=100`,\n    );\n    return;\n  }\n\n  // a regular request\n  http.get(`${__ENV.CADDY_HOSTNAME}/sleep.php?sleep=5&work=10000&output=100`);\n}\n"
  },
  {
    "path": "testdata/performance/hello-world.js",
    "content": "import http from \"k6/http\";\n\n/**\n * 'Hello world' tests the raw server performance.\n */\nexport const options = {\n  stages: [\n    { duration: \"5s\", target: 100 },\n    { duration: \"20s\", target: 400 },\n    { duration: \"5s\", target: 0 },\n  ],\n  thresholds: {\n    http_req_failed: [\"rate<0.01\"],\n  },\n};\n\n/* global __ENV */\nexport default function () {\n  http.get(`${__ENV.CADDY_HOSTNAME}/sleep.php`);\n}\n"
  },
  {
    "path": "testdata/performance/k6.Caddyfile",
    "content": "{\n\tfrankenphp {\n\t\tmax_threads {$MAX_THREADS}\n\t\tnum_threads {$NUM_THREADS}\n\t\tworker {\n\t\t\tfile /go/src/app/testdata/{$WORKER_FILE:sleep.php}\n\t\t\tnum {$WORKER_THREADS}\n\t\t}\n\t}\n}\n\n:80 {\n\troute {\n\t\troot /go/src/app/testdata\n\t\tphp {\n\t\t\troot /go/src/app/testdata\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "testdata/performance/perf-test.sh",
    "content": "#!/bin/bash\n\n# install the dev.Dockerfile, build the app and run k6 tests\n\ndocker build -t frankenphp-dev -f dev.Dockerfile .\n\nexport \"CADDY_HOSTNAME=http://host.docker.internal\"\n\nselect filename in ./testdata/performance/*.js; do\n\tread -r -p \"How many worker threads? \" workerThreads\n\tread -r -p \"How many max threads? \" maxThreads\n\n\tnumThreads=$((workerThreads + 1))\n\n\tdocker run --cap-add=SYS_PTRACE --security-opt seccomp=unconfined \\\n\t\t-p 8125:80 \\\n\t\t-v \"$PWD:/go/src/app\" \\\n\t\t--name load-test-container \\\n\t\t-e \"MAX_THREADS=$maxThreads\" \\\n\t\t-e \"WORKER_THREADS=$workerThreads\" \\\n\t\t-e \"NUM_THREADS=$numThreads\" \\\n\t\t-itd \\\n\t\tfrankenphp-dev \\\n\t\tsh /go/src/app/testdata/performance/start-server.sh\n\n\tdocker exec -d load-test-container sh /go/src/app/testdata/performance/flamegraph.sh\n\n\tsleep 10\n\n\tdocker run --entrypoint \"\" -it --rm -v .:/app -w /app \\\n\t\t--add-host \"host.docker.internal:host-gateway\" \\\n\t\tgrafana/k6:latest \\\n\t\tk6 run -e \"CADDY_HOSTNAME=$CADDY_HOSTNAME:8125\" \"./$filename\"\n\n\tdocker exec load-test-container curl \"http://localhost:2019/frankenphp/threads\"\n\n\tdocker stop load-test-container\n\tdocker rm load-test-container\ndone\n"
  },
  {
    "path": "testdata/performance/performance-testing.md",
    "content": "# Running Load tests\n\nTo run load tests with k6 you need to have Docker and Bash installed.\nGo the root of this repository and run:\n\n```sh\nbash testdata/performance/perf-test.sh\n```\n\nThis will build the `frankenphp-dev` Docker image and run it under the name 'load-test-container'\nin the background. Additionally, it will run the `grafana/k6` container, and you'll be able to choose\nthe load test you want to run. A `flamegraph.svg` will be created in the `testdata/performance` directory.\n\nIf the load test has stopped prematurely, you might have to remove the container manually:\n\n```sh\ndocker stop load-test-container\ndocker rm load-test-container\n```\n"
  },
  {
    "path": "testdata/performance/start-server.sh",
    "content": "#!/bin/bash\n\n# build and run FrankenPHP with the k6.Caddyfile\ncd /go/src/app/caddy/frankenphp &&\n\tgo build --buildvcs=false &&\n\tcd ../../testdata/performance &&\n\t/go/src/app/caddy/frankenphp/frankenphp run -c k6.Caddyfile\n"
  },
  {
    "path": "testdata/performance/timeouts.js",
    "content": "import http from \"k6/http\";\n\n/**\n * Databases or external resources can sometimes become unavailable for short periods of time.\n * Make sure the server can recover quickly from periods of unavailability.\n * This simulation swaps between a hanging and a working server every 10 seconds.\n */\nexport const options = {\n  stages: [\n    { duration: \"20s\", target: 100 },\n    { duration: \"20s\", target: 500 },\n    { duration: \"20s\", target: 0 },\n  ],\n  thresholds: {\n    http_req_failed: [\"rate<0.01\"],\n  },\n};\n\n/* global __ENV */\nexport default function () {\n  const tenSecondInterval = Math.floor(new Date().getSeconds() / 10);\n  const shouldHang = tenSecondInterval % 2 === 0;\n\n  // every 10 seconds requests lead to a max_execution-timeout\n  if (shouldHang) {\n    http.get(`${__ENV.CADDY_HOSTNAME}/sleep.php?sleep=50000`);\n    return;\n  }\n\n  // every other 10 seconds the resource is back\n  http.get(`${__ENV.CADDY_HOSTNAME}/sleep.php?sleep=5&work=30000&output=100`);\n}\n"
  },
  {
    "path": "testdata/persistent-object-require.php",
    "content": "<?php\n\nclass MyObject\n{\n    public function __construct(public string $id) {}\n}\n"
  },
  {
    "path": "testdata/persistent-object.php",
    "content": "<?php\n\nrequire_once __DIR__.'/_executor.php';\nrequire_once __DIR__.'/persistent-object-require.php';\n\n$foo = new MyObject('obj1');\n\nreturn function () use ($foo) {\n    echo 'request: ' . $_GET['i'] . \"\\n\";\n    echo 'class exists: ' . class_exists(MyObject::class) . \"\\n\";\n    echo 'id: ' . $foo->id . \"\\n\";\n    echo 'object id: '. spl_object_id($foo);\n};\n"
  },
  {
    "path": "testdata/phpinfo.php",
    "content": "<?php\n\nrequire_once __DIR__.'/_executor.php';\n\nreturn function () {\n    phpinfo();\n};\n"
  },
  {
    "path": "testdata/preload-check.php",
    "content": "<?php\n\nrequire_once __DIR__.'/_executor.php';\n\nreturn function () {\n    if (function_exists('preloaded_function')) {\n        echo preloaded_function();\n    } else {\n        echo 'not preloaded';\n    }    \n};\n\n"
  },
  {
    "path": "testdata/preload.php",
    "content": "<?php\n\n// verify ENV can be accessed during preload\n$_ENV['TEST'] = '123';\nfunction preloaded_function(): string\n{\n    return 'I am preloaded';\n}\n"
  },
  {
    "path": "testdata/request-headers.php",
    "content": "<?php\n\napache_request_headers();\n\nrequire_once __DIR__.'/_executor.php';\n\nreturn function() {\n    print_r(apache_request_headers());\n};\n"
  },
  {
    "path": "testdata/request-superglobal-conditional-include.php",
    "content": "<?php\n// This file tests if $_REQUEST is properly re-initialized in worker mode\n// The key test is: does $_REQUEST contain ONLY the current request's data?\n\n// Static counter to track how many times this file is executed (not compiled)\nstatic $execCount = 0;\n$execCount++;\n\necho \"EXEC_COUNT:\" . $execCount;\necho \"\\nREQUEST:\";\nvar_export($_REQUEST);\necho \"\\nREQUEST_COUNT:\" . count($_REQUEST);\n\n// Check if $_REQUEST was properly initialized for this request\n// If stale, it might have data from a previous request\nif (isset($_GET['val'])) {\n    $expected_val = $_GET['val'];\n    $actual_val = $_REQUEST['val'] ?? 'MISSING';\n    echo \"\\nVAL_CHECK:\" . ($expected_val === $actual_val ? \"MATCH\" : \"MISMATCH(expected=$expected_val,actual=$actual_val)\");\n}\n"
  },
  {
    "path": "testdata/request-superglobal-conditional.php",
    "content": "<?php\n\nrequire_once __DIR__.'/_executor.php';\n\nreturn function () {\n    // Only access $_REQUEST on requests where use_request=1 is passed\n    // This tests the \"re-arm\" scenario where $_REQUEST might be accessed\n    // for the first time during a later request\n    if (isset($_GET['use_request']) && $_GET['use_request'] === '1') {\n        include 'request-superglobal-conditional-include.php';\n    } else {\n        echo \"SKIPPED\";\n    }\n    echo \"\\nGET:\";\n    var_export($_GET);\n};\n"
  },
  {
    "path": "testdata/request-superglobal.php",
    "content": "<?php\n\nrequire_once __DIR__.'/_executor.php';\n\nreturn function () {\n    // Output $_REQUEST to verify it contains current request data\n    // $_REQUEST should be a merge of $_GET and $_POST (based on request_order)\n    echo \"REQUEST:\";\n    var_export($_REQUEST);\n    echo \"\\nGET:\";\n    var_export($_GET);\n    echo \"\\nPOST:\";\n    var_export($_POST);\n};\n"
  },
  {
    "path": "testdata/response-headers.php",
    "content": "<?php\n\nrequire_once __DIR__.'/_executor.php';\n\nreturn function () {\n    header('Foo: bar');\n    header('Foo2: bar2');\n    header('Invalid');\n    header('I: ' . ($_GET['i'] ?? 'i not set'));\n    if ($_GET['i'] % 3) {\n        http_response_code($_GET['i'] + 100);\n    }\n\n    var_export(apache_response_headers());\n};\n"
  },
  {
    "path": "testdata/server-all-vars-ordered.php",
    "content": "<?php\n\necho \"<pre>\\n\";\nforeach ([\n    'CONTENT_LENGTH',\n    'HTTP_CONTENT_LENGTH',\n    'CONTENT_TYPE',\n    'HTTP_CONTENT_TYPE',\n    'HTTP_SPECIAL_CHARS',\n    'DOCUMENT_ROOT',\n    'DOCUMENT_URI',\n    'GATEWAY_INTERFACE',\n    'HTTP_HOST',\n    'HTTPS',\n    'PATH_INFO',\n    'DOCUMENT_ROOT',\n    'REMOTE_ADDR',\n    'PHP_SELF',\n    'REMOTE_HOST',\n    'REQUEST_SCHEME',\n    'SCRIPT_FILENAME',\n    'SCRIPT_NAME',\n    'SERVER_NAME',\n    'SERVER_PORT',\n    'SERVER_PROTOCOL',\n    'SERVER_SOFTWARE',\n    'SSL_PROTOCOL',\n    'AUTH_TYPE',\n    'REMOTE_IDENT',\n    'PATH_TRANSLATED',\n    'QUERY_STRING',\n    'REMOTE_USER',\n    'REQUEST_METHOD',\n    'REQUEST_URI',\n    'HTTP_X_EMPTY_HEADER',\n] as $name) {\n    echo \"$name:\" . $_SERVER[$name] . \"\\n\";\n}\necho \"</pre>\\n\";\n"
  },
  {
    "path": "testdata/server-all-vars-ordered.txt",
    "content": "<pre>\nCONTENT_LENGTH:7\nHTTP_CONTENT_LENGTH:7\nCONTENT_TYPE:application/x-www-form-urlencoded\nHTTP_CONTENT_TYPE:application/x-www-form-urlencoded\nHTTP_SPECIAL_CHARS:<%00>\nDOCUMENT_ROOT:{documentRoot}\nDOCUMENT_URI:/server-all-vars-ordered.php\nGATEWAY_INTERFACE:CGI/1.1\nHTTP_HOST:localhost:{testPort}\nHTTPS:\nPATH_INFO:/path\nDOCUMENT_ROOT:{documentRoot}\nREMOTE_ADDR:127.0.0.1\nPHP_SELF:/server-all-vars-ordered.php/path\nREMOTE_HOST:127.0.0.1\nREQUEST_SCHEME:http\nSCRIPT_FILENAME:{documentRoot}/server-all-vars-ordered.php\nSCRIPT_NAME:/server-all-vars-ordered.php\nSERVER_NAME:localhost\nSERVER_PORT:{testPort}\nSERVER_PROTOCOL:HTTP/1.1\nSERVER_SOFTWARE:FrankenPHP\nSSL_PROTOCOL:\nAUTH_TYPE:\nREMOTE_IDENT:\nPATH_TRANSLATED:{documentRoot}/path\nQUERY_STRING:specialChars=%3E\\x00%00</>\nREMOTE_USER:user\nREQUEST_METHOD:POST\nREQUEST_URI:/original-path?specialChars=%3E\\x00%00</>\nHTTP_X_EMPTY_HEADER:\n</pre>\n"
  },
  {
    "path": "testdata/server-variable.php",
    "content": "<?php\n\nrequire_once __DIR__.'/_executor.php';\n\nreturn function () {\n    echo print_r($_SERVER);\n};\n"
  },
  {
    "path": "testdata/session-handler.php",
    "content": "<?php\n\nrequire_once __DIR__.'/_executor.php';\n\n// Custom session handler class\nclass TestSessionHandler implements SessionHandlerInterface\n{\n    private static array $data = [];\n\n    public function open(string $path, string $name): bool\n    {\n        return true;\n    }\n\n    public function close(): bool\n    {\n        return true;\n    }\n\n    public function read(string $id): string|false\n    {\n        return self::$data[$id] ?? '';\n    }\n\n    public function write(string $id, string $data): bool\n    {\n        self::$data[$id] = $data;\n        return true;\n    }\n\n    public function destroy(string $id): bool\n    {\n        unset(self::$data[$id]);\n        return true;\n    }\n\n    public function gc(int $max_lifetime): int|false\n    {\n        return 0;\n    }\n}\n\nreturn function () {\n    $action = $_GET['action'] ?? 'default';\n\n    // Collect output, don't send until end\n    $output = [];\n\n    switch ($action) {\n        case 'set_handler_and_start':\n            // Set custom handler and start session\n            $handler = new TestSessionHandler();\n            session_set_save_handler($handler, true);\n            session_id('test-session-id');\n            session_start();\n            $_SESSION['value'] = $_GET['value'] ?? 'none';\n            session_write_close();\n            $output[] = \"HANDLER_SET_AND_STARTED\";\n            $output[] = \"session.save_handler=\" . ini_get('session.save_handler');\n            break;\n\n        case 'start_without_handler':\n            // Try to start session without setting handler\n            // This should use the default handler (files) but in worker mode\n            // the INI session.save_handler might still be \"user\" from previous request\n            $saveHandlerBefore = ini_get('session.save_handler');\n            $error = null;\n            $exception = null;\n            $result = false;\n\n            // Capture any errors\n            set_error_handler(function($errno, $errstr) use (&$error) {\n                $error = $errstr;\n                return true;\n            });\n\n            try {\n                session_id('test-session-id-2');\n                $result = session_start();\n                if ($result) {\n                    session_write_close();\n                }\n            } catch (Throwable $e) {\n                $exception = $e->getMessage();\n            }\n\n            restore_error_handler();\n\n            // Now output everything\n            $output[] = \"save_handler_before=\" . $saveHandlerBefore;\n            $output[] = \"SESSION_START_RESULT=\" . ($result ? \"true\" : \"false\");\n            if ($error) {\n                $output[] = \"ERROR:\" . $error;\n            }\n            if ($exception) {\n                $output[] = \"EXCEPTION:\" . $exception;\n            }\n            break;\n\n        case 'just_start':\n            // Simple session start without any custom handler\n            // This should always work\n            session_id('test-session-id-3');\n            session_start();\n            $_SESSION['test'] = 'value';\n            session_write_close();\n            $output[] = \"SESSION_STARTED_OK\";\n            break;\n\n        default:\n            $output[] = \"UNKNOWN_ACTION\";\n    }\n\n    echo implode(\"\\n\", $output);\n};\n"
  },
  {
    "path": "testdata/session-leak.php",
    "content": "<?php\n\nrequire_once __DIR__.'/_executor.php';\n\nreturn function () {\n    $action = $_GET['action'] ?? 'check';\n    $output = [];\n\n    switch ($action) {\n        case 'set':\n            // Set a value in session\n            session_start();\n            $_SESSION['secret'] = $_GET['value'] ?? 'default_secret';\n            $_SESSION['client_id'] = $_GET['client_id'] ?? 'unknown';\n            session_write_close();\n            $output[] = 'SESSION_SET';\n            $output[] = 'secret=' . $_SESSION['secret'];\n            break;\n\n        case 'get':\n            // Read session and return values\n            session_start();\n            $output[] = 'SESSION_READ';\n            $output[] = 'secret=' . ($_SESSION['secret'] ?? 'NOT_FOUND');\n            $output[] = 'client_id=' . ($_SESSION['client_id'] ?? 'NOT_FOUND');\n            $output[] = 'session_id=' . session_id();\n            session_write_close();\n            break;\n\n        case 'set_and_exit':\n            // Set a value in session and exit without calling session_write_close\n            session_start();\n            $_SESSION['secret'] = $_GET['value'] ?? 'exit_secret';\n            $_SESSION['client_id'] = $_GET['client_id'] ?? 'exit_client';\n            // Intentionally NOT calling session_write_close() before exit\n            $output[] = 'BEFORE_EXIT';\n            echo implode(\"\\n\", $output);\n            flush();\n            exit(1);\n            break;\n\n        case 'check_empty':\n            // Check that session is empty (no leak from other clients)\n            // Note: We intentionally do NOT call session_start() here.\n            // $_SESSION should be empty without starting a session.\n            // If data leaks from previous requests, this test will catch it.\n            $output[] = 'SESSION_CHECK';\n            if (empty($_SESSION)) {\n                $output[] = 'SESSION_EMPTY=true';\n            } else {\n                $output[] = 'SESSION_EMPTY=false';\n                $output[] = 'LEAKED_DATA=' . json_encode($_SESSION);\n            }\n            $output[] = 'session_id=' . session_id();\n            break;\n\n        default:\n            $output[] = 'UNKNOWN_ACTION';\n    }\n\n    echo implode(\"\\n\", $output);\n};\n"
  },
  {
    "path": "testdata/session.php",
    "content": "<?php\n\nrequire_once __DIR__.'/_executor.php';\n\nreturn function () {\n    session_start();\n\n    if (isset($_SESSION['count'])) {\n        $_SESSION['count']++;\n    } else {\n        $_SESSION['count'] = 0;\n    }\n\n    echo 'Count: '.$_SESSION['count'].\"\\n\";\n};\n"
  },
  {
    "path": "testdata/sleep.php",
    "content": "<?php\n\nrequire_once __DIR__ . '/_executor.php';\n\nreturn function () {\n    $sleep = (int)($_GET['sleep'] ?? 0);\n    $work = (int)($_GET['work'] ?? 0);\n    $output = (int)($_GET['output'] ?? 1);\n    $iterations = (int)($_GET['iterations'] ?? 1);\n\n    for ($i = 0; $i < $iterations; $i++) {\n        // simulate work\n        // with 30_000 iterations we're in the range of a simple Laravel request\n        // (without JIT and with debug symbols enabled)\n        for ($j = 0; $j < $work; $j++) {\n            $a = +$j;\n        }\n\n        // simulate IO, sleep x milliseconds\n        if ($sleep > 0) {\n            usleep($sleep * 1000);\n        }\n\n        // simulate output\n        for ($k = 0; $k < $output; $k++) {\n            echo \"slept for $sleep ms and worked for $work iterations\";\n        }\n    }\n};\n"
  },
  {
    "path": "testdata/super-globals.php",
    "content": "<?php\n\nrequire_once __DIR__.'/_executor.php';\n\nreturn function () {\n    var_export($_GET);\n    var_export($_POST);\n    var_export($_SERVER);\n};\n"
  },
  {
    "path": "testdata/symlinks/test/document-root.php",
    "content": "<?php\n\nif (isset($_SERVER['FRANKENPHP_WORKER'])) {\n    $i = 0;\n    do {\n        $ok = frankenphp_handle_request(function () use ($i): void {\n            echo \"DOCUMENT_ROOT=\" . $_SERVER['DOCUMENT_ROOT'] . \"\\n\";\n        });\n        $i++;\n    } while ($ok);\n} else {\n    echo \"DOCUMENT_ROOT=\" . $_SERVER['DOCUMENT_ROOT'] . \"\\n\";\n}\n"
  },
  {
    "path": "testdata/symlinks/test/index.php",
    "content": "<?php\n\nif (!isset($_SERVER['FRANKENPHP_WORKER'])) {\n    die(\"Error: This script must be run in worker mode (FRANKENPHP_WORKER not set to '1')\\n\");\n}\n\n$i = 0;\ndo {\n    $ok = frankenphp_handle_request(function () use ($i): void {\n        echo sprintf(\"Request: %d\\n\", $i);\n    });\n    $i++;\n} while ($ok);\n"
  },
  {
    "path": "testdata/symlinks/test/nested/index.php",
    "content": "<?php\n\nif (!isset($_SERVER['FRANKENPHP_WORKER'])) {\n    die(\"Error: This script must be run in worker mode (FRANKENPHP_WORKER not set to '1')\\n\");\n}\n\n$i = 0;\ndo {\n    $ok = frankenphp_handle_request(function () use ($i): void {\n        echo sprintf(\"Nested request: %d\\n\", $i);\n    });\n    $i++;\n} while ($ok);\n"
  },
  {
    "path": "testdata/timeout.php",
    "content": "<?php\n\nrequire_once __DIR__.'/_executor.php';\n\nreturn function () {\n    printf(\"request: %d\\n\", $_GET['i'] ?? 'unknown');\n    set_time_limit(1);\n\n    $x = true;\n    $y = 0;\n    while ($x) {\n        $y++;\n    }\n};\n"
  },
  {
    "path": "testdata/transition-regular.php",
    "content": "<?php\n\necho \"Hello from regular thread\";\n"
  },
  {
    "path": "testdata/transition-worker-1.php",
    "content": "<?php\n\nwhile (frankenphp_handle_request(function () {\n    echo \"Hello from worker 1\";\n})) {\n\n}\n"
  },
  {
    "path": "testdata/transition-worker-2.php",
    "content": "<?php\n\nwhile (frankenphp_handle_request(function () {\n    echo \"Hello from worker 2\";\n    // Simulate work to force potential race conditions (phpmainthread_test.go)\n    usleep(1000);\n})) {\n\n}\n"
  },
  {
    "path": "testdata/worker-env.php",
    "content": "<?php\n\n$workerServer = $_SERVER;\n\nrequire_once __DIR__.'/_executor.php';\n\nreturn function () use ($workerServer) {\n    echo $_SERVER['FOO'] ?? '';\n    echo $workerServer['FOO'] ?? '';\n    echo $_GET['i'] ?? '';\n};\n"
  },
  {
    "path": "testdata/worker-getopt.php",
    "content": "<?php\n\ndo {\n    $ok = frankenphp_handle_request(function (): void {\n        print_r($_SERVER);\n\n\t\tif (!isset($_SERVER['HTTP_REQUEST'])) {\n\t\t\texit(1);\n\t\t}\n\t\tif (isset($_SERVER['FRANKENPHP_WORKER'])) {\n\t\t\texit(2);\n\t\t}\n\t\tif (isset($_SERVER['FOO'])) {\n\t\t\texit(3);\n\t\t}\n    });\n\n    getopt('abc');\n} while ($ok);\n"
  },
  {
    "path": "testdata/worker-restart.php",
    "content": "<?php\n\n$fn = static function () {\n    echo sprintf(\"Counter (%s)\", $_GET['i'] ?? 'i not set');\n};\n\n$loopMax = $_SERVER['EVERY'] ?? 10;\n$loops = 0;\ndo {\n    $ret = \\frankenphp_handle_request($fn);\n} while ($ret && (-1 === $loopMax || ++$loops < $loopMax));\n\nexit(0);\n"
  },
  {
    "path": "testdata/worker-with-counter.php",
    "content": "<?php\n\n$numberOfRequests = 0;\n$printNumberOfRequests = function () use (&$numberOfRequests) {\n    $numberOfRequests++;\n    echo \"requests:$numberOfRequests\";\n};\n\nwhile (frankenphp_handle_request($printNumberOfRequests)) {\n\n}\n"
  },
  {
    "path": "testdata/worker-with-env.php",
    "content": "﻿<?php\n\n$env = $_SERVER['APP_ENV'] ?? '';\ndo {\n    $ok = frankenphp_handle_request(function () use ($env): void {\n        echo \"Worker has APP_ENV=$env\";\n    });\n} while ($ok);\n"
  },
  {
    "path": "testdata/worker-with-session-handler.php",
    "content": "<?php\n\n// Custom session handler defined BEFORE the worker loop\nclass PreLoopSessionHandler implements SessionHandlerInterface\n{\n    private static array $data = [];\n\n    public function open(string $path, string $name): bool\n    {\n        return true;\n    }\n\n    public function close(): bool\n    {\n        return true;\n    }\n\n    public function read(string $id): string|false\n    {\n        return self::$data[$id] ?? '';\n    }\n\n    public function write(string $id, string $data): bool\n    {\n        self::$data[$id] = $data;\n        return true;\n    }\n\n    public function destroy(string $id): bool\n    {\n        unset(self::$data[$id]);\n        return true;\n    }\n\n    public function gc(int $max_lifetime): int|false\n    {\n        return 0;\n    }\n}\n\n// Set the session handler BEFORE the worker loop\n$handler = new PreLoopSessionHandler();\nsession_set_save_handler($handler, true);\n\n$requestCount = 0;\n\ndo {\n    $ok = frankenphp_handle_request(function () use (&$requestCount): void {\n        $requestCount++;\n        $output = [];\n        $output[] = \"request=$requestCount\";\n\n        $action = $_GET['action'] ?? 'check';\n\n        switch ($action) {\n            case 'use_session':\n                // Try to use the session - should work with pre-loop handler\n                $error = null;\n                set_error_handler(function ($errno, $errstr) use (&$error) {\n                    $error = $errstr;\n                    return true;\n                });\n\n                try {\n                    session_id('test-preloop-' . $requestCount);\n                    $result = session_start();\n                    if ($result) {\n                        $_SESSION['test'] = 'value-' . $requestCount;\n                        session_write_close();\n                        $output[] = \"SESSION_OK\";\n                    } else {\n                        $output[] = \"SESSION_START_FAILED\";\n                    }\n                } catch (Throwable $e) {\n                    $output[] = \"EXCEPTION:\" . $e->getMessage();\n                }\n\n                restore_error_handler();\n\n                if ($error) {\n                    $output[] = \"ERROR:\" . $error;\n                }\n                break;\n\n            case 'check':\n            default:\n                $saveHandler = ini_get('session.save_handler');\n                $output[] = \"save_handler=$saveHandler\";\n                if ($saveHandler === 'user') {\n                    $output[] = \"HANDLER_PRESERVED\";\n                } else {\n                    $output[] = \"HANDLER_LOST\";\n                }\n        }\n\n        echo implode(\"\\n\", $output);\n    });\n} while ($ok);\n"
  },
  {
    "path": "testdata/worker.php",
    "content": "<?php\n\n$i = 0;\ndo {\n    $ok = frankenphp_handle_request(function () use ($i): void {\n        echo sprintf(\"Requests handled: %d (request time: %s)\\n\", $i, $_SERVER['REQUEST_TIME_FLOAT']);\n    \n        var_export($_GET);\n        var_export($_POST);\n        var_export($_SERVER);    \n    });\n\n    $i++;\n} while ($ok);\n"
  },
  {
    "path": "threadinactive.go",
    "content": "package frankenphp\n\nimport (\n\t\"context\"\n\n\t\"github.com/dunglas/frankenphp/internal/state\"\n)\n\n// representation of a thread with no work assigned to it\n// implements the threadHandler interface\n// each inactive thread weighs around ~350KB\n// keeping threads at 'inactive' will consume more memory, but allow a faster transition\ntype inactiveThread struct {\n\tthread *phpThread\n}\n\nfunc convertToInactiveThread(thread *phpThread) {\n\tthread.setHandler(&inactiveThread{thread: thread})\n}\n\nfunc (handler *inactiveThread) beforeScriptExecution() string {\n\tthread := handler.thread\n\n\tswitch thread.state.Get() {\n\tcase state.TransitionRequested:\n\t\treturn thread.transitionToNewHandler()\n\n\tcase state.Booting, state.TransitionComplete:\n\t\tthread.state.Set(state.Inactive)\n\n\t\t// wait for external signal to start or shut down\n\t\tthread.state.MarkAsWaiting(true)\n\t\tthread.state.WaitFor(state.TransitionRequested, state.ShuttingDown)\n\t\tthread.state.MarkAsWaiting(false)\n\n\t\treturn handler.beforeScriptExecution()\n\n\tcase state.ShuttingDown:\n\t\t// signal to stop\n\t\treturn \"\"\n\t}\n\n\tpanic(\"unexpected state: \" + thread.state.Name())\n}\n\nfunc (handler *inactiveThread) afterScriptExecution(int) {\n\tpanic(\"inactive threads should not execute scripts\")\n}\n\nfunc (handler *inactiveThread) frankenPHPContext() *frankenPHPContext {\n\treturn nil\n}\n\nfunc (handler *inactiveThread) context() context.Context {\n\treturn globalCtx\n}\n\nfunc (handler *inactiveThread) name() string {\n\treturn \"Inactive PHP Thread\"\n}\n"
  },
  {
    "path": "threadregular.go",
    "content": "package frankenphp\n\nimport (\n\t\"context\"\n\t\"runtime\"\n\t\"sync\"\n\t\"sync/atomic\"\n\n\t\"github.com/dunglas/frankenphp/internal/state\"\n)\n\n// representation of a non-worker PHP thread\n// executes PHP scripts in a web context\n// implements the threadHandler interface\ntype regularThread struct {\n\tcontextHolder\n\n\tstate  *state.ThreadState\n\tthread *phpThread\n}\n\nvar (\n\tregularThreads       []*phpThread\n\tregularThreadMu      = &sync.RWMutex{}\n\tregularRequestChan   chan contextHolder\n\tqueuedRegularThreads = atomic.Int32{}\n)\n\nfunc convertToRegularThread(thread *phpThread) {\n\tthread.setHandler(&regularThread{\n\t\tthread: thread,\n\t\tstate:  thread.state,\n\t})\n\tattachRegularThread(thread)\n}\n\n// beforeScriptExecution returns the name of the script or an empty string on shutdown\nfunc (handler *regularThread) beforeScriptExecution() string {\n\tswitch handler.state.Get() {\n\tcase state.TransitionRequested:\n\t\tdetachRegularThread(handler.thread)\n\t\treturn handler.thread.transitionToNewHandler()\n\n\tcase state.TransitionComplete:\n\t\thandler.thread.updateContext(false)\n\t\thandler.state.Set(state.Ready)\n\n\t\treturn handler.waitForRequest()\n\n\tcase state.Ready:\n\t\treturn handler.waitForRequest()\n\n\tcase state.ShuttingDown:\n\t\tdetachRegularThread(handler.thread)\n\t\t// signal to stop\n\t\treturn \"\"\n\t}\n\n\tpanic(\"unexpected state: \" + handler.state.Name())\n}\n\nfunc (handler *regularThread) afterScriptExecution(_ int) {\n\thandler.afterRequest()\n}\n\nfunc (handler *regularThread) frankenPHPContext() *frankenPHPContext {\n\treturn handler.contextHolder.frankenPHPContext\n}\n\nfunc (handler *regularThread) context() context.Context {\n\treturn handler.ctx\n}\n\nfunc (handler *regularThread) name() string {\n\treturn \"Regular PHP Thread\"\n}\n\nfunc (handler *regularThread) waitForRequest() string {\n\thandler.state.MarkAsWaiting(true)\n\n\tvar ch contextHolder\n\n\tselect {\n\tcase <-handler.thread.drainChan:\n\t\t// go back to beforeScriptExecution\n\t\treturn handler.beforeScriptExecution()\n\tcase ch = <-regularRequestChan:\n\tcase ch = <-handler.thread.requestChan:\n\t}\n\n\thandler.ctx = ch.ctx\n\thandler.contextHolder.frankenPHPContext = ch.frankenPHPContext\n\thandler.state.MarkAsWaiting(false)\n\n\t// set the scriptFilename that should be executed\n\treturn handler.contextHolder.frankenPHPContext.scriptFilename\n}\n\nfunc (handler *regularThread) afterRequest() {\n\thandler.contextHolder.frankenPHPContext.closeContext()\n\thandler.contextHolder.frankenPHPContext = nil\n\thandler.ctx = nil\n}\n\nfunc handleRequestWithRegularPHPThreads(ch contextHolder) error {\n\tmetrics.StartRequest()\n\n\truntime.Gosched()\n\n\tif queuedRegularThreads.Load() == 0 {\n\t\tregularThreadMu.RLock()\n\t\tfor _, thread := range regularThreads {\n\t\t\tselect {\n\t\t\tcase thread.requestChan <- ch:\n\t\t\t\tregularThreadMu.RUnlock()\n\t\t\t\t<-ch.frankenPHPContext.done\n\t\t\t\tmetrics.StopRequest()\n\n\t\t\t\treturn nil\n\t\t\tdefault:\n\t\t\t\t// thread was not available\n\t\t\t}\n\t\t}\n\t\tregularThreadMu.RUnlock()\n\t}\n\n\t// if no thread was available, mark the request as queued and fan it out to all threads\n\tqueuedRegularThreads.Add(1)\n\tmetrics.QueuedRequest()\n\n\tfor {\n\t\tselect {\n\t\tcase regularRequestChan <- ch:\n\t\t\tqueuedRegularThreads.Add(-1)\n\t\t\tmetrics.DequeuedRequest()\n\n\t\t\t<-ch.frankenPHPContext.done\n\t\t\tmetrics.StopRequest()\n\n\t\t\treturn nil\n\t\tcase scaleChan <- ch.frankenPHPContext:\n\t\t\t// the request has triggered scaling, continue to wait for a thread\n\t\tcase <-timeoutChan(maxWaitTime):\n\t\t\t// the request has timed out stalling\n\t\t\tqueuedRegularThreads.Add(-1)\n\t\t\tmetrics.DequeuedRequest()\n\t\t\tmetrics.StopRequest()\n\n\t\t\tch.frankenPHPContext.reject(ErrMaxWaitTimeExceeded)\n\n\t\t\treturn ErrMaxWaitTimeExceeded\n\t\t}\n\t}\n}\n\nfunc attachRegularThread(thread *phpThread) {\n\tregularThreadMu.Lock()\n\tregularThreads = append(regularThreads, thread)\n\tregularThreadMu.Unlock()\n}\n\nfunc detachRegularThread(thread *phpThread) {\n\tregularThreadMu.Lock()\n\tfor i, t := range regularThreads {\n\t\tif t == thread {\n\t\t\tregularThreads = append(regularThreads[:i], regularThreads[i+1:]...)\n\t\t\tbreak\n\t\t}\n\t}\n\tregularThreadMu.Unlock()\n}\n"
  },
  {
    "path": "threadtasks_test.go",
    "content": "package frankenphp\n\nimport (\n\t\"context\"\n\t\"sync\"\n\n\t\"github.com/dunglas/frankenphp/internal/state\"\n)\n\n// representation of a thread that handles tasks directly assigned by go\n// implements the threadHandler interface\ntype taskThread struct {\n\tthread   *phpThread\n\texecChan chan *task\n}\n\n// task callbacks will be executed directly on the PHP thread\n// therefore having full access to the PHP runtime\ntype task struct {\n\tcallback func()\n\tdone     sync.Mutex\n}\n\nfunc newTask(cb func()) *task {\n\tt := &task{callback: cb}\n\tt.done.Lock()\n\n\treturn t\n}\n\nfunc (t *task) waitForCompletion() {\n\tt.done.Lock()\n}\n\nfunc convertToTaskThread(thread *phpThread) *taskThread {\n\thandler := &taskThread{\n\t\tthread:   thread,\n\t\texecChan: make(chan *task),\n\t}\n\tthread.setHandler(handler)\n\treturn handler\n}\n\nfunc (handler *taskThread) beforeScriptExecution() string {\n\tthread := handler.thread\n\n\tswitch thread.state.Get() {\n\tcase state.TransitionRequested:\n\t\treturn thread.transitionToNewHandler()\n\tcase state.Booting, state.TransitionComplete:\n\t\tthread.state.Set(state.Ready)\n\t\thandler.waitForTasks()\n\n\t\treturn handler.beforeScriptExecution()\n\tcase state.Ready:\n\t\thandler.waitForTasks()\n\n\t\treturn handler.beforeScriptExecution()\n\tcase state.ShuttingDown:\n\t\t// signal to stop\n\t\treturn \"\"\n\t}\n\tpanic(\"unexpected state: \" + thread.state.Name())\n}\n\nfunc (handler *taskThread) afterScriptExecution(_ int) {\n\tpanic(\"task threads should not execute scripts\")\n}\n\nfunc (handler *taskThread) frankenPHPContext() *frankenPHPContext {\n\treturn nil\n}\n\nfunc (handler *taskThread) context() context.Context {\n\treturn nil\n}\n\nfunc (handler *taskThread) name() string {\n\treturn \"Task PHP Thread\"\n}\n\nfunc (handler *taskThread) waitForTasks() {\n\tfor {\n\t\tselect {\n\t\tcase task := <-handler.execChan:\n\t\t\ttask.callback()\n\t\t\ttask.done.Unlock() // unlock the task to signal completion\n\t\tcase <-handler.thread.drainChan:\n\t\t\t// thread is shutting down, do not execute the function\n\t\t\treturn\n\t\t}\n\t}\n}\n\nfunc (handler *taskThread) execute(t *task) {\n\thandler.execChan <- t\n}\n"
  },
  {
    "path": "threadworker.go",
    "content": "package frankenphp\n\n// #include \"frankenphp.h\"\nimport \"C\"\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"path/filepath\"\n\t\"time\"\n\t\"unsafe\"\n\n\t\"github.com/dunglas/frankenphp/internal/state\"\n)\n\n// representation of a thread assigned to a worker script\n// executes the PHP worker script in a loop\n// implements the threadHandler interface\ntype workerThread struct {\n\tstate                   *state.ThreadState\n\tthread                  *phpThread\n\tworker                  *worker\n\tdummyFrankenPHPContext  *frankenPHPContext\n\tdummyContext            context.Context\n\tworkerFrankenPHPContext *frankenPHPContext\n\tworkerContext           context.Context\n\tisBootingScript         bool // true if the worker has not reached frankenphp_handle_request yet\n\tfailureCount            int  // number of consecutive startup failures\n}\n\nfunc convertToWorkerThread(thread *phpThread, worker *worker) {\n\tthread.setHandler(&workerThread{\n\t\tstate:  thread.state,\n\t\tthread: thread,\n\t\tworker: worker,\n\t})\n\tworker.attachThread(thread)\n}\n\n// beforeScriptExecution returns the name of the script or an empty string on shutdown\nfunc (handler *workerThread) beforeScriptExecution() string {\n\tswitch handler.state.Get() {\n\tcase state.TransitionRequested:\n\t\tif handler.worker.onThreadShutdown != nil {\n\t\t\thandler.worker.onThreadShutdown(handler.thread.threadIndex)\n\t\t}\n\t\thandler.worker.detachThread(handler.thread)\n\t\treturn handler.thread.transitionToNewHandler()\n\tcase state.Restarting:\n\t\tif handler.worker.onThreadShutdown != nil {\n\t\t\thandler.worker.onThreadShutdown(handler.thread.threadIndex)\n\t\t}\n\t\thandler.state.Set(state.Yielding)\n\t\thandler.state.WaitFor(state.Ready, state.ShuttingDown)\n\t\treturn handler.beforeScriptExecution()\n\tcase state.Ready, state.TransitionComplete:\n\t\thandler.thread.updateContext(true)\n\t\tif handler.worker.onThreadReady != nil {\n\t\t\thandler.worker.onThreadReady(handler.thread.threadIndex)\n\t\t}\n\n\t\tsetupWorkerScript(handler, handler.worker)\n\n\t\treturn handler.worker.fileName\n\tcase state.ShuttingDown:\n\t\tif handler.worker.onThreadShutdown != nil {\n\t\t\thandler.worker.onThreadShutdown(handler.thread.threadIndex)\n\t\t}\n\t\thandler.worker.detachThread(handler.thread)\n\n\t\t// signal to stop\n\t\treturn \"\"\n\t}\n\n\tpanic(\"unexpected state: \" + handler.state.Name())\n}\n\nfunc (handler *workerThread) afterScriptExecution(exitStatus int) {\n\ttearDownWorkerScript(handler, exitStatus)\n}\n\nfunc (handler *workerThread) frankenPHPContext() *frankenPHPContext {\n\tif handler.workerFrankenPHPContext != nil {\n\t\treturn handler.workerFrankenPHPContext\n\t}\n\n\treturn handler.dummyFrankenPHPContext\n}\nfunc (handler *workerThread) context() context.Context {\n\tif handler.workerContext != nil {\n\t\treturn handler.workerContext\n\t}\n\n\treturn handler.dummyContext\n}\n\nfunc (handler *workerThread) name() string {\n\treturn \"Worker PHP Thread - \" + handler.worker.fileName\n}\n\nfunc setupWorkerScript(handler *workerThread, worker *worker) {\n\tmetrics.StartWorker(worker.name)\n\n\t// Create a dummy request to set up the worker\n\tfc, err := newDummyContext(\n\t\tfilepath.Base(worker.fileName),\n\t\tworker.requestOptions...,\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tctx := context.WithValue(globalCtx, contextKey, fc)\n\n\tfc.worker = worker\n\thandler.dummyFrankenPHPContext = fc\n\thandler.dummyContext = ctx\n\thandler.isBootingScript = true\n\n\tif globalLogger.Enabled(ctx, slog.LevelDebug) {\n\t\tglobalLogger.LogAttrs(ctx, slog.LevelDebug, \"starting\", slog.String(\"worker\", worker.name), slog.Int(\"thread\", handler.thread.threadIndex))\n\t}\n}\n\nfunc tearDownWorkerScript(handler *workerThread, exitStatus int) {\n\tworker := handler.worker\n\thandler.dummyFrankenPHPContext = nil\n\thandler.dummyContext = nil\n\n\t// if the worker request is not nil, the script might have crashed\n\t// make sure to close the worker request context\n\tif handler.workerFrankenPHPContext != nil {\n\t\thandler.workerFrankenPHPContext.closeContext()\n\t\thandler.workerFrankenPHPContext = nil\n\t\thandler.workerContext = nil\n\t}\n\n\t// on exit status 0 we just run the worker script again\n\tif exitStatus == 0 && !handler.isBootingScript {\n\t\tmetrics.StopWorker(worker.name, StopReasonRestart)\n\n\t\tif globalLogger.Enabled(globalCtx, slog.LevelDebug) {\n\t\t\tglobalLogger.LogAttrs(globalCtx, slog.LevelDebug, \"restarting\", slog.String(\"worker\", worker.name), slog.Int(\"thread\", handler.thread.threadIndex), slog.Int(\"exit_status\", exitStatus))\n\t\t}\n\n\t\treturn\n\t}\n\n\t// worker has thrown a fatal error or has not reached frankenphp_handle_request\n\tif handler.isBootingScript {\n\t\tmetrics.StopWorker(worker.name, StopReasonBootFailure)\n\t} else {\n\t\tmetrics.StopWorker(worker.name, StopReasonCrash)\n\t}\n\n\tif !handler.isBootingScript {\n\t\t// fatal error (could be due to exit(1), timeouts, etc.)\n\t\tif globalLogger.Enabled(globalCtx, slog.LevelDebug) {\n\t\t\tglobalLogger.LogAttrs(globalCtx, slog.LevelDebug, \"restarting\", slog.String(\"worker\", worker.name), slog.Int(\"thread\", handler.thread.threadIndex), slog.Int(\"exit_status\", exitStatus))\n\t\t}\n\n\t\treturn\n\t}\n\n\tif worker.maxConsecutiveFailures >= 0 && startupFailChan != nil && !watcherIsEnabled && handler.failureCount >= worker.maxConsecutiveFailures {\n\t\tstartupFailChan <- fmt.Errorf(\"too many consecutive failures: worker %s has not reached frankenphp_handle_request()\", worker.fileName)\n\t\thandler.thread.state.Set(state.ShuttingDown)\n\t\treturn\n\t}\n\n\tif watcherIsEnabled {\n\t\t// worker script has probably failed due to script changes while watcher is enabled\n\t\tif globalLogger.Enabled(globalCtx, slog.LevelError) {\n\t\t\tglobalLogger.LogAttrs(globalCtx, slog.LevelWarn, \"(watcher enabled) worker script has not reached frankenphp_handle_request()\", slog.String(\"worker\", worker.name), slog.Int(\"thread\", handler.thread.threadIndex))\n\t\t}\n\t} else {\n\t\t// rare case where worker script has failed on a restart during normal operation\n\t\t// this can happen if startup success depends on external resources\n\t\tif globalLogger.Enabled(globalCtx, slog.LevelWarn) {\n\t\t\tglobalLogger.LogAttrs(globalCtx, slog.LevelWarn, \"worker script has failed on restart\", slog.String(\"worker\", worker.name), slog.Int(\"thread\", handler.thread.threadIndex), slog.Int(\"failures\", handler.failureCount))\n\t\t}\n\t}\n\n\t// wait a bit and try again (exponential backoff)\n\tbackoffDuration := time.Duration(handler.failureCount*handler.failureCount*100) * time.Millisecond\n\tif backoffDuration > time.Second {\n\t\tbackoffDuration = time.Second\n\t}\n\thandler.failureCount++\n\ttime.Sleep(backoffDuration)\n}\n\n// waitForWorkerRequest is called during frankenphp_handle_request in the php worker script.\nfunc (handler *workerThread) waitForWorkerRequest() (bool, any) {\n\t// unpin any memory left over from previous requests\n\thandler.thread.Unpin()\n\n\tif globalLogger.Enabled(globalCtx, slog.LevelDebug) {\n\t\tglobalLogger.LogAttrs(globalCtx, slog.LevelDebug, \"waiting for request\", slog.String(\"worker\", handler.worker.name), slog.Int(\"thread\", handler.thread.threadIndex))\n\t}\n\n\t// Clear the first dummy request created to initialize the worker\n\tif handler.isBootingScript {\n\t\thandler.isBootingScript = false\n\t\thandler.failureCount = 0\n\t\tif !C.frankenphp_shutdown_dummy_request() {\n\t\t\tpanic(\"Not in CGI context\")\n\t\t}\n\n\t\t// worker is truly ready only after reaching frankenphp_handle_request()\n\t\tmetrics.ReadyWorker(handler.worker.name)\n\t}\n\n\tif handler.state.Is(state.TransitionComplete) {\n\t\thandler.state.Set(state.Ready)\n\t}\n\n\thandler.state.MarkAsWaiting(true)\n\n\tvar requestCH contextHolder\n\tselect {\n\tcase <-handler.thread.drainChan:\n\t\tif globalLogger.Enabled(globalCtx, slog.LevelDebug) {\n\t\t\tglobalLogger.LogAttrs(globalCtx, slog.LevelDebug, \"shutting down\", slog.String(\"worker\", handler.worker.name), slog.Int(\"thread\", handler.thread.threadIndex))\n\t\t}\n\n\t\t// flush the opcache when restarting due to watcher or admin api\n\t\t// note: this is done right before frankenphp_handle_request() returns 'false'\n\t\tif handler.state.Is(state.Restarting) {\n\t\t\tC.frankenphp_reset_opcache()\n\t\t}\n\n\t\treturn false, nil\n\tcase requestCH = <-handler.thread.requestChan:\n\tcase requestCH = <-handler.worker.requestChan:\n\t}\n\n\thandler.workerContext = requestCH.ctx\n\thandler.workerFrankenPHPContext = requestCH.frankenPHPContext\n\thandler.state.MarkAsWaiting(false)\n\n\tif globalLogger.Enabled(requestCH.ctx, slog.LevelDebug) {\n\t\tif handler.workerFrankenPHPContext.request == nil {\n\t\t\tglobalLogger.LogAttrs(requestCH.ctx, slog.LevelDebug, \"request handling started\", slog.String(\"worker\", handler.worker.name), slog.Int(\"thread\", handler.thread.threadIndex))\n\t\t} else {\n\t\t\tglobalLogger.LogAttrs(requestCH.ctx, slog.LevelDebug, \"request handling started\", slog.String(\"worker\", handler.worker.name), slog.Int(\"thread\", handler.thread.threadIndex), slog.String(\"url\", handler.workerFrankenPHPContext.request.RequestURI))\n\t\t}\n\t}\n\n\treturn true, handler.workerFrankenPHPContext.handlerParameters\n}\n\n// go_frankenphp_worker_handle_request_start is called at the start of every php request served.\n//\n//export go_frankenphp_worker_handle_request_start\nfunc go_frankenphp_worker_handle_request_start(threadIndex C.uintptr_t) (C.bool, unsafe.Pointer) {\n\thandler := phpThreads[threadIndex].handler.(*workerThread)\n\thasRequest, parameters := handler.waitForWorkerRequest()\n\n\tif parameters != nil {\n\t\tvar ptr unsafe.Pointer\n\n\t\tswitch p := parameters.(type) {\n\t\tcase unsafe.Pointer:\n\t\t\tptr = p\n\n\t\tdefault:\n\t\t\tptr = PHPValue(p)\n\t\t}\n\t\thandler.thread.Pin(ptr)\n\n\t\treturn C.bool(hasRequest), ptr\n\t}\n\n\treturn C.bool(hasRequest), nil\n}\n\n// go_frankenphp_finish_worker_request is called at the end of every php request served.\n//\n//export go_frankenphp_finish_worker_request\nfunc go_frankenphp_finish_worker_request(threadIndex C.uintptr_t, retval *C.zval) {\n\tthread := phpThreads[threadIndex]\n\tctx := thread.context()\n\tfc := ctx.Value(contextKey).(*frankenPHPContext)\n\n\tif retval != nil {\n\t\tr, err := GoValue[any](unsafe.Pointer(retval))\n\t\tif err != nil && globalLogger.Enabled(ctx, slog.LevelError) {\n\t\t\tglobalLogger.LogAttrs(ctx, slog.LevelError, \"cannot convert return value\", slog.Any(\"error\", err), slog.Int(\"thread\", thread.threadIndex))\n\t\t}\n\n\t\tfc.handlerReturn = r\n\t}\n\n\tfc.closeContext()\n\tthread.handler.(*workerThread).workerFrankenPHPContext = nil\n\tthread.handler.(*workerThread).workerContext = nil\n\n\tif globalLogger.Enabled(ctx, slog.LevelDebug) {\n\t\tif fc.request == nil {\n\t\t\tfc.logger.LogAttrs(ctx, slog.LevelDebug, \"request handling finished\", slog.String(\"worker\", fc.worker.name), slog.Int(\"thread\", thread.threadIndex))\n\t\t} else {\n\t\t\tfc.logger.LogAttrs(ctx, slog.LevelDebug, \"request handling finished\", slog.String(\"worker\", fc.worker.name), slog.Int(\"thread\", thread.threadIndex), slog.String(\"url\", fc.request.RequestURI))\n\t\t}\n\t}\n}\n\n// when frankenphp_finish_request() is directly called from PHP\n//\n//export go_frankenphp_finish_php_request\nfunc go_frankenphp_finish_php_request(threadIndex C.uintptr_t) {\n\tthread := phpThreads[threadIndex]\n\tfc := thread.frankenPHPContext()\n\n\tfc.closeContext()\n\n\tctx := thread.context()\n\tif fc.logger.Enabled(ctx, slog.LevelDebug) {\n\t\tfc.logger.LogAttrs(ctx, slog.LevelDebug, \"request handling finished\", slog.Int(\"thread\", thread.threadIndex), slog.String(\"url\", fc.request.RequestURI))\n\t}\n}\n"
  },
  {
    "path": "types.c",
    "content": "#include \"types.h\"\n\nzval *get_ht_packed_data(HashTable *ht, uint32_t index) {\n  if (ht->u.flags & HASH_FLAG_PACKED) {\n    return &ht->arPacked[index];\n  }\n  return NULL;\n}\n\nBucket *get_ht_bucket_data(HashTable *ht, uint32_t index) {\n  if (!(ht->u.flags & HASH_FLAG_PACKED)) {\n    return &ht->arData[index];\n  }\n  return NULL;\n}\n\nvoid *__emalloc__(size_t size) { return malloc(size); }\n\nvoid __efree__(void *ptr) { free(ptr); }\n\nvoid __zend_hash_init__(HashTable *ht, uint32_t nSize, dtor_func_t pDestructor,\n                        bool persistent) {\n  zend_hash_init(ht, nSize, NULL, pDestructor, persistent);\n}\n\nvoid __hash_update_string__(zend_array *ht, zend_string *k, zend_string *v) {\n  zval zv = {0};\n  ZVAL_STR(&zv, v);\n  zend_hash_update(ht, k, &zv);\n}\n\nvoid __zval_null__(zval *zv) { ZVAL_NULL(zv); }\n\nvoid __zval_bool__(zval *zv, bool val) { ZVAL_BOOL(zv, val); }\n\nvoid __zval_long__(zval *zv, zend_long val) { ZVAL_LONG(zv, val); }\n\nvoid __zval_double__(zval *zv, double val) { ZVAL_DOUBLE(zv, val); }\n\nvoid __zval_string__(zval *zv, zend_string *str) { ZVAL_STR(zv, str); }\n\nvoid __zval_empty_string__(zval *zv) { ZVAL_EMPTY_STRING(zv); }\n\nvoid __zval_arr__(zval *zv, zend_array *arr) { ZVAL_ARR(zv, arr); }\n\nzend_array *__zend_new_array__(uint32_t size) { return zend_new_array(size); }\n\nint __zend_is_callable__(zval *cb) { return zend_is_callable(cb, 0, NULL); }\n\nint __call_user_function__(zval *function_name, zval *retval,\n                           uint32_t param_count, zval params[]) {\n  return call_user_function(CG(function_table), NULL, function_name, retval,\n                            param_count, params);\n}\n"
  },
  {
    "path": "types.go",
    "content": "package frankenphp\n\n/*\n#cgo nocallback __zend_new_array__\n#cgo nocallback __zval_null__\n#cgo nocallback __zval_bool__\n#cgo nocallback __zval_long__\n#cgo nocallback __zval_double__\n#cgo nocallback __zval_string__\n#cgo nocallback __zval_arr__\n#cgo noescape __zend_new_array__\n#cgo noescape __zval_null__\n#cgo noescape __zval_bool__\n#cgo noescape __zval_long__\n#cgo noescape __zval_double__\n#cgo noescape __zval_string__\n#cgo noescape __zval_arr__\n#cgo noescape __emalloc__\n#cgo noescape __efree__\n#include \"types.h\"\n*/\nimport \"C\"\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"reflect\"\n\t\"strconv\"\n\t\"unsafe\"\n)\n\ntype toZval interface {\n\ttoZval(*C.zval)\n}\n\n// EXPERIMENTAL: GoString copies a zend_string to a Go string.\nfunc GoString(s unsafe.Pointer) string {\n\tif s == nil {\n\t\treturn \"\"\n\t}\n\n\tzendStr := (*C.zend_string)(s)\n\n\treturn C.GoStringN((*C.char)(unsafe.Pointer(&zendStr.val)), C.int(zendStr.len))\n}\n\n// EXPERIMENTAL: PHPString converts a Go string to a zend_string with copy. The string can be\n// non-persistent (automatically freed after the request by the ZMM) or persistent. If you choose\n// the second mode, it is your repsonsability to free the allocated memory.\nfunc PHPString(s string, persistent bool) unsafe.Pointer {\n\tif s == \"\" {\n\t\treturn nil\n\t}\n\n\tzendStr := C.zend_string_init(\n\t\t(*C.char)(unsafe.Pointer(unsafe.StringData(s))),\n\t\tC.size_t(len(s)),\n\t\tC._Bool(persistent),\n\t)\n\n\treturn unsafe.Pointer(zendStr)\n}\n\n// AssociativeArray represents a PHP array with ordered key-value pairs\ntype AssociativeArray[T any] struct {\n\tMap   map[string]T\n\tOrder []string\n}\n\nfunc (a AssociativeArray[T]) toZval(zval *C.zval) {\n\tC.__zval_arr__(zval, (*C.zend_array)(PHPAssociativeArray[T](a)))\n}\n\n// EXPERIMENTAL: GoAssociativeArray converts a zend_array to a Go AssociativeArray\nfunc GoAssociativeArray[T any](arr unsafe.Pointer) (AssociativeArray[T], error) {\n\tentries, order, err := goArray[T](arr, true)\n\n\treturn AssociativeArray[T]{entries, order}, err\n}\n\n// EXPERIMENTAL: GoMap converts a zend_array to an unordered Go map\nfunc GoMap[T any](arr unsafe.Pointer) (map[string]T, error) {\n\tentries, _, err := goArray[T](arr, false)\n\n\treturn entries, err\n}\n\nfunc goArray[T any](arr unsafe.Pointer, ordered bool) (map[string]T, []string, error) {\n\tif arr == nil {\n\t\treturn nil, nil, errors.New(\"received a nil pointer on array conversion\")\n\t}\n\n\tarray := (*C.zend_array)(arr)\n\n\tif array == nil {\n\t\treturn nil, nil, fmt.Errorf(\"received a *zval that wasn't a HashTable on array conversion\")\n\t}\n\n\tnNumUsed := array.nNumUsed\n\tentries := make(map[string]T, nNumUsed)\n\tvar order []string\n\tif ordered {\n\t\torder = make([]string, 0, nNumUsed)\n\t}\n\n\tif htIsPacked(array) {\n\t\t// if the array is packed, convert all integer keys to strings\n\t\t// this is probably a bug by the dev using this function\n\t\t// still, we'll (inefficiently) convert to an associative array\n\t\tfor i := C.uint32_t(0); i < nNumUsed; i++ {\n\t\t\tv := C.get_ht_packed_data(array, i)\n\t\t\tif v != nil && C.zval_get_type(v) != C.IS_UNDEF {\n\t\t\t\tstrIndex := strconv.Itoa(int(i))\n\t\t\t\te, err := goValue[T](v)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, nil, err\n\t\t\t\t}\n\n\t\t\t\tentries[strIndex] = e\n\t\t\t\tif ordered {\n\t\t\t\t\torder = append(order, strIndex)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn entries, order, nil\n\t}\n\n\tvar zeroVal T\n\n\tfor i := C.uint32_t(0); i < nNumUsed; i++ {\n\t\tbucket := C.get_ht_bucket_data(array, i)\n\t\tif bucket == nil || C.zval_get_type(&bucket.val) == C.IS_UNDEF {\n\t\t\tcontinue\n\t\t}\n\n\t\tv, err := goValue[any](&bucket.val)\n\t\tif err != nil {\n\t\t\treturn nil, nil, err\n\t\t}\n\n\t\tif bucket.key != nil {\n\t\t\tkeyStr := GoString(unsafe.Pointer(bucket.key))\n\t\t\tif v == nil {\n\t\t\t\tentries[keyStr] = zeroVal\n\t\t\t} else {\n\t\t\t\tentries[keyStr] = v.(T)\n\t\t\t}\n\n\t\t\tif ordered {\n\t\t\t\torder = append(order, keyStr)\n\t\t\t}\n\n\t\t\tcontinue\n\t\t}\n\n\t\t// as fallback convert the bucket index to a string key\n\t\tstrIndex := strconv.Itoa(int(bucket.h))\n\t\tentries[strIndex] = v.(T)\n\t\tif ordered {\n\t\t\torder = append(order, strIndex)\n\t\t}\n\t}\n\n\treturn entries, order, nil\n}\n\n// EXPERIMENTAL: GoPackedArray converts a zend_array to a Go slice\nfunc GoPackedArray[T any](arr unsafe.Pointer) ([]T, error) {\n\tif arr == nil {\n\t\treturn nil, errors.New(\"GoPackedArray received a nil value\")\n\t}\n\n\tarray := (*C.zend_array)(arr)\n\n\tif array == nil {\n\t\treturn nil, fmt.Errorf(\"GoPackedArray received *zval that wasn't a HashTable\")\n\t}\n\n\tnNumUsed := array.nNumUsed\n\tresult := make([]T, 0, nNumUsed)\n\n\tif htIsPacked(array) {\n\t\tfor i := C.uint32_t(0); i < nNumUsed; i++ {\n\t\t\tv := C.get_ht_packed_data(array, i)\n\t\t\tif v != nil && C.zval_get_type(v) != C.IS_UNDEF {\n\t\t\t\tv, err := goValue[T](v)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\t\tresult = append(result, v)\n\t\t\t}\n\t\t}\n\n\t\treturn result, nil\n\t}\n\n\t// fallback if ht isn't packed - equivalent to array_values()\n\tfor i := C.uint32_t(0); i < nNumUsed; i++ {\n\t\tbucket := C.get_ht_bucket_data(array, i)\n\t\tif bucket != nil && C.zval_get_type(&bucket.val) != C.IS_UNDEF {\n\t\t\tv, err := goValue[T](&bucket.val)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tresult = append(result, v)\n\t\t}\n\t}\n\n\treturn result, nil\n}\n\n// EXPERIMENTAL: PHPMap converts an unordered Go map to a zend_array\nfunc PHPMap[T any](arr map[string]T) unsafe.Pointer {\n\treturn phpArray[T](arr, nil)\n}\n\n// EXPERIMENTAL: PHPAssociativeArray converts a Go AssociativeArray to a zend_array\nfunc PHPAssociativeArray[T any](arr AssociativeArray[T]) unsafe.Pointer {\n\treturn phpArray[T](arr.Map, arr.Order)\n}\n\nfunc phpArray[T any](entries map[string]T, order []string) unsafe.Pointer {\n\tvar zendArray *C.zend_array\n\n\tif len(order) != 0 {\n\t\tzendArray = createNewArray((uint32)(len(order)))\n\t\tfor _, key := range order {\n\t\t\tval := entries[key]\n\t\t\tzval := phpValue(val)\n\t\t\tC.zend_hash_str_update(zendArray, toUnsafeChar(key), C.size_t(len(key)), zval)\n\t\t\tC.__efree__(unsafe.Pointer(zval))\n\t\t}\n\t} else {\n\t\tzendArray = createNewArray((uint32)(len(entries)))\n\t\tfor key, val := range entries {\n\t\t\tzval := phpValue(val)\n\t\t\tC.zend_hash_str_update(zendArray, toUnsafeChar(key), C.size_t(len(key)), zval)\n\t\t\tC.__efree__(unsafe.Pointer(zval))\n\t\t}\n\t}\n\n\treturn unsafe.Pointer(zendArray)\n}\n\n// EXPERIMENTAL: PHPPackedArray converts a Go slice to a PHP zval with a zend_array value.\nfunc PHPPackedArray[T any](slice []T) unsafe.Pointer {\n\tzendArray := createNewArray((uint32)(len(slice)))\n\tfor _, val := range slice {\n\t\tzval := phpValue(val)\n\t\tC.zend_hash_next_index_insert(zendArray, zval)\n\t\tC.__efree__(unsafe.Pointer(zval))\n\t}\n\n\treturn unsafe.Pointer(zendArray)\n}\n\n// EXPERIMENTAL: GoValue converts a PHP zval to a Go value\n//\n// Zval having the null, bool, long, double, string and array types are currently supported.\n// Arrays can currently only be converted to any[] and AssociativeArray[any].\n// Any other type will cause an error.\n// More types may be supported in the future.\nfunc GoValue[T any](zval unsafe.Pointer) (T, error) {\n\treturn goValue[T]((*C.zval)(zval))\n}\n\nfunc goValue[T any](zval *C.zval) (res T, err error) {\n\tvar (\n\t\tresAny  any\n\t\tresZero T\n\t)\n\tt := C.zval_get_type(zval)\n\n\tswitch t {\n\tcase C.IS_NULL:\n\t\tresAny = any(nil)\n\tcase C.IS_FALSE:\n\t\tresAny = any(false)\n\tcase C.IS_TRUE:\n\t\tresAny = any(true)\n\tcase C.IS_LONG:\n\t\tv, err := extractZvalValue(zval, C.IS_LONG)\n\t\tif err != nil {\n\t\t\treturn resZero, err\n\t\t}\n\n\t\tif v != nil {\n\t\t\tresAny = any(int64(*(*C.zend_long)(v)))\n\n\t\t\tbreak\n\t\t}\n\n\t\tresAny = any(int64(0))\n\tcase C.IS_DOUBLE:\n\t\tv, err := extractZvalValue(zval, C.IS_DOUBLE)\n\t\tif err != nil {\n\t\t\treturn resZero, err\n\t\t}\n\n\t\tif v != nil {\n\t\t\tresAny = any(float64(*(*C.double)(v)))\n\n\t\t\tbreak\n\t\t}\n\n\t\tresAny = any(float64(0))\n\tcase C.IS_STRING:\n\t\tv, err := extractZvalValue(zval, C.IS_STRING)\n\t\tif err != nil {\n\t\t\treturn resZero, err\n\t\t}\n\n\t\tif v == nil {\n\t\t\tresAny = any(\"\")\n\n\t\t\tbreak\n\t\t}\n\n\t\tresAny = any(GoString(v))\n\tcase C.IS_ARRAY:\n\t\tv, err := extractZvalValue(zval, C.IS_ARRAY)\n\t\tif err != nil {\n\t\t\treturn resZero, err\n\t\t}\n\n\t\tarray := (*C.zend_array)(v)\n\t\tif array != nil && htIsPacked(array) {\n\t\t\ttyp := reflect.TypeOf(res)\n\t\t\tif typ == nil || typ.Kind() == reflect.Interface && typ.NumMethod() == 0 {\n\t\t\t\tr, e := GoPackedArray[any](unsafe.Pointer(array))\n\t\t\t\tif e != nil {\n\t\t\t\t\treturn resZero, e\n\t\t\t\t}\n\n\t\t\t\tresAny = any(r)\n\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\treturn resZero, fmt.Errorf(\"cannot convert packed array to non-any Go type %s\", typ.String())\n\t\t}\n\n\t\ta, err := GoAssociativeArray[T](unsafe.Pointer(array))\n\t\tif err != nil {\n\t\t\treturn resZero, err\n\t\t}\n\n\t\tresAny = any(a)\n\tdefault:\n\t\treturn resZero, fmt.Errorf(\"unsupported zval type %d\", t)\n\t}\n\n\tif resAny == nil {\n\t\treturn resZero, nil\n\t}\n\n\tif castRes, ok := resAny.(T); ok {\n\t\treturn castRes, nil\n\t}\n\n\treturn resZero, fmt.Errorf(\"cannot cast value of type %T to type %T\", resAny, res)\n}\n\n// EXPERIMENTAL: PHPValue converts a Go any to a PHP zval\n//\n// nil, bool, int, int64, float64, string, []any, and map[string]any are currently supported.\n// Any other type will cause a panic.\n// More types may be supported in the future.\nfunc PHPValue(value any) unsafe.Pointer {\n\treturn unsafe.Pointer(phpValue(value))\n}\n\nfunc phpValue(value any) *C.zval {\n\tzval := (*C.zval)(C.__emalloc__(C.size_t(unsafe.Sizeof(C.zval{}))))\n\n\tif toZvalObj, ok := value.(toZval); ok {\n\t\ttoZvalObj.toZval(zval)\n\t\treturn zval\n\t}\n\n\tswitch v := value.(type) {\n\tcase nil:\n\t\tC.__zval_null__(zval)\n\tcase bool:\n\t\tC.__zval_bool__(zval, C._Bool(v))\n\tcase int:\n\t\tC.__zval_long__(zval, C.zend_long(v))\n\tcase int64:\n\t\tC.__zval_long__(zval, C.zend_long(v))\n\tcase float64:\n\t\tC.__zval_double__(zval, C.double(v))\n\tcase string:\n\t\tif v == \"\" {\n\t\t\tC.__zval_empty_string__(zval)\n\t\t\tbreak\n\t\t}\n\t\tstr := (*C.zend_string)(PHPString(v, false))\n\t\tC.__zval_string__(zval, str)\n\tcase AssociativeArray[any]:\n\t\tC.__zval_arr__(zval, (*C.zend_array)(PHPAssociativeArray[any](v)))\n\tcase map[string]any:\n\t\tC.__zval_arr__(zval, (*C.zend_array)(PHPMap[any](v)))\n\tcase []any:\n\t\tC.__zval_arr__(zval, (*C.zend_array)(PHPPackedArray[any](v)))\n\tdefault:\n\t\tC.__efree__(unsafe.Pointer(zval))\n\t\tpanic(fmt.Sprintf(\"unsupported Go type %T\", v))\n\t}\n\n\treturn zval\n}\n\n// createNewArray creates a new zend_array with the specified size.\nfunc createNewArray(size uint32) *C.zend_array {\n\tarr := C.__zend_new_array__(C.uint32_t(size))\n\treturn (*C.zend_array)(unsafe.Pointer(arr))\n}\n\n// IsPacked determines if the given zend_array is a packed array (list).\n// Returns false if the array is nil or not packed.\nfunc IsPacked(arr unsafe.Pointer) bool {\n\tif arr == nil {\n\t\treturn false\n\t}\n\n\treturn htIsPacked((*C.zend_array)(arr))\n}\n\n// htIsPacked checks if a zend_array is a list (packed) or hashmap (not packed).\nfunc htIsPacked(ht *C.zend_array) bool {\n\tflags := *(*C.uint32_t)(unsafe.Pointer(&ht.u[0]))\n\n\treturn (flags & C.HASH_FLAG_PACKED) != 0\n}\n\n// extractZvalValue returns a pointer to the zval value cast to the expected type\nfunc extractZvalValue(zval *C.zval, expectedType C.uint8_t) (unsafe.Pointer, error) {\n\tif zval == nil {\n\t\tif expectedType == C.IS_NULL {\n\t\t\treturn nil, nil\n\t\t}\n\n\t\treturn nil, fmt.Errorf(\"zval type mismatch: expected %d, got nil\", expectedType)\n\t}\n\n\tif zType := C.zval_get_type(zval); zType != expectedType {\n\t\treturn nil, fmt.Errorf(\"zval type mismatch: expected %d, got %d\", expectedType, zType)\n\t}\n\n\tv := unsafe.Pointer(&zval.value[0])\n\n\tswitch expectedType {\n\tcase C.IS_LONG, C.IS_DOUBLE:\n\t\treturn v, nil\n\tcase C.IS_STRING:\n\t\treturn unsafe.Pointer(*(**C.zend_string)(v)), nil\n\tcase C.IS_ARRAY:\n\t\treturn unsafe.Pointer(*(**C.zend_array)(v)), nil\n\t}\n\n\treturn nil, fmt.Errorf(\"unsupported zval type %d\", expectedType)\n}\n\nfunc zendStringRelease(p unsafe.Pointer) {\n\tzs := (*C.zend_string)(p)\n\tC.zend_string_release(zs)\n}\n\nfunc zendHashDestroy(p unsafe.Pointer) {\n\tht := (*C.zend_array)(p)\n\tC.zend_hash_destroy(ht)\n}\n\n// EXPERIMENTAL: CallPHPCallable executes a PHP callable with the given parameters.\n// Returns the result of the callable as a Go interface{}, or nil if the call failed.\nfunc CallPHPCallable(cb unsafe.Pointer, params []interface{}) interface{} {\n\tif cb == nil {\n\t\treturn nil\n\t}\n\n\tcallback := (*C.zval)(cb)\n\tif callback == nil {\n\t\treturn nil\n\t}\n\n\tif C.__zend_is_callable__(callback) == 0 {\n\t\treturn nil\n\t}\n\n\tparamCount := len(params)\n\tvar paramStorage *C.zval\n\tif paramCount > 0 {\n\t\tparamStorage = (*C.zval)(C.__emalloc__(C.size_t(paramCount) * C.size_t(unsafe.Sizeof(C.zval{}))))\n\t\tdefer func() {\n\t\t\tfor i := 0; i < paramCount; i++ {\n\t\t\t\ttargetZval := (*C.zval)(unsafe.Pointer(uintptr(unsafe.Pointer(paramStorage)) + uintptr(i)*unsafe.Sizeof(C.zval{})))\n\t\t\t\tC.zval_ptr_dtor(targetZval)\n\t\t\t}\n\t\t\tC.__efree__(unsafe.Pointer(paramStorage))\n\t\t}()\n\n\t\tfor i, param := range params {\n\t\t\ttargetZval := (*C.zval)(unsafe.Pointer(uintptr(unsafe.Pointer(paramStorage)) + uintptr(i)*unsafe.Sizeof(C.zval{})))\n\t\t\tsourceZval := phpValue(param)\n\t\t\t*targetZval = *sourceZval\n\t\t\tC.__efree__(unsafe.Pointer(sourceZval))\n\t\t}\n\t}\n\n\tvar retval C.zval\n\n\tresult := C.__call_user_function__(callback, &retval, C.uint32_t(paramCount), paramStorage)\n\tif result != C.SUCCESS {\n\t\treturn nil\n\t}\n\n\tgoResult, err := goValue[any](&retval)\n\tC.zval_ptr_dtor(&retval)\n\n\tif err != nil {\n\t\treturn nil\n\t}\n\n\treturn goResult\n}\n"
  },
  {
    "path": "types.h",
    "content": "#ifndef TYPES_H\n#define TYPES_H\n\n#include \"frankenphp.h\"\n#include <Zend/zend.h>\n#include <Zend/zend_API.h>\n#include <Zend/zend_alloc.h>\n#include <Zend/zend_hash.h>\n#include <stdlib.h>\n\nzval *get_ht_packed_data(HashTable *, uint32_t index);\nBucket *get_ht_bucket_data(HashTable *, uint32_t index);\n\nvoid *__emalloc__(size_t size);\nvoid __efree__(void *ptr);\nvoid __zend_hash_init__(HashTable *ht, uint32_t nSize, dtor_func_t pDestructor,\n                        bool persistent);\nvoid __hash_update_string__(zend_array *ht, zend_string *k, zend_string *v);\n\nint __zend_is_callable__(zval *cb);\nint __call_user_function__(zval *function_name, zval *retval,\n                           uint32_t param_count, zval params[]);\n\nvoid __zval_null__(zval *zv);\nvoid __zval_bool__(zval *zv, bool val);\nvoid __zval_long__(zval *zv, zend_long val);\nvoid __zval_double__(zval *zv, double val);\nvoid __zval_string__(zval *zv, zend_string *str);\nvoid __zval_empty_string__(zval *zv);\nvoid __zval_arr__(zval *zv, zend_array *arr);\nzend_array *__zend_new_array__(uint32_t size);\n\n#endif\n"
  },
  {
    "path": "types_test.go",
    "content": "package frankenphp\n\nimport (\n\t\"log/slog\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// execute the function on a PHP thread directly\n// this is necessary if tests make use of PHP's internal allocation\nfunc testOnDummyPHPThread(t *testing.T, test func()) {\n\tt.Helper()\n\n\tglobalLogger = slog.Default()\n\t_, err := initPHPThreads(1, 1, nil) // boot 1 thread\n\tassert.NoError(t, err)\n\thandler := convertToTaskThread(phpThreads[0])\n\n\ttask := newTask(test)\n\thandler.execute(task)\n\ttask.waitForCompletion()\n\n\tdrainPHPThreads()\n}\n\nfunc TestGoString(t *testing.T) {\n\ttestOnDummyPHPThread(t, func() {\n\t\toriginalString := \"Hello, World!\"\n\n\t\tphpString := PHPString(originalString, false)\n\t\tdefer zendStringRelease(phpString)\n\n\t\tassert.Equal(t, originalString, GoString(phpString), \"string -> zend_string -> string should yield an equal string\")\n\t})\n}\n\nfunc TestPHPMap(t *testing.T) {\n\ttestOnDummyPHPThread(t, func() {\n\t\toriginalMap := map[string]string{\n\t\t\t\"foo1\": \"bar1\",\n\t\t\t\"foo2\": \"bar2\",\n\t\t}\n\n\t\tphpArray := PHPMap(originalMap)\n\t\tdefer zendHashDestroy(phpArray)\n\t\tconvertedMap, err := GoMap[string](phpArray)\n\t\trequire.NoError(t, err)\n\n\t\tassert.Equal(t, originalMap, convertedMap, \"associative array should be equal after conversion\")\n\t})\n}\n\nfunc TestOrderedPHPAssociativeArray(t *testing.T) {\n\ttestOnDummyPHPThread(t, func() {\n\t\toriginalArray := AssociativeArray[string]{\n\t\t\tMap: map[string]string{\n\t\t\t\t\"foo1\": \"bar1\",\n\t\t\t\t\"foo2\": \"bar2\",\n\t\t\t},\n\t\t\tOrder: []string{\"foo2\", \"foo1\"},\n\t\t}\n\n\t\tphpArray := PHPAssociativeArray(originalArray)\n\t\tdefer zendHashDestroy(phpArray)\n\t\tconvertedArray, err := GoAssociativeArray[string](phpArray)\n\t\trequire.NoError(t, err)\n\n\t\tassert.Equal(t, originalArray, convertedArray, \"associative array should be equal after conversion\")\n\t})\n}\n\nfunc TestPHPPackedArray(t *testing.T) {\n\ttestOnDummyPHPThread(t, func() {\n\t\toriginalSlice := []string{\"bar1\", \"bar2\"}\n\n\t\tphpArray := PHPPackedArray(originalSlice)\n\t\tdefer zendHashDestroy(phpArray)\n\t\tconvertedSlice, err := GoPackedArray[string](phpArray)\n\t\trequire.NoError(t, err)\n\n\t\tassert.Equal(t, originalSlice, convertedSlice, \"slice should be equal after conversion\")\n\t})\n}\n\nfunc TestPHPPackedArrayToGoMap(t *testing.T) {\n\ttestOnDummyPHPThread(t, func() {\n\t\toriginalSlice := []string{\"bar1\", \"bar2\"}\n\t\texpectedMap := map[string]string{\n\t\t\t\"0\": \"bar1\",\n\t\t\t\"1\": \"bar2\",\n\t\t}\n\n\t\tphpArray := PHPPackedArray(originalSlice)\n\t\tdefer zendHashDestroy(phpArray)\n\t\tconvertedMap, err := GoMap[string](phpArray)\n\t\trequire.NoError(t, err)\n\n\t\tassert.Equal(t, expectedMap, convertedMap, \"convert a packed to an associative array\")\n\t})\n}\n\nfunc TestPHPAssociativeArrayToPacked(t *testing.T) {\n\ttestOnDummyPHPThread(t, func() {\n\t\toriginalArray := AssociativeArray[string]{\n\t\t\tMap: map[string]string{\n\t\t\t\t\"foo1\": \"bar1\",\n\t\t\t\t\"foo2\": \"bar2\",\n\t\t\t},\n\t\t\tOrder: []string{\"foo1\", \"foo2\"},\n\t\t}\n\t\texpectedSlice := []string{\"bar1\", \"bar2\"}\n\n\t\tphpArray := PHPAssociativeArray(originalArray)\n\t\tdefer zendHashDestroy(phpArray)\n\t\tconvertedSlice, err := GoPackedArray[string](phpArray)\n\t\trequire.NoError(t, err)\n\n\t\tassert.Equal(t, expectedSlice, convertedSlice, \"convert an associative array to a slice\")\n\t})\n}\n\nfunc TestNestedMixedArray(t *testing.T) {\n\ttestOnDummyPHPThread(t, func() {\n\t\toriginalArray := map[string]any{\n\t\t\t\"string\":      \"value\",\n\t\t\t\"int\":         int64(123),\n\t\t\t\"float\":       1.2,\n\t\t\t\"true\":        true,\n\t\t\t\"false\":       false,\n\t\t\t\"nil\":         nil,\n\t\t\t\"packedArray\": []any{\"bar1\", \"bar2\"},\n\t\t\t\"associativeArray\": AssociativeArray[any]{\n\t\t\t\tMap:   map[string]any{\"foo1\": \"bar1\", \"foo2\": \"bar2\"},\n\t\t\t\tOrder: []string{\"foo2\", \"foo1\"},\n\t\t\t},\n\t\t}\n\n\t\tphpArray := PHPMap(originalArray)\n\t\tdefer zendHashDestroy(phpArray)\n\t\tconvertedArray, err := GoMap[any](phpArray)\n\t\trequire.NoError(t, err)\n\n\t\tassert.Equal(t, originalArray, convertedArray, \"nested mixed array should be equal after conversion\")\n\t})\n}\n"
  },
  {
    "path": "vcpkg.json",
    "content": "{\n  \"dependencies\": [\"brotli\", \"pthreads\"]\n}\n"
  },
  {
    "path": "watcher-skip.go",
    "content": "//go:build nowatcher\n\npackage frankenphp\n\nimport \"errors\"\n\ntype hotReloadOpt struct {\n}\n\nvar errWatcherNotEnabled = errors.New(\"watcher support is not enabled\")\n\nfunc initWatchers(o *opt) error {\n\tfor _, o := range o.workers {\n\t\tif len(o.watch) != 0 {\n\t\t\treturn errWatcherNotEnabled\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc drainWatchers() {\n}\n"
  },
  {
    "path": "watcher.go",
    "content": "//go:build !nowatcher\n\npackage frankenphp\n\nimport (\n\t\"sync/atomic\"\n\n\t\"github.com/dunglas/frankenphp/internal/watcher\"\n\twatcherGo \"github.com/e-dant/watcher/watcher-go\"\n)\n\ntype hotReloadOpt struct {\n\thotReload []*watcher.PatternGroup\n}\n\nvar restartWorkers atomic.Bool\n\nfunc initWatchers(o *opt) error {\n\twatchPatterns := make([]*watcher.PatternGroup, 0, len(o.hotReload))\n\n\tfor _, o := range o.workers {\n\t\tif len(o.watch) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\twatcherIsEnabled = true\n\t\twatchPatterns = append(watchPatterns, &watcher.PatternGroup{Patterns: o.watch, Callback: func(_ []*watcherGo.Event) {\n\t\t\trestartWorkers.Store(true)\n\t\t}})\n\t}\n\n\tif watcherIsEnabled {\n\t\twatchPatterns = append(watchPatterns, &watcher.PatternGroup{\n\t\t\tCallback: func(_ []*watcherGo.Event) {\n\t\t\t\tif restartWorkers.Swap(false) {\n\t\t\t\t\tRestartWorkers()\n\t\t\t\t}\n\t\t\t},\n\t\t})\n\t}\n\n\treturn watcher.InitWatcher(globalCtx, globalLogger, append(watchPatterns, o.hotReload...))\n}\n\nfunc drainWatchers() {\n\twatcher.DrainWatcher()\n}\n"
  },
  {
    "path": "watcher_test.go",
    "content": "//go:build !nowatcher\n\npackage frankenphp_test\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// we have to wait a few milliseconds for the watcher debounce to take effect\nconst pollingTime = 250\n\n// in tests checking for no reload: we will poll 3x250ms = 0.75s\nconst minTimesToPollForChanges = 3\n\n// in tests checking for a reload: we will poll a maximum of 60x250ms = 15s\nconst maxTimesToPollForChanges = 60\n\nfunc TestWorkersShouldReloadOnMatchingPattern(t *testing.T) {\n\twatch := []string{\"./testdata/**/*.txt\"}\n\n\trunTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {\n\t\trequestBodyHasReset := pollForWorkerReset(t, handler, maxTimesToPollForChanges)\n\t\tassert.True(t, requestBodyHasReset)\n\t}, &testOptions{nbParallelRequests: 1, nbWorkers: 1, workerScript: \"worker-with-counter.php\", watch: watch})\n}\n\nfunc TestWorkersShouldNotReloadOnExcludingPattern(t *testing.T) {\n\twatch := []string{\"./testdata/**/*.php\"}\n\n\trunTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {\n\t\trequestBodyHasReset := pollForWorkerReset(t, handler, minTimesToPollForChanges)\n\t\tassert.False(t, requestBodyHasReset)\n\t}, &testOptions{nbParallelRequests: 1, nbWorkers: 1, workerScript: \"worker-with-counter.php\", watch: watch})\n}\n\nfunc pollForWorkerReset(t *testing.T, handler func(http.ResponseWriter, *http.Request), limit int) bool {\n\tt.Helper()\n\n\t// first we make an initial request to start the request counter\n\tbody, _ := testGet(\"http://example.com/worker-with-counter.php\", handler, t)\n\tassert.Equal(t, \"requests:1\", body)\n\n\t// now we spam file updates and check if the request counter resets\n\tfor range limit {\n\t\tupdateTestFile(t, filepath.Join(\".\", \"testdata\", \"files\", \"test.txt\"), \"updated\")\n\t\ttime.Sleep(pollingTime * time.Millisecond)\n\t\tbody, _ := testGet(\"http://example.com/worker-with-counter.php\", handler, t)\n\t\tif body == \"requests:1\" {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\nfunc updateTestFile(t *testing.T, fileName, content string) {\n\tabsFileName, err := filepath.Abs(fileName)\n\trequire.NoError(t, err)\n\n\trequire.NoError(t, os.MkdirAll(filepath.Dir(absFileName), 0700))\n\trequire.NoError(t, os.WriteFile(absFileName, []byte(content), 0644))\n}\n"
  },
  {
    "path": "worker.go",
    "content": "package frankenphp\n\n// #include \"frankenphp.h\"\nimport \"C\"\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/dunglas/frankenphp/internal/fastabs\"\n\t\"github.com/dunglas/frankenphp/internal/state\"\n)\n\n// represents a worker script and can have many threads assigned to it\ntype worker struct {\n\tmercureContext\n\n\tname                   string\n\tfileName               string\n\tnum                    int\n\tmaxThreads             int\n\trequestOptions         []RequestOption\n\trequestChan            chan contextHolder\n\tthreads                []*phpThread\n\tthreadMutex            sync.RWMutex\n\tallowPathMatching      bool\n\tmaxConsecutiveFailures int\n\tonThreadReady          func(int)\n\tonThreadShutdown       func(int)\n\tqueuedRequests         atomic.Int32\n}\n\nvar (\n\tworkers          []*worker\n\tworkersByName    map[string]*worker\n\tworkersByPath    map[string]*worker\n\twatcherIsEnabled bool\n\tstartupFailChan  chan error\n)\n\nfunc initWorkers(opt []workerOpt) error {\n\tif len(opt) == 0 {\n\t\treturn nil\n\t}\n\n\tvar (\n\t\tworkersReady        sync.WaitGroup\n\t\ttotalThreadsToStart int\n\t)\n\n\tworkers = make([]*worker, 0, len(opt))\n\tworkersByName = make(map[string]*worker, len(opt))\n\tworkersByPath = make(map[string]*worker, len(opt))\n\n\tfor _, o := range opt {\n\t\tw, err := newWorker(o)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\ttotalThreadsToStart += w.num\n\t\tworkers = append(workers, w)\n\t\tworkersByName[w.name] = w\n\t\tif w.allowPathMatching {\n\t\t\tworkersByPath[w.fileName] = w\n\t\t}\n\t}\n\n\tstartupFailChan = make(chan error, totalThreadsToStart)\n\n\tfor _, w := range workers {\n\t\tfor i := 0; i < w.num; i++ {\n\t\t\tthread := getInactivePHPThread()\n\t\t\tconvertToWorkerThread(thread, w)\n\n\t\t\tworkersReady.Go(func() {\n\t\t\t\tthread.state.WaitFor(state.Ready, state.ShuttingDown, state.Done)\n\t\t\t})\n\t\t}\n\t}\n\n\tworkersReady.Wait()\n\n\tselect {\n\tcase err := <-startupFailChan:\n\t\t// at least 1 worker has failed, return an error\n\t\treturn fmt.Errorf(\"failed to initialize workers: %w\", err)\n\tdefault:\n\t\t// all workers started successfully\n\t\tstartupFailChan = nil\n\t}\n\n\treturn nil\n}\n\nfunc newWorker(o workerOpt) (*worker, error) {\n\t// Order is important!\n\t// This order ensures that FrankenPHP started from inside a symlinked directory will properly resolve any paths.\n\t// If it is started from outside a symlinked directory, it is resolved to the same path that we use in the Caddy module.\n\tabsFileName, err := filepath.EvalSymlinks(filepath.FromSlash(o.fileName))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"worker filename is invalid %q: %w\", o.fileName, err)\n\t}\n\n\tabsFileName, err = fastabs.FastAbs(absFileName)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"worker filename is invalid %q: %w\", o.fileName, err)\n\t}\n\n\tif _, err := os.Stat(absFileName); err != nil {\n\t\treturn nil, fmt.Errorf(\"worker file not found %q: %w\", absFileName, err)\n\t}\n\n\tif o.name == \"\" {\n\t\to.name = absFileName\n\t}\n\n\t// workers that have a name starting with \"m#\" are module workers\n\t// they can only be matched by their name, not by their path\n\tallowPathMatching := !strings.HasPrefix(o.name, \"m#\")\n\n\tif w := workersByPath[absFileName]; w != nil && allowPathMatching {\n\t\treturn w, fmt.Errorf(\"two workers cannot have the same filename: %q\", absFileName)\n\t}\n\tif w := workersByName[o.name]; w != nil {\n\t\treturn w, fmt.Errorf(\"two workers cannot have the same name: %q\", o.name)\n\t}\n\n\tif o.env == nil {\n\t\to.env = make(PreparedEnv, 1)\n\t}\n\n\to.env[\"FRANKENPHP_WORKER\\x00\"] = \"1\"\n\tw := &worker{\n\t\tname:                   o.name,\n\t\tfileName:               absFileName,\n\t\trequestOptions:         o.requestOptions,\n\t\tnum:                    o.num,\n\t\tmaxThreads:             o.maxThreads,\n\t\trequestChan:            make(chan contextHolder),\n\t\tthreads:                make([]*phpThread, 0, o.num),\n\t\tallowPathMatching:      allowPathMatching,\n\t\tmaxConsecutiveFailures: o.maxConsecutiveFailures,\n\t\tonThreadReady:          o.onThreadReady,\n\t\tonThreadShutdown:       o.onThreadShutdown,\n\t}\n\n\tw.configureMercure(&o)\n\n\tw.requestOptions = append(\n\t\tw.requestOptions,\n\t\tWithRequestDocumentRoot(filepath.Dir(o.fileName), false),\n\t\tWithRequestPreparedEnv(o.env),\n\t)\n\n\tif o.extensionWorkers != nil {\n\t\to.extensionWorkers.internalWorker = w\n\t}\n\n\treturn w, nil\n}\n\n// EXPERIMENTAL: DrainWorkers finishes all worker scripts before a graceful shutdown\nfunc DrainWorkers() {\n\t_ = drainWorkerThreads()\n}\n\nfunc drainWorkerThreads() []*phpThread {\n\tvar (\n\t\tready          sync.WaitGroup\n\t\tdrainedThreads []*phpThread\n\t)\n\n\tfor _, worker := range workers {\n\t\tworker.threadMutex.RLock()\n\t\tready.Add(len(worker.threads))\n\n\t\tfor _, thread := range worker.threads {\n\t\t\tif !thread.state.RequestSafeStateChange(state.Restarting) {\n\t\t\t\tready.Done()\n\n\t\t\t\t// no state change allowed == thread is shutting down\n\t\t\t\t// we'll proceed to restart all other threads anyway\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tclose(thread.drainChan)\n\t\t\tdrainedThreads = append(drainedThreads, thread)\n\n\t\t\tgo func(thread *phpThread) {\n\t\t\t\tthread.state.WaitFor(state.Yielding)\n\t\t\t\tready.Done()\n\t\t\t}(thread)\n\t\t}\n\n\t\tworker.threadMutex.RUnlock()\n\t}\n\n\tready.Wait()\n\n\treturn drainedThreads\n}\n\n// RestartWorkers attempts to restart all workers gracefully\n// All workers must be restarted at the same time to prevent issues with opcache resetting.\nfunc RestartWorkers() {\n\t// disallow scaling threads while restarting workers\n\tscalingMu.Lock()\n\tdefer scalingMu.Unlock()\n\n\tthreadsToRestart := drainWorkerThreads()\n\n\tfor _, thread := range threadsToRestart {\n\t\tthread.drainChan = make(chan struct{})\n\t\tthread.state.Set(state.Ready)\n\t}\n}\n\nfunc (worker *worker) attachThread(thread *phpThread) {\n\tworker.threadMutex.Lock()\n\tworker.threads = append(worker.threads, thread)\n\tworker.threadMutex.Unlock()\n}\n\nfunc (worker *worker) detachThread(thread *phpThread) {\n\tworker.threadMutex.Lock()\n\tfor i, t := range worker.threads {\n\t\tif t == thread {\n\t\t\tworker.threads = append(worker.threads[:i], worker.threads[i+1:]...)\n\t\t\tbreak\n\t\t}\n\t}\n\tworker.threadMutex.Unlock()\n}\n\nfunc (worker *worker) countThreads() int {\n\tworker.threadMutex.RLock()\n\tl := len(worker.threads)\n\tworker.threadMutex.RUnlock()\n\n\treturn l\n}\n\n// check if max_threads has been reached\nfunc (worker *worker) isAtThreadLimit() bool {\n\tif worker.maxThreads <= 0 {\n\t\treturn false\n\t}\n\n\tworker.threadMutex.RLock()\n\tatMaxThreads := len(worker.threads) >= worker.maxThreads\n\tworker.threadMutex.RUnlock()\n\n\treturn atMaxThreads\n}\n\nfunc (worker *worker) handleRequest(ch contextHolder) error {\n\tmetrics.StartWorkerRequest(worker.name)\n\n\truntime.Gosched()\n\n\tif worker.queuedRequests.Load() == 0 {\n\t\t// dispatch requests to all worker threads in order\n\t\tworker.threadMutex.RLock()\n\t\tfor _, thread := range worker.threads {\n\t\t\tselect {\n\t\t\tcase thread.requestChan <- ch:\n\t\t\t\tworker.threadMutex.RUnlock()\n\t\t\t\t<-ch.frankenPHPContext.done\n\t\t\t\tmetrics.StopWorkerRequest(worker.name, time.Since(ch.frankenPHPContext.startedAt))\n\n\t\t\t\treturn nil\n\t\t\tdefault:\n\t\t\t\t// thread is busy, continue\n\t\t\t}\n\t\t}\n\t\tworker.threadMutex.RUnlock()\n\t}\n\n\t// if no thread was available, mark the request as queued and apply the scaling strategy\n\tworker.queuedRequests.Add(1)\n\tmetrics.QueuedWorkerRequest(worker.name)\n\n\tfor {\n\t\tworkerScaleChan := scaleChan\n\t\tif worker.isAtThreadLimit() {\n\t\t\tworkerScaleChan = nil // max_threads for this worker reached, do not attempt scaling\n\t\t}\n\n\t\tselect {\n\t\tcase worker.requestChan <- ch:\n\t\t\tworker.queuedRequests.Add(-1)\n\t\t\tmetrics.DequeuedWorkerRequest(worker.name)\n\t\t\t<-ch.frankenPHPContext.done\n\t\t\tmetrics.StopWorkerRequest(worker.name, time.Since(ch.frankenPHPContext.startedAt))\n\n\t\t\treturn nil\n\t\tcase workerScaleChan <- ch.frankenPHPContext:\n\t\t\t// the request has triggered scaling, continue to wait for a thread\n\t\tcase <-timeoutChan(maxWaitTime):\n\t\t\t// the request has timed out stalling\n\t\t\tworker.queuedRequests.Add(-1)\n\t\t\tmetrics.DequeuedWorkerRequest(worker.name)\n\t\t\tmetrics.StopWorkerRequest(worker.name, time.Since(ch.frankenPHPContext.startedAt))\n\n\t\t\tch.frankenPHPContext.reject(ErrMaxWaitTimeExceeded)\n\n\t\t\treturn ErrMaxWaitTimeExceeded\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "worker_test.go",
    "content": "package frankenphp_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/dunglas/frankenphp\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestWorker(t *testing.T) {\n\trunTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {\n\t\tformData := url.Values{\"baz\": {\"bat\"}}\n\t\treq := httptest.NewRequest(\"POST\", \"http://example.com/worker.php?foo=bar\", strings.NewReader(formData.Encode()))\n\t\treq.Header.Set(\"Content-Type\", strings.Clone(\"application/x-www-form-urlencoded\"))\n\t\tw := httptest.NewRecorder()\n\t\thandler(w, req)\n\n\t\tresp := w.Result()\n\t\tbody, _ := io.ReadAll(resp.Body)\n\n\t\tassert.Contains(t, string(body), fmt.Sprintf(\"Requests handled: %d\", i*2))\n\n\t\tformData2 := url.Values{\"baz2\": {\"bat2\"}}\n\t\treq2 := httptest.NewRequest(\"POST\", \"http://example.com/worker.php?foo2=bar2\", strings.NewReader(formData2.Encode()))\n\t\treq2.Header.Set(\"Content-Type\", strings.Clone(\"application/x-www-form-urlencoded\"))\n\n\t\tw2 := httptest.NewRecorder()\n\t\thandler(w2, req2)\n\n\t\tresp2 := w2.Result()\n\t\tbody2, _ := io.ReadAll(resp2.Body)\n\n\t\tassert.Contains(t, string(body2), fmt.Sprintf(\"Requests handled: %d\", i*2+1))\n\t}, &testOptions{workerScript: \"worker.php\", nbWorkers: 1, nbParallelRequests: 1})\n}\n\nfunc TestWorkerDie(t *testing.T) {\n\trunTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {\n\t\treq := httptest.NewRequest(\"GET\", \"http://example.com/die.php\", nil)\n\t\tw := httptest.NewRecorder()\n\t\thandler(w, req)\n\t}, &testOptions{workerScript: \"die.php\", nbWorkers: 1, nbParallelRequests: 10})\n}\n\nfunc TestNonWorkerModeAlwaysWorks(t *testing.T) {\n\trunTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {\n\t\treq := httptest.NewRequest(\"GET\", \"http://example.com/index.php\", nil)\n\t\tw := httptest.NewRecorder()\n\t\thandler(w, req)\n\n\t\tresp := w.Result()\n\t\tbody, _ := io.ReadAll(resp.Body)\n\n\t\tassert.Contains(t, string(body), \"I am by birth a Genevese\")\n\t}, &testOptions{workerScript: \"phpinfo.php\"})\n}\n\nfunc TestCannotCallHandleRequestInNonWorkerMode(t *testing.T) {\n\trunTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {\n\t\treq := httptest.NewRequest(\"GET\", \"http://example.com/non-worker.php\", nil)\n\t\tw := httptest.NewRecorder()\n\t\thandler(w, req)\n\n\t\tresp := w.Result()\n\t\tbody, _ := io.ReadAll(resp.Body)\n\n\t\tassert.Contains(t, string(body), \"<b>Fatal error</b>:  Uncaught RuntimeException: frankenphp_handle_request() called while not in worker mode\")\n\t}, nil)\n}\n\nfunc TestWorkerEnv(t *testing.T) {\n\trunTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {\n\t\treq := httptest.NewRequest(\"GET\", fmt.Sprintf(\"http://example.com/worker-env.php?i=%d\", i), nil)\n\t\tw := httptest.NewRecorder()\n\t\thandler(w, req)\n\n\t\tresp := w.Result()\n\t\tbody, _ := io.ReadAll(resp.Body)\n\n\t\tassert.Equal(t, fmt.Sprintf(\"bar%d\", i), string(body))\n\t}, &testOptions{workerScript: \"worker-env.php\", nbWorkers: 1, env: map[string]string{\"FOO\": \"bar\"}, nbParallelRequests: 10})\n}\n\nfunc TestWorkerGetOpt(t *testing.T) {\n\tlogger, buf := newTestLogger(t)\n\n\trunTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {\n\t\treq := httptest.NewRequest(\"GET\", fmt.Sprintf(\"http://example.com/worker-getopt.php?i=%d\", i), nil)\n\t\treq.Header.Add(\"Request\", strconv.Itoa(i))\n\t\tw := httptest.NewRecorder()\n\n\t\thandler(w, req)\n\n\t\tresp := w.Result()\n\t\tbody, _ := io.ReadAll(resp.Body)\n\n\t\tassert.Contains(t, string(body), fmt.Sprintf(\"[HTTP_REQUEST] => %d\", i))\n\t\tassert.Contains(t, string(body), fmt.Sprintf(\"[REQUEST_URI] => /worker-getopt.php?i=%d\", i))\n\t}, &testOptions{logger: logger, workerScript: \"worker-getopt.php\", env: map[string]string{\"FOO\": \"bar\"}})\n\n\tassert.NotRegexp(t, buf.String(), \"exit_status=[1-9]\")\n}\n\nfunc ExampleServeHTTP_workers() {\n\tif err := frankenphp.Init(\n\t\tfrankenphp.WithWorkers(\"worker1\", \"worker1.php\", 4,\n\t\t\tfrankenphp.WithWorkerEnv(map[string]string{\"ENV1\": \"foo\"}),\n\t\t\tfrankenphp.WithWorkerWatchMode([]string{}),\n\t\t\tfrankenphp.WithWorkerMaxFailures(0),\n\t\t),\n\t\tfrankenphp.WithWorkers(\"worker2\", \"worker2.php\", 2,\n\t\t\tfrankenphp.WithWorkerEnv(map[string]string{\"ENV2\": \"bar\"}),\n\t\t\tfrankenphp.WithWorkerWatchMode([]string{}),\n\t\t\tfrankenphp.WithWorkerMaxFailures(0),\n\t\t),\n\t); err != nil {\n\t\tpanic(err)\n\t}\n\tdefer frankenphp.Shutdown()\n\n\thttp.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) {\n\t\treq, err := frankenphp.NewRequestWithContext(r, frankenphp.WithRequestDocumentRoot(\"/path/to/document/root\", false))\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\n\t\tif err := frankenphp.ServeHTTP(w, req); err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t})\n\tlog.Fatal(http.ListenAndServe(\":8080\", nil))\n}\n\nfunc TestWorkerHasOSEnvironmentVariableInSERVER(t *testing.T) {\n\trunTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {\n\t\treq := httptest.NewRequest(\"GET\", \"http://example.com/worker.php\", nil)\n\t\tw := httptest.NewRecorder()\n\t\thandler(w, req)\n\n\t\tresp := w.Result()\n\t\tbody, _ := io.ReadAll(resp.Body)\n\n\t\tassert.Contains(t, string(body), \"CUSTOM_OS_ENV_VARIABLE\")\n\t\tassert.Contains(t, string(body), \"custom_env_variable_value\")\n\t}, &testOptions{workerScript: \"worker.php\", nbWorkers: 1, nbParallelRequests: 1})\n}\n\nfunc TestKeepRunningOnConnectionAbort(t *testing.T) {\n\trunTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {\n\t\treq := httptest.NewRequest(\"GET\", \"http://example.com/worker-with-counter.php\", nil)\n\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\treq = req.WithContext(ctx)\n\t\tcancel()\n\t\tbody1, _ := testRequest(req, handler, t)\n\n\t\tassert.Equal(t, \"requests:1\", body1, \"should have handled exactly one request\")\n\t\tbody2, _ := testGet(\"http://example.com/worker-with-counter.php\", handler, t)\n\n\t\tassert.Equal(t, \"requests:2\", body2, \"should not have stopped execution after the first request was aborted\")\n\t}, &testOptions{workerScript: \"worker-with-counter.php\", nbWorkers: 1, nbParallelRequests: 1})\n}\n"
  },
  {
    "path": "workerextension.go",
    "content": "package frankenphp\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\n// EXPERIMENTAL: Workers allows you to register a worker.\ntype Workers interface {\n\t// SendRequest calls the closure passed to frankenphp_handle_request() and updates the PHP context .\n\t// The generated HTTP response will be written through the provided writer.\n\tSendRequest(rw http.ResponseWriter, r *http.Request) error\n\t// SendMessage calls the closure passed to frankenphp_handle_request(), passes message as a parameter, and returns the value produced by the closure.\n\tSendMessage(ctx context.Context, message any, rw http.ResponseWriter) (any, error)\n\t// NumThreads returns the number of available threads.\n\tNumThreads() int\n}\n\ntype extensionWorkers struct {\n\tname           string\n\tfileName       string\n\tnum            int\n\toptions        []WorkerOption\n\tinternalWorker *worker\n}\n\n// EXPERIMENTAL: SendRequest sends an HTTP request to the worker and writes the response to the provided ResponseWriter.\nfunc (w *extensionWorkers) SendRequest(rw http.ResponseWriter, r *http.Request) error {\n\tfr, err := NewRequestWithContext(\n\t\tr,\n\t\tWithOriginalRequest(r),\n\t\tWithWorkerName(w.name),\n\t)\n\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn ServeHTTP(rw, fr)\n}\n\nfunc (w *extensionWorkers) NumThreads() int {\n\treturn w.internalWorker.countThreads()\n}\n\n// EXPERIMENTAL: SendMessage sends a message to the worker and waits for a response.\nfunc (w *extensionWorkers) SendMessage(ctx context.Context, message any, rw http.ResponseWriter) (any, error) {\n\tfc := newFrankenPHPContext()\n\tfc.logger = globalLogger\n\tfc.worker = w.internalWorker\n\tfc.responseWriter = rw\n\tfc.handlerParameters = message\n\n\terr := w.internalWorker.handleRequest(contextHolder{context.WithValue(ctx, contextKey, fc), fc})\n\n\treturn fc.handlerReturn, err\n}\n"
  },
  {
    "path": "workerextension_test.go",
    "content": "package frankenphp\n\nimport (\n\t\"io\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestWorkersExtension(t *testing.T) {\n\tt.Cleanup(Shutdown)\n\n\treadyWorkers := 0\n\tshutdownWorkers := 0\n\tserverStarts := 0\n\tserverShutDowns := 0\n\n\texternalWorkers, o := WithExtensionWorkers(\n\t\t\"extensionWorkers\",\n\t\t\"testdata/worker.php\",\n\t\t1,\n\t\tWithWorkerOnReady(func(id int) {\n\t\t\treadyWorkers++\n\t\t}),\n\t\tWithWorkerOnShutdown(func(id int) {\n\t\t\tserverShutDowns++\n\t\t}),\n\t\tWithWorkerOnServerStartup(func() {\n\t\t\tserverStarts++\n\t\t}),\n\t\tWithWorkerOnServerShutdown(func() {\n\t\t\tshutdownWorkers++\n\t\t}),\n\t)\n\n\trequire.NoError(t, Init(o))\n\tt.Cleanup(func() {\n\t\tShutdown()\n\t\tassert.Equal(t, 1, shutdownWorkers, \"Worker shutdown hook should have been called\")\n\t\tassert.Equal(t, 1, serverShutDowns, \"Server shutdown hook should have been called\")\n\t})\n\n\tassert.Equal(t, readyWorkers, 1, \"Worker thread should have called onReady()\")\n\tassert.Equal(t, serverStarts, 1, \"Server start hook should have been called\")\n\tassert.Equal(t, externalWorkers.NumThreads(), 1, \"NumThreads() should report 1 thread\")\n\n\t// Create a test request\n\treq := httptest.NewRequest(\"GET\", \"https://example.com/test/?foo=bar\", nil)\n\treq.Header.Set(\"X-Test-Header\", \"test-value\")\n\tw := httptest.NewRecorder()\n\n\t// Inject the request into the worker through the extension\n\terr := externalWorkers.SendRequest(w, req)\n\tassert.NoError(t, err, \"Sending request should not produce an error\")\n\n\tresp := w.Result()\n\tbody, _ := io.ReadAll(resp.Body)\n\n\t// The worker.php script should output information about the request\n\t// We're just checking that we got a response, not the specific content\n\tassert.NotEmpty(t, body, \"Response body should not be empty\")\n\tassert.Contains(t, string(body), \"Requests handled: 0\", \"Response body should contain request information\")\n}\n\nfunc TestWorkerExtensionSendMessage(t *testing.T) {\n\texternalWorker, o := WithExtensionWorkers(\"extensionWorkers\", \"testdata/message-worker.php\", 1)\n\n\terr := Init(o)\n\trequire.NoError(t, err)\n\tt.Cleanup(Shutdown)\n\n\tret, err := externalWorker.SendMessage(t.Context(), \"Hello Workers\", nil)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, \"received message: Hello Workers\", ret)\n}\n\nfunc TestErrorIf2WorkersHaveSameName(t *testing.T) {\n\t_, o1 := WithExtensionWorkers(\"duplicateWorker\", \"testdata/worker.php\", 1)\n\t_, o2 := WithExtensionWorkers(\"duplicateWorker\", \"testdata/worker2.php\", 1)\n\n\tt.Cleanup(Shutdown)\n\trequire.Error(t, Init(o1, o2))\n}\n"
  },
  {
    "path": "zizmor.yaml",
    "content": "---\nrules:\n  unpinned-uses:\n    config:\n      policies:\n        \"*\": ref-pin\n"
  }
]