Compare commits

...

112 Commits

Author SHA1 Message Date
dependabot[bot]
6e6f665d82 ci: bump the github-actions group with 3 updates
Bumps the github-actions group with 3 updates: [actions/upload-artifact](https://github.com/actions/upload-artifact), [actions/download-artifact](https://github.com/actions/download-artifact) and [actions/cache](https://github.com/actions/cache).


Updates `actions/upload-artifact` from 5 to 6
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v5...v6)

Updates `actions/download-artifact` from 6 to 7
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v6...v7)

Updates `actions/cache` from 4 to 5
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: actions/download-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: actions/cache
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-22 11:07:22 +00:00
Kévin Dunglas
57c58faf1c chore: prepare release 1.11.1 2025-12-20 09:16:23 +01:00
Loric Brevet
25d9cb9600 fix: crash when using the logger outside of the a request context 2025-12-20 09:15:29 +01:00
Kévin Dunglas
4092ecb5b5 fix: frankenphp_log() level parameter must be optional 2025-12-19 16:25:32 +01:00
Kévin Dunglas
75ccccf1b2 fix(caddy): use default patterns when hot_reload is alone 2025-12-19 09:38:05 +01:00
Kévin Dunglas
6231bf4a1c chore: prepare release 1.11.0 2025-12-18 16:51:41 +01:00
Kévin Dunglas
e01e40fd97 chore: bump deps (#2078) 2025-12-17 11:47:14 +01:00
Alexander Stecher
175e644d10 feat: multiple curly braces for watcher (#2068)
Allows doing something like this:

```caddyfile
watch "/app/{config,src}/*.{php,js}"
```

In the long term it would be nice to have pattern matching in the
watcher repo itself
2025-12-17 00:22:28 +01:00
Kévin Dunglas
a8f75d0eef ci: verbose logs for StaticPHP (#2074) 2025-12-15 20:13:15 +01:00
Raphael Coeffic
91c553f3d9 feat: add support for structured logging with the frankenphp_log() PHP function (#1979)
As discussed in https://github.com/php/frankenphp/discussions/1961,
there is no real way to pass a severity/level to any log handler offered
by PHP that would make it to the FrankenPHP layer. This new function
allows applications embedding FrankenPHP to integrate PHP logging into
the application itself, thus offering a more streamlined experience.

---------

Co-authored-by: Quentin Burgess <qutn.burgess@gmail.com>
Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
2025-12-15 16:10:35 +01:00
Alexandre Daubois
7fca07ed67 feat(types): expose IsPacked to help dealing with hashmaps and lists in Go code 2025-12-15 15:35:21 +01:00
dependabot[bot]
3599299cde chore(caddy): bump github.com/spf13/cobra
Bumps the go-modules group in /caddy with 1 update: [github.com/spf13/cobra](https://github.com/spf13/cobra).


Updates `github.com/spf13/cobra` from 1.10.1 to 1.10.2
- [Release notes](https://github.com/spf13/cobra/releases)
- [Commits](https://github.com/spf13/cobra/compare/v1.10.1...v1.10.2)

---
updated-dependencies:
- dependency-name: github.com/spf13/cobra
  dependency-version: 1.10.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: go-modules
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-15 13:41:56 +01:00
Alexandre Daubois
bb1c3678dc feat(extgen): add support for callable in parameters (#1731) 2025-12-15 12:50:50 +01:00
dependabot[bot]
58a63703b4 ci: bump actions/checkout from 5 to 6 in the github-actions group
Bumps the github-actions group with 1 update: [actions/checkout](https://github.com/actions/checkout).


Updates `actions/checkout` from 5 to 6
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-15 12:23:53 +01:00
Alexandre Daubois
694ab86cef doc(runtime): mention Symfony 7.4 native support for worker mode (#1668)
Fixes https://github.com/symfony/symfony-docs/issues/21099, related to
https://github.com/symfony/symfony/pull/60503
2025-12-14 17:06:28 +01:00
Marc
e23e0c571e update config doc for new deb/rpm packages (#2071) 2025-12-13 19:32:27 +01:00
Kévin Dunglas
f02e6f2f85 fix: update mercure_publish() to use the new GoPackedArray() API 2025-12-13 19:30:22 +01:00
Alexander Stecher
11213fd1de fix: returns a zend_array directly in types.go (#1894) 2025-12-12 22:55:58 +01:00
Alexandre Daubois
41da660088 fix(hot-reload): fix import (#2069) 2025-12-12 15:23:55 +01:00
Alexandre Daubois
599c92b15d tests(extgen): add integration tests (#1984)
Fix #1975
2025-12-12 14:32:00 +01:00
Kévin Dunglas
225ca409d3 feat: hot reload (#2031)
This patch brings hot reloading capabilities to PHP apps: in
development, the browser will automatically refresh the page when any
source file changes!
It's similar to HMR in JavaScript.

It is built on top of [the watcher
mechanism](https://frankenphp.dev/docs/config/#watching-for-file-changes)
and of the [Mercure](https://frankenphp.dev/docs/mercure/) integration.

Each time a watched file is modified, a Mercure update is sent, giving
the ability to the client to reload the page, or part of the page
(assets, images...).

Here is an example implementation:

```caddyfile
root ./public


mercure {
      subscriber_jwt {env.MERCURE_SUBSCRIBER_JWT_KEY}
      anonymous
}

php_server {
      hot_reload
}
```

```php
<?php
header('Content-Type: text/html');
?>
<!DOCTYPE html>
<html lang="en">
<head>
<title>Test</title>
<script>
    const es = new EventSource('<?=$_SERVER['FRANKENPHP_HOT_RELOAD']?>');
    es.onmessage = () => location.reload();
</script>
</head>
<body>
Hello
```

I plan to create a helper JS library to handle more advanced cases
(reloading CSS, JS, etc), similar to [HotWire
Spark](https://github.com/hotwired/spark). Be sure to attend my
SymfonyCon to learn more!

There is still room for improvement:

- Provide an option to only trigger the update without reloading the
worker for some files (ex, images, JS, CSS...)
- Support classic mode (currently, only the worker mode is supported)
- Don't reload all workers when only the files used by one change

However, this PR is working as-is and can be merged as a first step.

This patch heavily refactors the watcher module. Maybe it will be
possible to extract it as a standalone library at some point (would be
useful to add a similar feature but not tight to PHP as a Caddy module).

---------

Signed-off-by: Kévin Dunglas <kevin@dunglas.fr>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-12 14:29:18 +01:00
Francis Lavoie
d2007620a4 docs: Fix file extension in FrankenPHP configuration (#2067)
Corrected the file extension for additional configuration files in
FrankenPHP section.

---------

Signed-off-by: Francis Lavoie <lavofr@gmail.com>
2025-12-11 21:40:34 +01:00
Kévin Dunglas
4ac024a1d0 fix: remove deprecated Mercure "transport_url" directive from Caddyfile 2025-12-10 15:40:21 +01:00
Kacper Rowiński
e0dcf42852 chore: bump github.com/smallstep/certificates/ from 0.28.4 to 0.29.0 2025-12-09 11:40:58 +01:00
Arkeins
7b8cf6b127 docs: wrong config path for autoloaded .caddyfile (#2059)
Hi !

I am playing with FrankenPHP, and frankly new to the thing so I may be
wrong.

The [current documentation](https://frankenphp.dev/docs/config/#docker)
mention that :

> /etc/frankenphp/caddy.d/*.caddy: additional configuration files that
are loaded automatically

But in the main Caddyfile ([here on
GitHub](https://github.com/php/frankenphp/blob/main/caddy/frankenphp/Caddyfile#L59))
imports differently:

```ini
import Caddyfile.d/*.caddyfile
```

This PR aims to correct this :)

Thank you for your time !

Signed-off-by: Arkeins <7311955+Arkeins@users.noreply.github.com>
2025-12-03 18:20:09 +01:00
Alexander Stecher
98573ed7c0 refactor: extract the state module and make the backoff error instead of panic
This PR:
- moves state.go to its own module
- moves the phpheaders test the phpheaders module
- simplifies backoff.go
- makes the backoff error instead of panic (so it can be tested)
- removes some unused C structs
2025-12-02 23:10:12 +01:00
Kévin Dunglas
16e2bbb969 tests: improve benchmarks 2025-12-02 17:13:38 +01:00
Kévin Dunglas
816bcc2ad6 chore: make super-linter green (#2051) 2025-12-01 15:52:08 +01:00
Alexandre Daubois
1fbabf91c9 fix(extgen): use RETURN_EMPTY_STRING() when returning empty string (#2049)
One last bug spotted by #1984, empty strings should be returned using
`RETURN_EMPTY_STRING()` or it may segfault.
2025-12-01 15:43:45 +01:00
Alexandre Daubois
2fa7663d3b fix(extgen): use REGISTER(_NS)_BOOL_CONSTANT (#2047)
Spotted in #1984, this is the right macros to declare boolean constants
2025-12-01 15:35:34 +01:00
Kévin Dunglas
b1bdce359b tests: simplify benchmarks code 2025-12-01 15:34:04 +01:00
Max
c9ad9fc55a headerKeyCache: use otter v2 (#2040)
Benchmarks show that version 1, while extremely fast with hot keys,
becomes several times slower than no‑cache under frequent misses.
Version 2 delivers consistently better performance across all scenarios,
with no allocations and stable latency.

```
BenchmarkGetUnCommonHeaderNoCacheSequential-12                           7545640               169.4 ns/op            72 B/op          4 allocs/op
BenchmarkGetUnCommonHeaderV2Sequential-12                               14471982                85.98 ns/op            0 B/op          0 allocs/op
BenchmarkGetUnCommonHeaderV1Sequential-12                               19748048                59.63 ns/op            0 B/op          0 allocs/op

BenchmarkGetUnCommonHeaderNoCacheParallelOneKey-12                      24352088                44.47 ns/op           72 B/op          4 allocs/op
BenchmarkGetUnCommonHeaderV2ParallelOneKey-12                           91024160                11.76 ns/op            0 B/op          0 allocs/op
BenchmarkGetUnCommonHeaderV1ParallelOneKey-12                           192048842                6.186 ns/op           0 B/op          0 allocs/op

BenchmarkGetUnCommonHeaderNoCacheParallelRandomMaximumSize-12           26261611                43.07 ns/op           62 B/op          3 allocs/op
BenchmarkGetUnCommonHeaderV2ParallelRandomMaximumSize-12                100000000               14.43 ns/op            0 B/op          0 allocs/op
BenchmarkGetUnCommonHeaderV1ParallelRandomMaximumSize-12                137813384                8.965 ns/op           0 B/op          0 allocs/op

BenchmarkGetUnCommonHeaderNoCacheParallelRandomLenKeys-12               24224664                46.57 ns/op           71 B/op          3 allocs/op
BenchmarkGetUnCommonHeaderV2ParallelRandomLenKeys-12                    69002575                17.42 ns/op            0 B/op          0 allocs/op
BenchmarkGetUnCommonHeaderV1ParallelRandomLenKeys-12                     8498404               253.1 ns/op            42 B/op          1 allocs/op
```

---------

Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
2025-12-01 11:37:40 +01:00
Marc
12d4c3d09b [docs] update other languages based on English (#2044)
They were a little out of date, making use of AI to update them to the
same format.
2025-12-01 09:08:53 +01:00
Marc
7fceb32f7b give /var/lib/frankenphp sys_rw_content_t permissions for mercure.db files (#2037)
The current configuration is not able to start FrankenPHP when mercure
and SELinux are used with a Caddyfile like this:

```Caddyfile
mercure {
    transport bolt {
        path mercure.db
    }
}
```

closes https://github.com/php/frankenphp/issues/2035

Exact error:
```
SELinux is preventing /usr/bin/frankenphp from map access on the file /var/lib/frankenphp/mercure.db.

*****  Plugin catchall_boolean (89.3 confidence) suggests   ******************

If you want to allow domain to can mmap files
Then you must tell SELinux about this by enabling the 'domain_can_mmap_files' boolean.

Do
setsebool -P domain_can_mmap_files 1

*****  Plugin catchall (11.6 confidence) suggests   **************************

If you believe that frankenphp should be allowed map access on the mercure.db file by default.
Then you should report this as a bug.
You can generate a local policy module to allow this access.
Do
allow this access for now by executing:
# ausearch -c 'frankenphp' --raw | audit2allow -M my-frankenphp
# semodule -X 300 -i my-frankenphp.pp


Additional Information:
Source Context                system_u:system_r:httpd_t:s0
Target Context                system_u:object_r:httpd_var_lib_t:s0
Target Objects                /var/lib/frankenphp/mercure.db [ file ]
Source                        frankenphp
Source Path                   /usr/bin/frankenphp
Port                          <Unknown>
Host                          localhost
Source RPM Packages           frankenphp-1.10.0_84-1.x86_64
Target RPM Packages
SELinux Policy RPM            selinux-policy-targeted-3.14.3-139.el8_10.1.noarch
Local Policy RPM              selinux-policy-targeted-3.14.3-139.el8_10.1.noarch
Selinux Enabled               True
Policy Type                   targeted
Enforcing Mode                Enforcing
Host Name                     localhost
Platform                      Linux localhost
                              4.18.0-553.81.1.el8_10.x86_64 #1 SMP Mon Oct 27
                              11:29:19 EDT 2025 x86_64 x86_64
Alert Count                   12
First Seen                    2025-10-29 17:25:26 CET
Last Seen                     2025-11-25 17:18:19 CET
Local ID                      c4e79504-117e-4e9f-ad8c-f0bcc4856697

Raw Audit Messages
type=AVC msg=audit(1764087499.320:475517): avc:  denied  { map } for  pid=322613 comm="frankenphp" path="/var/lib/frankenphp/mercure.db" dev="md3" ino=93716492 scontext=system_u:system_r:httpd_t:s0 tcontext=system_u:object_r:httpd_var_lib_t:s0 tclass=file permissive=0


type=SYSCALL msg=audit(1764087499.320:475517): arch=x86_64 syscall=mmap success=no exit=EACCES a0=0 a1=8000 a2=1 a3=1 items=0 ppid=1 pid=322613 auid=4294967295 uid=991 gid=988 euid=991 suid=991 fsuid=991 egid=988 sgid=988 fsgid=988 tty=(none) ses=4294967295 comm=frankenphp exe=/usr/bin/frankenphp subj=system_u:system_r:httpd_t:s0 key=(null)

Hash: frankenphp,httpd_t,httpd_var_lib_t,file,map
```
2025-11-28 11:11:28 +01:00
Marc
1b30905c26 fox(static): add watcher to defaultExtensionLibs (#2039)
fix https://github.com/php/frankenphp/issues/2038
2025-11-27 00:10:41 +01:00
Alexander Stecher
dadeb5a628 perf: tail latency with goSched (#2033)
Alternate implementation to #2016 that doesn't reduce RPS with lower
amounts of threads
2025-11-26 18:33:07 +01:00
Marc
abaf03c7f7 deduplicate installation instructions in README(#2013) 2025-11-26 08:19:27 +01:00
Kévin Dunglas
fc5f6ef092 chore: prepare release 1.10.1 2025-11-25 10:54:34 +01:00
Marc
65111334a1 docs: update issue template to differentiate between deb and RPM packages 2025-11-25 10:50:38 +01:00
dependabot[bot]
6747aaae2d ci: bump actions/checkout from 5 to 6 in the github-actions group
Bumps the github-actions group with 1 update: [actions/checkout](https://github.com/actions/checkout).


Updates `actions/checkout` from 5 to 6
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-25 10:49:11 +01:00
Kévin Dunglas
6c764ad9c5 fix: correctly set the Mercure hub for the main worker request 2025-11-24 11:21:45 +01:00
Kévin Dunglas
e6b3f70d91 chore: bump deps 2025-11-23 23:13:12 +01:00
Kévin Dunglas
911e6d156b fix: crash when a string is passed for the topics parameter of the mercure_publish() function (#2021) 2025-11-23 17:03:43 +01:00
Kévin Dunglas
c6cadf3bf6 chore: prepare release 1.10.0 2025-11-21 16:16:01 +01:00
Marc
f28f6e8d03 docs: update docs for rpm packages and extension availability (#1988)
continues https://github.com/php/frankenphp/pull/1756

---------

Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
2025-11-21 14:31:43 +01:00
Antonin CLAUZIER
01beb66573 ci: PHP 8.5 (#2006)
Co-authored-by: Marc <m@pyc.ac>
Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
2025-11-21 13:54:07 +01:00
Kévin Dunglas
41e0713a1b fix: allow null for mercure_publish() retry parameter 2025-11-21 13:52:55 +01:00
Kévin Dunglas
bbfb1b0a0e ci: upgrade macOS runners 2025-11-21 13:51:15 +01:00
Marc
49e98cc8d6 delete source/downloads after building in script, add .editorconfig (#2000)
* delete source/downloads after building in script, not in dockerfile

* add editorconfig

* eol

* cs fix

* added \n there

* we expect Hello\n

* Change tab width for shell scripts to 4 spaces

* bring back embed comment
2025-11-20 11:49:09 +01:00
Kévin Dunglas
c93729e136 chore: use sync.WaitGroup.Go when possible (#1996)
* chore: use sync.WaitGroup.Go when possible

* Update internal/watcher/watcher.go

Co-authored-by: Alexandre Daubois <2144837+alexandre-daubois@users.noreply.github.com>

---------

Co-authored-by: Alexandre Daubois <2144837+alexandre-daubois@users.noreply.github.com>
2025-11-20 11:48:18 +01:00
Kévin Dunglas
ea042637e6 ci: add back ARMv6 build 2025-11-20 11:47:36 +01:00
Marc
0b74507945 don't upx pack on macos (#2003) 2025-11-20 10:19:27 +01:00
Kévin Dunglas
aa1bd23004 ci: use local sources when building with Bake 2025-11-20 05:48:40 +01:00
Marc
02f900bb97 we use "mac" for os-name, not "darwin" (#2001) 2025-11-19 22:11:23 +01:00
Vincent Amstoutz
56df2666e1 ci: add PHP 8.5 support for building images 2025-11-19 16:19:32 +01:00
Marc
1de9073e49 simplify build-static script (#1968)
* simplify build-static script

* we don't require go anymore, since spc will install it

* bring back eof newline

* move to frankenphp-os-arch again

* shell fmt

* Add FrankenPHP Caddy modules to build script
2025-11-19 15:30:00 +01:00
Kévin Dunglas
36062a0dce feat(static): add XSL extension (#1998) 2025-11-19 14:55:53 +01:00
Kévin Dunglas
10cf2c4a2e fix: use the global logger during classes preloading (#1994)
* fix: use the global logger during classes preloading

* better fix

* fix comparision

* Update frankenphp.go
2025-11-19 14:18:29 +01:00
Ahmet Türk
f224f8e391 docs: fix minor typo (#1991) 2025-11-18 14:29:25 +01:00
Alexander Stecher
0b2d3c913f feat: per worker max threads (#1962)
* adds worker max_threads

* Adds tests for all calculation cases.

* Adds max_threads limitation to test.

* Removes the test sleep.

* Adds max_threads to error message.

* correctly uses continue.

* Fixes logic with only worker max_threads set.

* Adjust comments.

* Removes unnecessary check.

* Fixes comment.

* suggestions by @dunlgas.

* copilot suggestions.

* Renames logger.

* review

---------

Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
2025-11-18 11:55:29 +01:00
Kévin Dunglas
75a48e81a7 chore: bump deps 2025-11-18 11:48:59 +01:00
Alexandre Daubois
bd943f49de feat(extgen): print gen_stub.php in case of failure 2025-11-18 11:10:03 +01:00
Alexandre Daubois
8f298ab060 fix(extgen): constant should be declared under the namespace provided by export_php:namespace 2025-11-18 10:40:59 +01:00
Kévin Dunglas
41cb2bbeaa feat: mercure_publish() PHP function to dispatch Mercure updates (#1927)
* feat: mercure_publish() PHP function to dispatch Mercure updates

* fix stubs for old versions

* review

* cleanup and fixes
2025-11-18 09:59:53 +01:00
Marc
853cb67e95 shallow clone to save space in CI (#1987)
* shallow clone

* also remove source dir after building in CI

* formatting

* pass them through?

* only CI

* add as variable
2025-11-18 08:48:06 +01:00
Alexandre Daubois
eeb7d1a0c4 fix(extgen): only register ext_functions if functions are declared 2025-11-17 17:40:30 +01:00
Kévin Dunglas
8341cc98c6 refactor: rely on context.Context for log/slog and others (#1969)
* refactor: rely on context.Context for log/slog and others

* optimize

* refactor

* Apply suggestion from @Copilot

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix watcher-skip

* better globals handling

* fix

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-17 16:32:23 +01:00
Alexandre Daubois
40cb42aace chore: bump net 2025-11-17 15:09:30 +01:00
Alexandre Daubois
1e48fbb801 chore(docs): catch-up french translation (#1978) 2025-11-17 12:07:38 +01:00
Alexandre Daubois
4e6d67e0b4 fix(extgen): don't remove everything in the build directory now that there's no build subdir 2025-11-14 15:13:54 +01:00
Alexandre Daubois
18946308fd docs: remove superfluous arg in an example (#1972) 2025-11-14 15:12:56 +01:00
Alexandre Daubois
f7298557aa feat(extgen): automatically add "runtime/cgo" to the imports if necessary 2025-11-14 15:12:28 +01:00
Alexandre Daubois
861b345b05 fix(extgen): replace any by interface{} in the generated go file when dealing with handles 2025-11-14 14:54:40 +01:00
Kévin Dunglas
724c0b11ca feat: set a custom Server header
# Conflicts:
#	caddy/module.go
#	frankenphp.go
2025-11-10 17:25:22 +01:00
dependabot[bot]
63168e087e ci: bump golangci/golangci-lint-action in the github-actions group
Bumps the github-actions group with 1 update: [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action).


Updates `golangci/golangci-lint-action` from 8 to 9
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/v8...v9)

---
updated-dependencies:
- dependency-name: golangci/golangci-lint-action
  dependency-version: '9'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-10 17:24:24 +01:00
Kévin Dunglas
6225da9c18 refactor: improve ExtensionWorkers API (#1952)
* refactor: improve ExtensionWorkers API

* Update workerextension.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update workerextension.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update caddy/app.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Apply suggestion from @Copilot

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* review

* fix tests

* docs

* errors

* improved error handling

* fix race

* add missing return

* use %q in Errorf

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-10 14:12:14 +01:00
Alexander Stecher
407ef09ac3 fix: fail immediately on missing worker file (#1963) 2025-11-10 09:23:50 +01:00
Marc
bf4c9fe986 fix test typo (#1964)
* Fix typo in TestFlushEmptyResponse_worker function

* Fix test function name for flush empty response
2025-11-08 08:28:56 +01:00
Marc
b22bdd987b update docs to remove old references to frankenphp:static-builder (#1950)
* update docs to remove old references to frankenphp:static-builder (we have -musl and -gnu)

* remove --platform and make gnu default

* add --platform back in (let dunglas decide)

* fix missed translations
2025-11-07 10:27:37 +01:00
Alexandre Daubois
28d17b39dc chore: bump GitHub Action deps (#1957) 2025-11-04 08:52:23 +01:00
Marc
264f92835d bring back logic for workers to inherit php_server parent environment (#1956)
* bring back logic to inherit php_server parent environment

* change order to account for

php {
    worker file.php 1 {

    }
}
cases

* suggestion

* add inherit env test
2025-11-02 14:50:50 +01:00
Alexandre Daubois
b49aed1934 chore: bump deps 2025-10-31 16:51:26 +01:00
Kévin Dunglas
4d0fb7d0f8 refactor: simplify Init() 2025-10-29 23:14:46 +01:00
Marc
5447a7a6c8 add compile from sources fallback to unsupported OS message (#1939)
* add compile from sources fallback to unsupported OS message

* rewrite message to indicate general support, but no precompiled binaries

* add logs to build-static
2025-10-29 19:25:51 +01:00
Alexander Stecher
1270784cd3 suggestion: external worker api (#1928)
* Cleaner request apis.
2025-10-29 11:36:33 +01:00
Kévin Dunglas
9b8d215727 refactor: improve Worker public API and docs 2025-10-29 11:36:33 +01:00
Alexandre Daubois
94e58eb215 fix: replace file_put_contents() by file_get_contents() in Mercure docs 2025-10-28 15:00:43 +01:00
Alexander Stecher
bf6e6534f6 fix: exit() and dd() support in worker mode (#1946)
* Verifies exit behavior.

* formatting

* Checks for actual exit.

* Fixes test.

* Fixes test.

* Update testdata/dd.php

Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>

---------

Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
2025-10-28 10:57:50 +01:00
Michal Kleiner
fb1f46808e fix: typo in method name in example usage (#1935) 2025-10-22 09:14:36 +02:00
Florent Drousset
7f64673495 Fix small typo (#1934) 2025-10-21 17:50:00 +02:00
Kévin Dunglas
f7756717b5 feat: allow creating strongly typed slices and maps from PHP values with type utilities (#1933)
* feat: use generics in type functions for better type support

* various improvements

* better docs

* update docs
2025-10-21 11:20:54 +02:00
Damien Calesse
9aee496b96 Add patchelf installation in static-gnu Dockerfile (#1899)
* Add patchelf installation in static-gnu Dockerfile

* reduce static-builder-gnu.Dockerfile layers

---------

Co-authored-by: henderkes <m@pyc.ac>
2025-10-20 18:08:40 +02:00
Alexander Stecher
45823c51b2 fix: catches panic on invalid status code (#1920) 2025-10-16 11:35:35 +02:00
Kévin Dunglas
f8ea48c3b1 chore(caddy): better error handling 2025-10-15 11:12:34 +02:00
Laury S.
1fbd619597 fix: remove BOM on config fr doc file (#1924) 2025-10-14 17:50:51 +02:00
Kévin Dunglas
d52ce94341 docs: improve Mercure documentation and various other parts 2025-10-14 14:32:38 +02:00
Kévin Dunglas
b749f52ae5 chore: simplify string using backticks
# Conflicts:
#	internal/extgen/classparser.go
#	internal/extgen/gofile_test.go
2025-10-14 14:09:17 +02:00
Kévin Dunglas
e917ab7974 fix: callback parameters handling in worker extensions 2025-10-09 15:42:29 +02:00
Kévin Dunglas
5514491a18 feat(extgen): support for mixed type (#1913)
* feat(extgent): support for mixed type

* refactor: use unsafe.Pointer

* Revert "refactor: use unsafe.Pointer"

This reverts commit 8a0b9c1beb.

* fix docs

* fix docs

* cleanup template

* fix template

* fix tests
2025-10-09 14:10:45 +02:00
Kévin Dunglas
c42d287138 refactor: extension worker (#1910)
* refactor: extension worker

* feat: optional HTTP request

* allow passing unsafe.Pointer to the extension callback

* lint

* simplify
2025-10-09 14:10:09 +02:00
SpencerMalone
1f6f768c97 fix: release but don't free CLI streams when executing cli scripts (#1906)
* Bring upstream commit 0a4a55fd44 into cli_register_file_handles to release but not free stdout/in/err.

Fixes being unable to log to stdout or error after using frankenphp.ExecutePHPCode

* chore: clang-format

---------

Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
2025-10-08 17:07:54 +02:00
Kévin Dunglas
a4596b7767 ci: fix Biome linter issue (#1911) 2025-10-08 08:38:00 +02:00
Rob Landers
fa3a7032a4 refactor: make Worker an embeddable struct (#1884)
* make WorkerExtension embeddable

Signed-off-by: Robert Landers <landers.robert@gmail.com>

* change names

Signed-off-by: Robert Landers <landers.robert@gmail.com>

---------

Signed-off-by: Robert Landers <landers.robert@gmail.com>
2025-10-07 16:56:56 +02:00
Filippo Tessarotto
ab1ec71d24 docs(worker): Prefer try-catch over set_exception_handler (#1897) 2025-10-06 09:01:49 +02:00
dependabot[bot]
219a5407ff chore: bump the go-modules group with 2 updates (#1903) 2025-09-29 17:56:28 +02:00
Marc
76511c2e19 fix(packages): frankenphp trust failing because admin API isn't started #1846 (#1870) 2025-09-27 15:50:29 +02:00
Alexandre Daubois
7668a27d4b chore: bump /caddy sub-group deps (#1863) 2025-09-23 10:26:13 +02:00
Artur Melanchyk
e4c1801c25 fix: added missing decrement for the "ready" WaitGroup counter (#1890)
Co-authored-by: Artur Melanchyk <13834276+arturmelanchyk@users.noreply.github.com>
2025-09-22 19:57:01 +02:00
Rob Landers
52df300f86 feat: custom workers initial support (#1795)
* create a simple thread framework

Signed-off-by: Robert Landers <landers.robert@gmail.com>

* add tests

Signed-off-by: Robert Landers <landers.robert@gmail.com>

* fix comment

Signed-off-by: Robert Landers <landers.robert@gmail.com>

* remove mention of an old function that no longer exists

Signed-off-by: Robert Landers <landers.robert@gmail.com>

* simplify providing a request

Signed-off-by: Robert Landers <landers.robert@gmail.com>

* satisfy linter

Signed-off-by: Robert Landers <landers.robert@gmail.com>

* add error handling and handle shutdowns

Signed-off-by: Robert Landers <landers.robert@gmail.com>

* add tests

Signed-off-by: Robert Landers <landers.robert@gmail.com>

* pipes are tied to workers, not threads

Signed-off-by: Robert Landers <landers.robert@gmail.com>

* fix test

Signed-off-by: Robert Landers <landers.robert@gmail.com>

* add a way to detect when a request is completed

Signed-off-by: Robert Landers <landers.robert@gmail.com>

* we never shutdown workers or remove them, so we do not need this

Signed-off-by: Robert Landers <landers.robert@gmail.com>

* add more comments

Signed-off-by: Robert Landers <landers.robert@gmail.com>

* Simplify modular threads (#1874)

* Simplify

* remove unused variable

* log thread index

* feat: allow passing parameters to the PHP callback and accessing its return value (#1881)

* fix formatting

Signed-off-by: Robert Landers <landers.robert@gmail.com>

* fix test compilation

Signed-off-by: Robert Landers <landers.robert@gmail.com>

* fix segfaults

Signed-off-by: Robert Landers <landers.robert@gmail.com>

* Update frankenphp.c

Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>

---------

Signed-off-by: Robert Landers <landers.robert@gmail.com>
Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
2025-09-18 09:21:49 +02:00
Florent Drousset
fe7aa2cae4 docs: fix small typo in x-sendfile.md (#1882)
Just fixing a small typo (double "à") that I've seen in the doc.
2025-09-16 20:02:43 +02:00
Kévin Dunglas
7754abcbd0 fix: PHPValue() and GoValue() types (#1879)
* fix: PHPValue() return type

* fix: GoValue() argument type
2025-09-15 17:04:06 +02:00
Kévin Dunglas
52a0be5728 feat(ext): expose GoValue() and PHPValue() functions (#1877)
* feat(ext): expose a GoValue function

* GoValue()
2025-09-15 16:25:11 +02:00
206 changed files with 8497 additions and 3674 deletions

1
.clang-format-ignore Normal file
View File

@@ -0,0 +1 @@
frankenphp_arginfo.h

13
.editorconfig Normal file
View File

@@ -0,0 +1,13 @@
root = true
[*]
end_of_line = lf
insert_final_newline = true
[*.sh]
indent_style = tab
tab_width = 4
[*.Dockerfile]
indent_style = tab
tab_width = 4

View File

@@ -29,8 +29,9 @@ body:
- Docker (Debian Trixie)
- Docker (Debian Bookworm)
- Docker (Alpine)
- Official static build
- Standalone binary
- deb packages
- rpm packages
- Static binary
- Custom (tell us more in the description)
default: 0
validations:

View File

@@ -11,6 +11,8 @@ updates:
go-modules:
patterns:
- "*"
cooldown:
default-days: 7
- package-ecosystem: gomod
directory: /caddy
schedule:
@@ -21,6 +23,8 @@ updates:
go-modules:
patterns:
- "*"
cooldown:
default-days: 7
- package-ecosystem: github-actions
directory: /
schedule:
@@ -31,3 +35,5 @@ updates:
github-actions:
patterns:
- "*"
cooldown:
default-days: 7

View File

@@ -46,6 +46,8 @@ jobs:
php_version: ${{ steps.check.outputs.php_version }}
php82_version: ${{ steps.check.outputs.php82_version }}
php83_version: ${{ steps.check.outputs.php83_version }}
php84_version: ${{ steps.check.outputs.php84_version }}
php85_version: ${{ steps.check.outputs.php85_version }}
skip: ${{ steps.check.outputs.skip }}
ref: ${{ steps.check.outputs.ref || (github.event_name == 'workflow_dispatch' && inputs.version) || '' }}
steps:
@@ -57,11 +59,13 @@ jobs:
PHP_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="; "")')
PHP_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="; "")')
PHP_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="; "")')
PHP_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="; "")')
{
echo php_version="${PHP_82_LATEST},${PHP_83_LATEST},${PHP_84_LATEST}"
echo php_version="${PHP_82_LATEST},${PHP_83_LATEST},${PHP_84_LATEST},${PHP_85_LATEST}"
echo php82_version="${PHP_82_LATEST//./-}"
echo php83_version="${PHP_83_LATEST//./-}"
echo php84_version="${PHP_84_LATEST//./-}"
echo php85_version="${PHP_85_LATEST//./-}"
} >> "${GITHUB_OUTPUT}"
# Check if the Docker images must be rebuilt
@@ -75,8 +79,9 @@ jobs:
FRANKENPHP_82_LATEST=$(skopeo inspect docker://docker.io/dunglas/frankenphp:"${FRANKENPHP_LATEST_TAG_NO_PREFIX}"-php8.2 --override-os linux --override-arch amd64 | jq -r '.Env[] | select(test("^PHP_VERSION=")) | sub("^PHP_VERSION="; "")')
FRANKENPHP_83_LATEST=$(skopeo inspect docker://docker.io/dunglas/frankenphp:"${FRANKENPHP_LATEST_TAG_NO_PREFIX}"-php8.3 --override-os linux --override-arch amd64 | jq -r '.Env[] | select(test("^PHP_VERSION=")) | sub("^PHP_VERSION="; "")')
FRANKENPHP_84_LATEST=$(skopeo inspect docker://docker.io/dunglas/frankenphp:"${FRANKENPHP_LATEST_TAG_NO_PREFIX}"-php8.4 --override-os linux --override-arch amd64 | jq -r '.Env[] | select(test("^PHP_VERSION=")) | sub("^PHP_VERSION="; "")')
FRANKENPHP_85_LATEST=$(skopeo inspect docker://docker.io/dunglas/frankenphp:"${FRANKENPHP_LATEST_TAG_NO_PREFIX}"-php8.5 --override-os linux --override-arch amd64 | jq -r '.Env[] | select(test("^PHP_VERSION=")) | sub("^PHP_VERSION="; "")')
if [[ "${FRANKENPHP_82_LATEST}" == "${PHP_82_LATEST}" ]] && [[ "${FRANKENPHP_83_LATEST}" == "${PHP_83_LATEST}" ]] && [[ "${FRANKENPHP_84_LATEST}" == "${PHP_84_LATEST}" ]]; then
if [[ "${FRANKENPHP_82_LATEST}" == "${PHP_82_LATEST}" ]] && [[ "${FRANKENPHP_83_LATEST}" == "${PHP_83_LATEST}" ]] && [[ "${FRANKENPHP_84_LATEST}" == "${PHP_84_LATEST}" ]] && [[ "${FRANKENPHP_85_LATEST}" == "${PHP_85_LATEST}" ]]; then
echo skip=true >> "${GITHUB_OUTPUT}"
exit 0
fi
@@ -85,7 +90,7 @@ jobs:
echo ref="${FRANKENPHP_LATEST_TAG}"
echo skip=false
} >> "${GITHUB_OUTPUT}"
- uses: actions/checkout@v5
- uses: actions/checkout@v6
if: ${{ !fromJson(steps.check.outputs.skip) }}
with:
ref: ${{ steps.check.outputs.ref }}
@@ -129,17 +134,25 @@ jobs:
platform: linux/arm/v6
- variant: php-${{ needs.prepare.outputs.php83_version }}-trixie
platform: linux/arm/v6
- variant: php-${{ needs.prepare.outputs.php84_version }}-trixie
platform: linux/arm/v6
- variant: php-${{ needs.prepare.outputs.php85_version }}-trixie
platform: linux/arm/v6
- variant: php-${{ needs.prepare.outputs.php82_version }}-bookworm
platform: linux/arm/v6
- variant: php-${{ needs.prepare.outputs.php83_version }}-bookworm
platform: linux/arm/v6
- variant: php-${{ needs.prepare.outputs.php84_version }}-bookworm
platform: linux/arm/v6
- variant: php-${{ needs.prepare.outputs.php85_version }}-bookworm
platform: linux/arm/v6
steps:
- name: Prepare
id: prepare
run: echo "sanitized_platform=${PLATFORM//\//-}" >> "${GITHUB_OUTPUT}"
env:
PLATFORM: ${{ matrix.platform }}
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
ref: ${{ needs.prepare.outputs.ref }}
persist-credentials: false
@@ -159,6 +172,7 @@ jobs:
with:
pull: true
load: ${{ !fromJson(needs.prepare.outputs.push) }}
source: .
targets: |
builder-${{ matrix.variant }}
runner-${{ matrix.variant }}
@@ -194,7 +208,7 @@ jobs:
VARIANT: ${{ matrix.variant }}
- name: Upload builder metadata
if: fromJson(needs.prepare.outputs.push)
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: metadata-builder-${{ matrix.variant }}-${{ steps.prepare.outputs.sanitized_platform }}
path: /tmp/metadata/builder/*
@@ -202,7 +216,7 @@ jobs:
retention-days: 1
- name: Upload runner metadata
if: fromJson(needs.prepare.outputs.push)
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: metadata-runner-${{ matrix.variant }}-${{ steps.prepare.outputs.sanitized_platform }}
path: /tmp/metadata/runner/*
@@ -234,7 +248,7 @@ jobs:
target: ["builder", "runner"]
steps:
- name: Download metadata
uses: actions/download-artifact@v5
uses: actions/download-artifact@v7
with:
pattern: metadata-${{ matrix.target }}-${{ matrix.variant }}-*
path: /tmp/metadata

View File

@@ -20,7 +20,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
fetch-depth: 0
persist-credentials: false
@@ -40,8 +40,12 @@ jobs:
VALIDATE_TERRAGRUNT: false
VALIDATE_DOCKERFILE_HADOLINT: false
VALIDATE_TRIVY: false
# Prettier and StandardJS are incompatible
# Prettier, Biome and StandardJS are incompatible
VALIDATE_JAVASCRIPT_PRETTIER: false
VALIDATE_TYPESCRIPT_PRETTIER: false
VALIDATE_BIOME_FORMAT: false
VALIDATE_BIOME_LINT: false
# Conflicts with MARKDOWN
VALIDATE_MARKDOWN_PRETTIER: false
# To re-enable when https://github.com/super-linter/super-linter/issues/7244 will be closed
VALIDATE_EDITORCONFIG: false

View File

@@ -40,7 +40,7 @@ jobs:
steps:
- name: Remove local PHP
run: sudo apt-get remove --purge --autoremove 'php*' 'libmemcached*'
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
persist-credentials: false
- uses: actions/setup-go@v6
@@ -52,12 +52,12 @@ jobs:
- name: Determine PHP version
id: determine-php-version
run: |
curl -fsSL 'https://www.php.net/releases/index.php?json&max=1&version=8.4' -o version.json
curl -fsSL 'https://www.php.net/releases/index.php?json&max=1&version=8.5' -o version.json
echo version="$(jq -r 'keys[0]' version.json)" >> "$GITHUB_OUTPUT"
echo archive="$(jq -r '.[] .source[] | select(.filename |endswith(".xz")) | "https://www.php.net/distributions/" + .filename' version.json)" >> "$GITHUB_OUTPUT"
- name: Cache PHP
id: cache-php
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: php/target
key: php-sanitizers-${{ matrix.sanitizer }}-${{ runner.arch }}-${{ steps.determine-php-version.outputs.version }}

View File

@@ -10,7 +10,7 @@ on:
- main
paths:
- "docker-bake.hcl"
- ".github/workflows/docker.yaml"
- ".github/workflows/static.yaml"
- "**cgo.go"
- "**Dockerfile"
- "**.c"
@@ -37,6 +37,7 @@ permissions:
env:
IMAGE_NAME: ${{ (github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && inputs.version) || startsWith(github.ref, 'refs/tags/')) && 'dunglas/frankenphp' || 'dunglas/frankenphp-dev' }}
SPC_OPT_BUILD_ARGS: --debug
GOTOOLCHAIN: local
jobs:
@@ -62,7 +63,7 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REF: ${{ (github.ref_type == 'tag' && github.ref_name) || (github.event_name == 'workflow_dispatch' && inputs.version) || '' }}
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
ref: ${{ steps.check.outputs.ref }}
persist-credentials: false
@@ -108,7 +109,7 @@ jobs:
run: echo "sanitized_platform=${PLATFORM//\//-}" >> "${GITHUB_OUTPUT}"
env:
PLATFORM: ${{ matrix.platform }}
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
ref: ${{ needs.prepare.outputs.ref }}
persist-credentials: false
@@ -122,12 +123,26 @@ jobs:
with:
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Set VERSION
run: |
if [ "${GITHUB_REF_TYPE}" == "tag" ]; then
export VERSION=${GITHUB_REF_NAME:1}
elif [ "${GITHUB_EVENT_NAME}" == "schedule" ]; then
export VERSION="${REF}"
else
export VERSION=${GITHUB_SHA}
fi
echo "VERSION=${VERSION}" >> "${GITHUB_ENV}"
env:
REF: ${{ needs.prepare.outputs.ref }}
- name: Build
id: build
uses: docker/bake-action@v6
with:
pull: true
load: ${{ !fromJson(needs.prepare.outputs.push) || matrix.debug || matrix.mimalloc }}
source: .
targets: static-builder-musl
set: |
${{ matrix.debug && 'static-builder-musl.args.DEBUG_SYMBOLS=1' || '' }}
@@ -141,7 +156,6 @@ jobs:
${{ (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) || '' }}
env:
SHA: ${{ github.sha }}
VERSION: ${{ (github.ref_type == 'tag' && github.ref_name) || needs.prepare.outputs.ref || 'dev' }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- # Workaround for https://github.com/actions/runner/pull/2477#issuecomment-1501003600
name: Export metadata
@@ -156,7 +170,7 @@ jobs:
METADATA: ${{ steps.build.outputs.metadata }}
- name: Upload metadata
if: fromJson(needs.prepare.outputs.push) && !matrix.debug && !matrix.mimalloc
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: metadata-static-builder-musl-${{ steps.prepare.outputs.sanitized_platform }}
path: /tmp/metadata/*
@@ -174,7 +188,7 @@ jobs:
PLATFORM: ${{ matrix.platform }}
- name: Upload artifact
if: ${{ !fromJson(needs.prepare.outputs.push) }}
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: frankenphp-linux-${{ matrix.platform == 'linux/amd64' && 'x86_64' || 'aarch64' }}${{ matrix.debug && '-debug' || '' }}${{ matrix.mimalloc && '-mimalloc' || '' }}
path: frankenphp-linux-${{ matrix.platform == 'linux/amd64' && 'x86_64' || 'aarch64' }}${{ matrix.debug && '-debug' || '' }}${{ matrix.mimalloc && '-mimalloc' || '' }}
@@ -214,15 +228,56 @@ jobs:
runs-on: ${{ startsWith(matrix.platform, 'linux/arm') && 'ubuntu-24.04-arm' || 'ubuntu-24.04' }}
needs: [prepare]
steps:
# Inspired by https://gist.github.com/antiphishfish/1e3fbc3f64ef6f1ab2f47457d2da5d9d and https://github.com/apache/flink/blob/master/tools/azure-pipelines/free_disk_space.sh
- name: Free disk space
run: |
set -xe
sudo rm -rf /usr/share/dotnet
sudo rm -rf /usr/share/swift
sudo rm -rf /usr/local/lib/android
sudo rm -rf /opt/ghc
sudo rm -rf /usr/local/.ghcup
sudo rm -rf "/usr/local/share/boost"
sudo rm -rf "$AGENT_TOOLSDIRECTORY"
sudo rm -rf /opt/hostedtoolcache/
sudo rm -rf /usr/local/graalvm/
sudo rm -rf /usr/local/share/powershell
sudo rm -rf /usr/local/share/chromium
sudo rm -rf /usr/local/lib/node_modules
sudo docker image prune --all --force
APT_PARAMS='sudo apt -y -qq -o=Dpkg::Use-Pty=0'
$APT_PARAMS remove -y '^dotnet-.*'
$APT_PARAMS remove -y '^llvm-.*'
$APT_PARAMS remove -y '^php.*'
$APT_PARAMS remove -y '^mongodb-.*'
$APT_PARAMS remove -y '^mysql-.*'
$APT_PARAMS remove -y azure-cli firefox powershell mono-devel libgl1-mesa-dri
$APT_PARAMS autoremove --purge -y
$APT_PARAMS autoclean
$APT_PARAMS clean
- name: Prepare
id: prepare
run: echo "sanitized_platform=${PLATFORM//\//-}" >> "${GITHUB_OUTPUT}"
env:
PLATFORM: ${{ matrix.platform }}
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
ref: ${{ needs.prepare.outputs.ref }}
persist-credentials: false
- name: Set VERSION
run: |
if [ "${GITHUB_REF_TYPE}" == "tag" ]; then
export VERSION=${GITHUB_REF_NAME:1}
elif [ "${GITHUB_EVENT_NAME}" == "schedule" ]; then
export VERSION="${REF}"
else
export VERSION=${GITHUB_SHA}
fi
echo "VERSION=${VERSION}" >> "${GITHUB_ENV}"
env:
REF: ${{ needs.prepare.outputs.ref }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
@@ -239,10 +294,10 @@ jobs:
with:
pull: true
load: ${{ !fromJson(needs.prepare.outputs.push) }}
source: .
targets: static-builder-gnu
set: |
${{ (github.event_name == 'pull_request' || matrix.platform == 'linux/arm64') && 'static-builder-gnu.args.NO_COMPRESS=1' || '' }}
static-builder-gnu.args.BUILD_PACKAGES=1
*.tags=
*.platform=${{ matrix.platform }}
*.cache-from=type=gha,scope=${{ needs.prepare.outputs.ref || github.ref }}-static-builder-gnu
@@ -251,7 +306,6 @@ jobs:
${{ fromJson(needs.prepare.outputs.push) && format('*.output=type=image,name={0},push-by-digest=true,name-canonical=true,push=true', env.IMAGE_NAME) || '' }}
env:
SHA: ${{ github.sha }}
VERSION: ${{ (github.ref_type == 'tag' && github.ref_name) || needs.prepare.outputs.ref || 'dev' }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- # Workaround for https://github.com/actions/runner/pull/2477#issuecomment-1501003600
name: Export metadata
@@ -266,7 +320,7 @@ jobs:
METADATA: ${{ steps.build.outputs.metadata }}
- name: Upload metadata
if: fromJson(needs.prepare.outputs.push)
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: metadata-static-builder-gnu-${{ steps.prepare.outputs.sanitized_platform }}
path: /tmp/metadata-gnu/*
@@ -290,7 +344,7 @@ jobs:
PLATFORM: ${{ matrix.platform }}
- name: Upload artifact
if: ${{ !fromJson(needs.prepare.outputs.push) }}
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: frankenphp-linux-${{ matrix.platform == 'linux/amd64' && 'x86_64' || 'aarch64' }}-gnu-files
path: gh-output/*
@@ -326,13 +380,13 @@ jobs:
if: fromJson(needs.prepare.outputs.push)
steps:
- name: Download metadata
uses: actions/download-artifact@v5
uses: actions/download-artifact@v7
with:
pattern: metadata-static-builder-musl-*
path: /tmp/metadata
merge-multiple: true
- name: Download GNU metadata
uses: actions/download-artifact@v5
uses: actions/download-artifact@v7
with:
pattern: metadata-static-builder-gnu-*
path: /tmp/metadata-gnu
@@ -384,12 +438,12 @@ jobs:
matrix:
platform: ["arm64", "x86_64"]
name: Build macOS ${{ matrix.platform }} binaries
runs-on: ${{ matrix.platform == 'arm64' && 'macos-14' || 'macos-13' }}
runs-on: ${{ matrix.platform == 'arm64' && 'macos-15' || 'macos-15-intel' }}
needs: [prepare]
env:
HOMEBREW_NO_AUTO_UPDATE: 1
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
ref: ${{ needs.prepare.outputs.ref }}
persist-credentials: false
@@ -419,13 +473,19 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE: ${{ (needs.prepare.outputs.ref || github.ref_type == 'tag') && '1' || '' }}
NO_COMPRESS: ${{ github.event_name == 'pull_request' && '1' || '' }}
- name: Upload logs
if: ${{ failure() }}
uses: actions/upload-artifact@v6
with:
path: dist/static-php-cli/log
name: static-php-cli-log-${{ matrix.platform }}-${{ github.sha }}
- if: needs.prepare.outputs.ref || github.ref_type == 'tag'
uses: actions/attest-build-provenance@v3
with:
subject-path: ${{ github.workspace }}/dist/frankenphp-mac-*
- name: Upload artifact
if: github.ref_type == 'branch'
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: frankenphp-mac-${{ matrix.platform }}
path: dist/frankenphp-mac-${{ matrix.platform }}

View File

@@ -23,7 +23,7 @@ jobs:
tests-linux:
name: Tests (Linux, PHP ${{ matrix.php-versions }})
runs-on: ubuntu-latest
continue-on-error: ${{ matrix.experimental || false }}
continue-on-error: false
strategy:
fail-fast: false
matrix:
@@ -32,12 +32,11 @@ jobs:
- php-versions: "8.3"
- php-versions: "8.4"
- php-versions: "8.5"
experimental: true
env:
GOMAXPROCS: 10
LIBRARY_PATH: ${{ github.workspace }}/watcher/target/lib
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
persist-credentials: false
- uses: actions/setup-go@v6
@@ -83,35 +82,78 @@ jobs:
- name: Run integrations tests
run: ./reload_test.sh
- name: Lint Go code
uses: golangci/golangci-lint-action@v8
if: matrix.php-versions == '8.4'
uses: golangci/golangci-lint-action@v9
if: matrix.php-versions == '8.5'
with:
version: latest
- name: Ensure go.mod is tidy
if: matrix.php-versions == '8.4'
if: matrix.php-versions == '8.5'
run: go mod tidy -diff
- name: Ensure caddy/go.mod is tidy
if: matrix.php-versions == '8.4'
if: matrix.php-versions == '8.5'
run: go mod tidy -diff
working-directory: caddy/
tests-mac:
name: Tests (macOS, PHP 8.4)
runs-on: macos-latest
env:
HOMEBREW_NO_AUTO_UPDATE: 1
integration-tests:
name: Integration Tests (Linux, PHP ${{ matrix.php-versions }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php-versions: ["8.3", "8.4", "8.5"]
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
persist-credentials: false
- uses: actions/setup-go@v6
with:
go-version: "1.25"
cache-dependency-path: |
go.sum
go.sum
caddy/go.sum
- uses: shivammathur/setup-php@v2
with:
php-version: 8.4
php-version: ${{ matrix.php-versions }}
ini-file: development
coverage: none
tools: none
env:
phpts: ts
debug: true
- name: Install PHP development libraries
run: sudo apt-get update && sudo apt-get install -y libkrb5-dev libsodium-dev libargon2-dev
- name: Install xcaddy
run: go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest
- name: Download PHP sources
run: |
PHP_VERSION=$(php -r "echo PHP_VERSION;")
wget -q "https://www.php.net/distributions/php-${PHP_VERSION}.tar.gz"
tar xzf "php-${PHP_VERSION}.tar.gz"
echo "GEN_STUB_SCRIPT=${PWD}/php-${PHP_VERSION}/build/gen_stub.php" >> "${GITHUB_ENV}"
- name: Set CGO flags
run: |
echo "CGO_CFLAGS=$(php-config --includes)" >> "${GITHUB_ENV}"
echo "CGO_LDFLAGS=$(php-config --ldflags) $(php-config --libs)" >> "${GITHUB_ENV}"
- name: Run integration tests
working-directory: internal/extgen/
run: go test -tags integration -v -timeout 30m
tests-mac:
name: Tests (macOS, PHP 8.5)
runs-on: macos-latest
env:
HOMEBREW_NO_AUTO_UPDATE: 1
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- uses: actions/setup-go@v6
with:
go-version: "1.25"
cache-dependency-path: |
go.sum
caddy/go.sum
- uses: shivammathur/setup-php@v2
with:
php-version: 8.5
ini-file: development
coverage: none
tools: none

1
.gitleaksignore Normal file
View File

@@ -0,0 +1 @@
/github/workspace/docs/mercure.md:jwt:88

View File

@@ -1,4 +1,5 @@
---
no-hard-tabs: false
MD010: false
MD013: false
MD033: false
MD060: false

View File

@@ -110,7 +110,7 @@ RUN go mod download
WORKDIR /go/src/app
COPY --link . ./
# See https://github.com/docker-library/php/blob/master/8.4/trixie/zts/Dockerfile#L57-L59 for PHP values
# See https://github.com/docker-library/php/blob/master/8.5/trixie/zts/Dockerfile#L57-L59 for PHP values
ENV CGO_CFLAGS="-DFRANKENPHP_VERSION=$FRANKENPHP_VERSION $PHP_CFLAGS"
ENV CGO_CPPFLAGS=$PHP_CPPFLAGS
ENV CGO_LDFLAGS="-L/usr/local/lib -lssl -lcrypto -lreadline -largon2 -lcurl -lonig -lz $PHP_LDFLAGS"

View File

@@ -16,21 +16,77 @@ FrankenPHP can also be used as a standalone Go library to embed PHP in any app u
## Getting Started
### Standalone Binary
We provide static FrankenPHP binaries for Linux and macOS
containing [PHP 8.4](https://www.php.net/releases/8.4/en.php) and most popular PHP extensions.
On Windows, use [WSL](https://learn.microsoft.com/windows/wsl/) to run FrankenPHP.
[Download FrankenPHP](https://github.com/php/frankenphp/releases) or copy this line into your
terminal to automatically install the version appropriate for your platform:
### Install Script
You can copy this line into your terminal to automatically
install an appropriate version for your platform:
```console
curl https://frankenphp.dev/install.sh | sh
mv frankenphp /usr/local/bin/
```
### Standalone Binary
We provide static FrankenPHP binaries for development purposes on Linux and macOS
containing [PHP 8.4](https://www.php.net/releases/8.4/en.php) and most popular PHP extensions.
[Download FrankenPHP](https://github.com/php/frankenphp/releases)
**Installing extensions:** Most common extensions are bundled. It's not possible to install more extensions.
### rpm Packages
Our maintainers offer rpm packages for all systems using `dnf`. To install, run:
```console
sudo dnf install https://rpm.henderkes.com/static-php-1-0.noarch.rpm
sudo dnf module enable php-zts:static-8.4 # 8.2-8.5 available
sudo dnf install frankenphp
```
**Installing extensions:** `sudo dnf install php-zts-<extension>`
For extensions not available by default, use [PIE](https://github.com/php/pie):
```console
sudo dnf install pie-zts
sudo pie-zts install asgrim/example-pie-extension
```
### deb Packages
Our maintainers offer deb packages for all systems using `apt`. To install, run:
```console
sudo curl -fsSL https://key.henderkes.com/static-php.gpg -o /usr/share/keyrings/static-php.gpg && \
echo "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 && \
sudo apt update
sudo apt install frankenphp
```
**Installing extensions:** `sudo apt install php-zts-<extension>`
For extensions not available by default, use [PIE](https://github.com/php/pie):
```console
sudo apt install pie-zts
sudo pie-zts install asgrim/example-pie-extension
```
### Homebrew
FrankenPHP is also available as a [Homebrew](https://brew.sh) package for macOS and Linux.
```console
brew install dunglas/frankenphp/frankenphp
```
**Installing extensions:** Use [PIE](https://github.com/php/pie).
### Usage
To serve the content of the current directory, run:
```console
@@ -43,6 +99,12 @@ You can also run command-line scripts with:
frankenphp php-cli /path/to/your/script.php
```
For the deb and rpm packages, you can also start the systemd service:
```console
sudo systemctl start frankenphp
```
### Docker
Alternatively, [Docker images](https://frankenphp.dev/docs/docker/) are available:
@@ -60,22 +122,6 @@ Go to `https://localhost`, and enjoy!
> Do not attempt to use `https://127.0.0.1`. Use `https://localhost` and accept the self-signed certificate.
> Use the [`SERVER_NAME` environment variable](docs/config.md#environment-variables) to change the domain to use.
### Homebrew
FrankenPHP is also available as a [Homebrew](https://brew.sh) package for macOS and Linux.
To install it:
```console
brew install dunglas/frankenphp/frankenphp
```
To serve the content of the current directory, run:
```console
frankenphp php-server
```
## Docs
- [Classic mode](https://frankenphp.dev/docs/classic/)

View File

@@ -66,12 +66,12 @@ ENV GOTOOLCHAIN=local
RUN apk add --no-cache --virtual .build-deps \
$PHPIZE_DEPS \
argon2-dev \
# Needed for the custom Go build
# Needed for the custom Go build \
bash \
brotli-dev \
coreutils \
curl-dev \
# Needed for the custom Go build
# Needed for the custom Go build \
git \
gnu-libiconv-dev \
libsodium-dev \
@@ -89,20 +89,20 @@ RUN apk add --no-cache --virtual .build-deps \
# Install e-dant/watcher (necessary for file watching)
WORKDIR /usr/local/src/watcher
RUN --mount=type=secret,id=github-token \
if [ -f /run/secrets/github-token ] && [ -s /run/secrets/github-token ]; then \
curl -s -H "Authorization: Bearer $(cat /run/secrets/github-token)" https://api.github.com/repos/e-dant/watcher/releases/latest; \
else \
curl -s https://api.github.com/repos/e-dant/watcher/releases/latest; \
fi | \
grep tarball_url | \
awk '{ print $2 }' | \
sed 's/,$//' | \
sed 's/"//g' | \
xargs curl -L | \
tar xz --strip-components 1 && \
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release && \
cmake --build build && \
cmake --install build
if [ -f /run/secrets/github-token ] && [ -s /run/secrets/github-token ]; then \
curl -s -H "Authorization: Bearer $(cat /run/secrets/github-token)" https://api.github.com/repos/e-dant/watcher/releases/latest; \
else \
curl -s https://api.github.com/repos/e-dant/watcher/releases/latest; \
fi | \
grep tarball_url | \
awk '{ print $2 }' | \
sed 's/,$//' | \
sed 's/"//g' | \
xargs curl -L | \
tar xz --strip-components 1 && \
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release && \
cmake --build build && \
cmake --install build
WORKDIR /go/src/app
@@ -123,7 +123,7 @@ ENV CGO_LDFLAGS="-lssl -lcrypto -lreadline -largon2 -lcurl -lonig -lz $PHP_LDFLA
WORKDIR /go/src/app/caddy/frankenphp
RUN GOBIN=/usr/local/bin \
../../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'" -buildvcs=true && \
../../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'" -buildvcs=true && \
setcap cap_net_bind_service=+ep /usr/local/bin/frankenphp && \
([ -z "${NO_COMPRESS}" ] && upx --best /usr/local/bin/frankenphp || true) && \
frankenphp version && \

View File

@@ -1,51 +0,0 @@
package frankenphp
import (
"sync"
"time"
)
type exponentialBackoff struct {
backoff time.Duration
failureCount int
mu sync.RWMutex
maxBackoff time.Duration
minBackoff time.Duration
maxConsecutiveFailures int
}
// recordSuccess resets the backoff and failureCount
func (e *exponentialBackoff) recordSuccess() {
e.mu.Lock()
e.failureCount = 0
e.backoff = e.minBackoff
e.mu.Unlock()
}
// recordFailure increments the failure count and increases the backoff, it returns true if maxConsecutiveFailures has been reached
func (e *exponentialBackoff) recordFailure() bool {
e.mu.Lock()
e.failureCount += 1
if e.backoff < e.minBackoff {
e.backoff = e.minBackoff
}
e.backoff = min(e.backoff*2, e.maxBackoff)
e.mu.Unlock()
return e.maxConsecutiveFailures != -1 && e.failureCount >= e.maxConsecutiveFailures
}
// wait sleeps for the backoff duration if failureCount is non-zero.
// NOTE: this is not tested and should be kept 'obviously correct' (i.e., simple)
func (e *exponentialBackoff) wait() {
e.mu.RLock()
if e.failureCount == 0 {
e.mu.RUnlock()
return
}
e.mu.RUnlock()
time.Sleep(e.backoff)
}

View File

@@ -1,41 +0,0 @@
package frankenphp
import (
"github.com/stretchr/testify/assert"
"testing"
"time"
)
func TestExponentialBackoff_Reset(t *testing.T) {
e := &exponentialBackoff{
maxBackoff: 5 * time.Second,
minBackoff: 500 * time.Millisecond,
maxConsecutiveFailures: 3,
}
assert.False(t, e.recordFailure())
assert.False(t, e.recordFailure())
e.recordSuccess()
e.mu.RLock()
defer e.mu.RUnlock()
assert.Equal(t, 0, e.failureCount, "expected failureCount to be reset to 0")
assert.Equal(t, e.backoff, e.minBackoff, "expected backoff to be reset to minBackoff")
}
func TestExponentialBackoff_Trigger(t *testing.T) {
e := &exponentialBackoff{
maxBackoff: 500 * 3 * time.Millisecond,
minBackoff: 500 * time.Millisecond,
maxConsecutiveFailures: 3,
}
assert.False(t, e.recordFailure())
assert.False(t, e.recordFailure())
assert.True(t, e.recordFailure())
e.mu.RLock()
defer e.mu.RUnlock()
assert.Equal(t, e.failureCount, e.maxConsecutiveFailures, "expected failureCount to be maxConsecutiveFailures")
assert.Equal(t, e.backoff, e.maxBackoff, "expected backoff to be maxBackoff")
}

View File

@@ -1,119 +0,0 @@
#!/bin/bash
set -o errexit
set -x
# Ensure required tools are installed
if ! command -v rpmbuild &>/dev/null; then
echo "Error: rpm-build is required to create RPM packages."
echo "Install it with: sudo dnf install rpm-build"
exit 1
fi
if ! command -v ruby &>/dev/null; then
echo "Error: Ruby is required by FPM."
echo "Install it with: sudo dnf install ruby"
exit 1
fi
if ! command -v fpm &>/dev/null; then
echo "Error: FPM (rubygem-fpm) is required to create RPM packages."
echo "Install it with: sudo gem install fpm"
exit 1
fi
arch="$(uname -m)"
os="$(uname -s | tr '[:upper:]' '[:lower:]')"
bin="frankenphp-${os}-${arch}"
if [ ! -f "dist/$bin" ]; then
echo "Error: dist/$bin not found. Run './build-static.sh' first"
exit 1
fi
version_output="$(dist/"$bin" version)"
frankenphp_version=$(echo "$version_output" | grep -oP 'FrankenPHP\s+\K[^ ]+' || true)
frankenphp_version=${frankenphp_version#v}
if [[ ! "${frankenphp_version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Warning: frankenphp_version must be set to X.Y.Z (e.g. 1.5.1), got '${frankenphp_version}'"
echo "Falling back to non-release version 0.0.0"
frankenphp_version=0.0.0
fi
group_preexists=0
user_preexists=0
if getent group frankenphp >/dev/null; then
group_preexists=1
else
sudo groupadd --system frankenphp
fi
if getent passwd frankenphp >/dev/null; then
user_preexists=1
else
sudo useradd --system \
--gid frankenphp \
--create-home \
--home-dir /var/lib/frankenphp \
--shell /usr/sbin/nologin \
--comment "FrankenPHP web server" \
frankenphp
fi
mkdir -p package/empty
mkdir -p package/etc
[ -f ./dist/static-php-cli/source/php-src/php.ini-production ] && cp -f ./dist/static-php-cli/source/php-src/php.ini-production ./package/etc/php.ini
cd dist
iteration=1
glibc_version=$(ldd -v "$bin" | awk '/GLIBC_/ {gsub(/[()]/, "", $2); print $2}' | grep -v GLIBC_PRIVATE | sort -V | tail -n1)
cxxabi_version=$(strings "$bin" | grep -oP 'CXXABI_\d+\.\d+(\.\d+)?' | sort -V | tail -n1)
fpm -s dir -t rpm -n frankenphp -v "${frankenphp_version}" \
--config-files /etc/frankenphp/Caddyfile \
--config-files /etc/frankenphp/php.ini \
--depends "libc.so.6(${glibc_version})(64bit)" \
--depends "libstdc++.so.6(${cxxabi_version})(64bit)" \
--before-install ../package/rhel/preinstall.sh \
--after-install ../package/rhel/postinstall.sh \
--before-remove ../package/rhel/preuninstall.sh \
--after-remove ../package/rhel/postuninstall.sh \
--iteration "${iteration}" \
--rpm-user frankenphp --rpm-group frankenphp \
"${bin}=/usr/bin/frankenphp" \
"../package/rhel/frankenphp.service=/usr/lib/systemd/system/frankenphp.service" \
"../package/Caddyfile=/etc/frankenphp/Caddyfile" \
"../package/content/=/usr/share/frankenphp" \
"../package/etc/php.ini=/etc/frankenphp/php.ini" \
"../package/empty/=/etc/frankenphp/php.d" \
"../package/empty/=/usr/lib/frankenphp/modules" \
"../package/empty/=/var/lib/frankenphp"
glibc_version=$(ldd -v "$bin" | awk '/GLIBC_/ {gsub(/[()]/, "", $2); print $2}' | grep -v GLIBC_PRIVATE | sed 's/GLIBC_//' | sort -V | tail -n1)
cxxabi_version=$(strings "$bin" | grep -oP 'CXXABI_\d+\.\d+(\.\d+)?' | sed 's/CXXABI_//' | sort -V | tail -n1)
fpm -s dir -t deb -n frankenphp -v "${frankenphp_version}" \
--config-files /etc/frankenphp/Caddyfile \
--config-files /etc/frankenphp/php.ini \
--depends "libc6 (>= ${glibc_version})" \
--depends "libstdc++6 (>= ${cxxabi_version})" \
--after-install ../package/debian/postinst.sh \
--before-remove ../package/debian/prerm.sh \
--after-remove ../package/debian/postrm.sh \
--iteration "${iteration}" \
--deb-user frankenphp --deb-group frankenphp \
"${bin}=/usr/bin/frankenphp" \
"../package/debian/frankenphp.service=/usr/lib/systemd/system/frankenphp.service" \
"../package/Caddyfile=/etc/frankenphp/Caddyfile" \
"../package/content/=/usr/share/frankenphp" \
"../package/etc/php.ini=/etc/frankenphp/php.ini" \
"../package/empty/=/etc/frankenphp/php.d" \
"../package/empty/=/usr/lib/frankenphp/modules" \
"../package/empty/=/var/lib/frankenphp"
[ "$user_preexists" -eq 0 ] && sudo userdel frankenphp
[ "$group_preexists" -eq 0 ] && (sudo groupdel frankenphp || true)
cd ..

View File

@@ -8,8 +8,11 @@ if ! type "git" >/dev/null 2>&1; then
exit 1
fi
CURRENT_DIR=$(pwd)
arch="$(uname -m)"
os="$(uname -s | tr '[:upper:]' '[:lower:]')"
[ "$os" = "darwin" ] && os="mac"
# Supported variables:
# - PHP_VERSION: PHP version to build (default: "8.4")
@@ -73,27 +76,8 @@ if [ -z "${PHP_VERSION}" ]; then
export PHP_VERSION
fi
# default extension set
defaultExtensions="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,memcache,memcached,mysqli,mysqlnd,opcache,openssl,password-argon2,parallel,pcntl,pdo,pdo_mysql,pdo_pgsql,pdo_sqlite,pdo_sqlsrv,pgsql,phar,posix,protobuf,readline,redis,session,shmop,simplexml,soap,sockets,sodium,sqlite3,ssh2,sysvmsg,sysvsem,sysvshm,tidy,tokenizer,xlswriter,xml,xmlreader,xmlwriter,xz,zip,zlib,yaml,zstd"
defaultExtensionLibs="libavif,nghttp2,nghttp3,ngtcp2"
md5binary="md5sum"
if [ "${os}" = "darwin" ]; then
os="mac"
md5binary="md5 -q"
fi
if [ "${os}" = "linux" ] && ! type "cmake" >/dev/null 2>&1; then
echo "The \"cmake\" command must be installed."
exit 1
fi
if [ "${os}" = "linux" ] && { [[ "${arch}" =~ "aarch" ]] || [[ "${arch}" =~ "arm" ]]; }; then
fpic="-fPIC"
fpie="-fPIE"
else
fpic="-fpic"
fpie="-fpie"
fi
defaultExtensions="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,memcache,memcached,mysqli,mysqlnd,opcache,openssl,password-argon2,parallel,pcntl,pdo,pdo_mysql,pdo_pgsql,pdo_sqlite,pdo_sqlsrv,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"
defaultExtensionLibs="libavif,nghttp2,nghttp3,ngtcp2,watcher"
if [ -z "${FRANKENPHP_VERSION}" ]; then
FRANKENPHP_VERSION="$(git rev-parse --verify HEAD)"
@@ -115,8 +99,6 @@ elif [ -d ".git/" ]; then
fi
fi
bin="frankenphp-${os}-${arch}"
if [ -n "${CLEAN}" ]; then
rm -Rf dist/
go clean -cache
@@ -194,153 +176,51 @@ if [ -n "${MIMALLOC}" ]; then
fi
fi
# Build libphp if necessary
cache_key="${PHP_VERSION}-${PHP_EXTENSIONS}-${PHP_EXTENSION_LIBS}"
if [ -f ../cache_key ] && [ "$(cat ../cache_key)" = "${cache_key}" ] && [ -f "buildroot/lib/libphp.a" ]; then
echo "Hit cache, skipping libphp build."
else
${spcCommand} doctor --auto-fix
# shellcheck disable=SC2086
${spcCommand} download --with-php="${PHP_VERSION}" --for-extensions="${PHP_EXTENSIONS}" --for-libs="${PHP_EXTENSION_LIBS}" ${SPC_OPT_DOWNLOAD_ARGS}
# shellcheck disable=SC2086
${spcCommand} build --enable-zts --build-embed ${SPC_OPT_BUILD_ARGS} "${PHP_EXTENSIONS}" --with-libs="${PHP_EXTENSION_LIBS}"
echo -n "${cache_key}" >../cache_key
fi
if ! type "go" >/dev/null 2>&1; then
echo "The \"go\" command must be installed."
exit 1
fi
XCADDY_COMMAND="xcaddy"
if ! type "$XCADDY_COMMAND" >/dev/null 2>&1; then
go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest
XCADDY_COMMAND="$(go env GOPATH)/bin/xcaddy"
fi
curlGitHubHeaders=(--header "X-GitHub-Api-Version: 2022-11-28")
if [ "${GITHUB_TOKEN}" ]; then
curlGitHubHeaders+=(--header "Authorization: Bearer ${GITHUB_TOKEN}")
fi
# Compile e-dant/watcher as a static library
mkdir -p watcher
cd watcher
curl -f --retry 5 "${curlGitHubHeaders[@]}" https://api.github.com/repos/e-dant/watcher/releases/latest |
grep tarball_url |
awk '{ print $2 }' |
sed 's/,$//' |
sed 's/"//g' |
xargs curl -fL --retry 5 "${curlGitHubHeaders[@]}" |
tar xz --strip-components 1
cd watcher-c
if [ -z "${CC}" ]; then
watcherCC=cc
else
watcherCC="${CC}"
fi
${watcherCC} -c -o libwatcher-c.o ./src/watcher-c.cpp -I ./include -I ../include -std=c++17 -Wall -Wextra "${fpic}"
ar rcs libwatcher-c.a libwatcher-c.o
cp libwatcher-c.a ../../buildroot/lib/libwatcher-c.a
mkdir -p ../../buildroot/include/wtr
cp -R include/wtr/watcher-c.h ../../buildroot/include/wtr/watcher-c.h
cd ../../
# See https://github.com/docker-library/php/blob/master/8.3/alpine3.20/zts/Dockerfile#L53-L55
CGO_CFLAGS="-DFRANKENPHP_VERSION=${FRANKENPHP_VERSION} -I${PWD}/buildroot/include/ $(${spcCommand} spc-config "${PHP_EXTENSIONS}" --with-libs="${PHP_EXTENSION_LIBS}" --includes)"
if [ -n "${DEBUG_SYMBOLS}" ]; then
CGO_CFLAGS="-g ${CGO_CFLAGS}"
else
CGO_CFLAGS="-fstack-protector-strong ${fpic} ${fpie} -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 ${CGO_CFLAGS}"
fi
export CGO_CFLAGS
export CGO_CPPFLAGS="${CGO_CFLAGS}"
if [ "${os}" = "mac" ]; then
export CGO_LDFLAGS="-framework CoreFoundation -framework SystemConfiguration"
elif [ "${os}" = "linux" ] && [ -z "${DEBUG_SYMBOLS}" ]; then
CGO_LDFLAGS="-Wl,-O1 -pie"
fi
if [ "${os}" = "linux" ] && [ "${SPC_LIBC}" = "glibc" ]; then
CGO_LDFLAGS="${CGO_LDFLAGS} -Wl,--allow-multiple-definition -Wl,--export-dynamic"
fi
CGO_LDFLAGS="${CGO_LDFLAGS} ${PWD}/buildroot/lib/libbrotlicommon.a ${PWD}/buildroot/lib/libbrotlienc.a ${PWD}/buildroot/lib/libbrotlidec.a ${PWD}/buildroot/lib/libwatcher-c.a $(${spcCommand} spc-config "${PHP_EXTENSIONS}" --with-libs="${PHP_EXTENSION_LIBS}" --libs)"
if [[ "$CGO_LDFLAGS" == *"${PWD}/buildroot/lib/mimalloc.o"* ]]; then
CGO_LDFLAGS=${CGO_LDFLAGS//${PWD}\/buildroot\/lib\/mimalloc.o/}
CGO_LDFLAGS="${PWD}/buildroot/lib/libmimalloc.a $CGO_LDFLAGS"
fi
if [ "${os}" = "linux" ] && [ "${SPC_LIBC}" = "glibc" ]; then
CGO_LDFLAGS="${CGO_LDFLAGS//-lphp/-Wl,--whole-archive -lphp -Wl,--no-whole-archive}"
# shellcheck disable=SC2046
ar d "${PWD}/buildroot/lib/libphp.a" $(ar t "${PWD}/buildroot/lib/libphp.a" | grep '\.a$')
fi
export CGO_LDFLAGS
LIBPHP_VERSION="$(./buildroot/bin/php-config --version)"
cd ../
if [ -z "${DEBUG_SYMBOLS}" ]; then
extraLdflags="-w -s"
fi
cd ../
# Embed PHP app, if any
if [ -n "${EMBED}" ] && [ -d "${EMBED}" ]; then
tar -cf app.tar -C "${EMBED}" .
${md5binary} app.tar | awk '{printf $1}' >app_checksum.txt
SPC_OPT_BUILD_ARGS="${SPC_OPT_BUILD_ARGS} --with-frankenphp-app=${EMBED}"
fi
if [ -z "${XCADDY_ARGS}" ]; then
XCADDY_ARGS="--with github.com/dunglas/caddy-cbrotli --with github.com/dunglas/mercure/caddy --with github.com/dunglas/vulcain/caddy"
SPC_OPT_INSTALL_ARGS="go-xcaddy"
if [ -z "${DEBUG_SYMBOLS}" ] && [ -z "${NO_COMPRESS}" ] && [ "${os}" = "linux" ]; then
SPC_OPT_BUILD_ARGS="${SPC_OPT_BUILD_ARGS} --with-upx-pack"
SPC_OPT_INSTALL_ARGS="${SPC_OPT_INSTALL_ARGS} upx"
fi
XCADDY_DEBUG=0
export SPC_DEFAULT_C_FLAGS="-fPIC -O2"
if [ -n "${DEBUG_SYMBOLS}" ]; then
XCADDY_DEBUG=1
fi
if [ "${SPC_LIBC}" = "musl" ]; then
muslStackSizeFix="-Wl,-z,stack-size=0x80000"
fi
go env
cd caddy/
if [ -z "${SPC_LIBC}" ] || [ "${SPC_LIBC}" = "musl" ]; then
xcaddyGoBuildFlags="-buildmode=pie -tags cgo,netgo,osusergo,static_build,nobadger,nomysql,nopgx -ldflags \"-linkmode=external -extldflags '-static-pie ${muslStackSizeFix}' ${extraLdflags} -X 'github.com/caddyserver/caddy/v2.CustomVersion=FrankenPHP ${FRANKENPHP_VERSION} PHP ${LIBPHP_VERSION} Caddy'\""
elif [ "${SPC_LIBC}" = "glibc" ]; then
xcaddyGoBuildFlags="-buildmode=pie -tags cgo,netgo,osusergo,nobadger,nomysql,nopgx -ldflags \"-linkmode=external -extldflags '-pie' ${extraLdflags} -X 'github.com/caddyserver/caddy/v2.CustomVersion=FrankenPHP ${FRANKENPHP_VERSION} PHP ${LIBPHP_VERSION} Caddy'\""
SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS="${SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS} -fPIE -g"
else
SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS="${SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS} -fPIE -fstack-protector-strong -O2 -w -s"
fi
export SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS
export SPC_CMD_VAR_FRANKENPHP_XCADDY_MODULES="--with github.com/dunglas/mercure/caddy --with github.com/dunglas/vulcain/caddy --with github.com/dunglas/caddy-cbrotli"
# Build FrankenPHP
${spcCommand} doctor --auto-fix
for pkg in ${SPC_OPT_INSTALL_ARGS}; do
${spcCommand} install-pkg "${pkg}"
done
# shellcheck disable=SC2086
CGO_ENABLED=1 \
XCADDY_GO_BUILD_FLAGS=${xcaddyGoBuildFlags} \
XCADDY_DEBUG="${XCADDY_DEBUG}" \
${XCADDY_COMMAND} build \
--output "../dist/${bin}" \
${XCADDY_ARGS} \
--with github.com/dunglas/frankenphp=.. \
--with github.com/dunglas/frankenphp/caddy=.
cd ..
${spcCommand} download --with-php="${PHP_VERSION}" --for-extensions="${PHP_EXTENSIONS}" --for-libs="${PHP_EXTENSION_LIBS}" ${SPC_OPT_DOWNLOAD_ARGS}
export FRANKENPHP_SOURCE_PATH="${CURRENT_DIR}"
# shellcheck disable=SC2086
${spcCommand} build --enable-zts --build-embed --build-frankenphp ${SPC_OPT_BUILD_ARGS} "${PHP_EXTENSIONS}" --with-libs="${PHP_EXTENSION_LIBS}"
if [ -d "${EMBED}" ]; then
truncate -s 0 app.tar
truncate -s 0 app_checksum.txt
if [ -n "$CI" ]; then
rm -rf ./downloads
rm -rf ./source
fi
if type "upx" >/dev/null 2>&1 && [ -z "${DEBUG_SYMBOLS}" ] && [ -z "${NO_COMPRESS}" ]; then
upx --best "dist/${bin}"
fi
cd ../..
"dist/${bin}" version
"dist/${bin}" build-info
bin="dist/frankenphp-${os}-${arch}"
cp "dist/static-php-cli/buildroot/bin/frankenphp" "${bin}"
"${bin}" version
"${bin}" build-info
if [ -n "${RELEASE}" ]; then
gh release upload "v${FRANKENPHP_VERSION}" "dist/${bin}" --repo dunglas/frankenphp --clobber
gh release upload "v${FRANKENPHP_VERSION}" "${bin}" --repo dunglas/frankenphp --clobber
fi
if [ -n "${CURRENT_REF}" ]; then

View File

@@ -4,12 +4,13 @@ import (
"bytes"
"encoding/json"
"fmt"
"github.com/dunglas/frankenphp/internal/fastabs"
"io"
"net/http"
"sync"
"testing"
"github.com/dunglas/frankenphp/internal/fastabs"
"github.com/caddyserver/caddy/v2/caddytest"
"github.com/dunglas/frankenphp"
"github.com/stretchr/testify/assert"
@@ -94,7 +95,10 @@ func TestAutoScaleWorkerThreads(t *testing.T) {
frankenphp {
max_threads 10
num_threads 2
worker ../testdata/sleep.php 1
worker ../testdata/sleep.php {
num 1
max_threads 3
}
}
}
@@ -128,8 +132,8 @@ func TestAutoScaleWorkerThreads(t *testing.T) {
}
}
// assert that there are now more threads than before
assert.NotEqual(t, amountOfThreads, 2)
assert.NotEqual(t, amountOfThreads, 2, "at least one thread should have been auto-scaled")
assert.LessOrEqual(t, amountOfThreads, 4, "at most 3 max_threads + 1 regular thread should be present")
}
// Note this test requires at least 2x40MB available memory for the process

View File

@@ -1,12 +1,14 @@
package caddy
import (
"context"
"errors"
"fmt"
"log/slog"
"path/filepath"
"strconv"
"strings"
"sync"
"time"
"github.com/caddyserver/caddy/v2"
@@ -18,6 +20,22 @@ import (
"github.com/dunglas/frankenphp/internal/fastabs"
)
var (
options []frankenphp.Option
optionsMU sync.RWMutex
)
// EXPERIMENTAL: RegisterWorkers provides a way for extensions to register frankenphp.Workers
func RegisterWorkers(name, fileName string, num int, wo ...frankenphp.WorkerOption) frankenphp.Workers {
w, opt := frankenphp.WithExtensionWorkers(name, fileName, num, wo...)
optionsMU.Lock()
options = append(options, opt)
optionsMU.Unlock()
return w
}
// FrankenPHPApp represents the global "frankenphp" directive in the Caddyfile
// it's responsible for starting up the global PHP instance and all threads
//
@@ -31,18 +49,20 @@ type FrankenPHPApp struct {
NumThreads int `json:"num_threads,omitempty"`
// MaxThreads limits how many threads can be started at runtime. Default 2x NumThreads
MaxThreads int `json:"max_threads,omitempty"`
// Workers configures the worker scripts to start.
// Workers configures the worker scripts to start
Workers []workerConfig `json:"workers,omitempty"`
// Overwrites the default php ini configuration
PhpIni map[string]string `json:"php_ini,omitempty"`
// The maximum amount of time a request may be stalled waiting for a thread
MaxWaitTime time.Duration `json:"max_wait_time,omitempty"`
opts []frankenphp.Option
metrics frankenphp.Metrics
ctx context.Context
logger *slog.Logger
}
var iniError = errors.New("'php_ini' must be in the format: php_ini \"<key>\" \"<value>\"")
var iniError = errors.New(`"php_ini" must be in the format: php_ini "<key>" "<value>"`)
// CaddyModule returns the Caddy module information.
func (f FrankenPHPApp) CaddyModule() caddy.ModuleInfo {
@@ -54,8 +74,12 @@ func (f FrankenPHPApp) CaddyModule() caddy.ModuleInfo {
// Provision sets up the module.
func (f *FrankenPHPApp) Provision(ctx caddy.Context) error {
f.ctx = ctx
f.logger = ctx.Slogger()
// We have at least 7 hardcoded options
f.opts = make([]frankenphp.Option, 0, 7+len(options))
if httpApp, err := ctx.AppIfConfigured("http"); err == nil {
if httpApp.(*caddyhttp.App).Metrics != nil {
f.metrics = frankenphp.NewPrometheusMetrics(ctx.GetMetricsRegistry())
@@ -94,42 +118,54 @@ retry:
func (f *FrankenPHPApp) addModuleWorkers(workers ...workerConfig) ([]workerConfig, error) {
for i := range workers {
w := &workers[i]
if frankenphp.EmbeddedAppPath != "" && filepath.IsLocal(w.FileName) {
w.FileName = filepath.Join(frankenphp.EmbeddedAppPath, w.FileName)
}
if w.Name == "" {
w.Name = f.generateUniqueModuleWorkerName(w.FileName)
} else if !strings.HasPrefix(w.Name, "m#") {
w.Name = "m#" + w.Name
}
f.Workers = append(f.Workers, *w)
}
return workers, nil
}
func (f *FrankenPHPApp) Start() error {
repl := caddy.NewReplacer()
opts := []frankenphp.Option{
optionsMU.RLock()
f.opts = append(f.opts, options...)
optionsMU.RUnlock()
f.opts = append(f.opts,
frankenphp.WithContext(f.ctx),
frankenphp.WithLogger(f.logger),
frankenphp.WithNumThreads(f.NumThreads),
frankenphp.WithMaxThreads(f.MaxThreads),
frankenphp.WithLogger(f.logger),
frankenphp.WithMetrics(f.metrics),
frankenphp.WithPhpIni(f.PhpIni),
frankenphp.WithMaxWaitTime(f.MaxWaitTime),
}
for _, w := range append(f.Workers) {
workerOpts := []frankenphp.WorkerOption{
)
for _, w := range f.Workers {
w.options = append(w.options,
frankenphp.WithWorkerEnv(w.Env),
frankenphp.WithWorkerWatchMode(w.Watch),
frankenphp.WithWorkerMaxFailures(w.MaxConsecutiveFailures),
}
frankenphp.WithWorkerMaxThreads(w.MaxThreads),
frankenphp.WithWorkerRequestOptions(w.requestOptions...),
)
opts = append(opts, frankenphp.WithWorkers(w.Name, repl.ReplaceKnown(w.FileName, ""), w.Num, workerOpts...))
f.opts = append(f.opts, frankenphp.WithWorkers(w.Name, repl.ReplaceKnown(w.FileName, ""), w.Num, w.options...))
}
frankenphp.Shutdown()
if err := frankenphp.Init(opts...); err != nil {
if err := frankenphp.Init(f.opts...); err != nil {
return err
}
@@ -137,7 +173,11 @@ func (f *FrankenPHPApp) Start() error {
}
func (f *FrankenPHPApp) Stop() error {
f.logger.Info("FrankenPHP stopped 🐘")
ctx := caddy.ActiveContext()
if f.logger.Enabled(caddy.ActiveContext(), slog.LevelInfo) {
f.logger.LogAttrs(ctx, slog.LevelInfo, "FrankenPHP stopped 🐘")
}
// attempt a graceful shutdown if caddy is exiting
// note: Exiting() is currently marked as 'experimental'
@@ -151,6 +191,10 @@ func (f *FrankenPHPApp) Stop() error {
f.NumThreads = 0
f.MaxWaitTime = 0
optionsMU.Lock()
options = nil
optionsMU.Unlock()
return nil
}
@@ -194,7 +238,7 @@ func (f *FrankenPHPApp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
v, err := time.ParseDuration(d.Val())
if err != nil {
return errors.New("max_wait_time must be a valid duration (example: 10s)")
return d.Err("max_wait_time must be a valid duration (example: 10s)")
}
f.MaxWaitTime = v
@@ -202,14 +246,14 @@ func (f *FrankenPHPApp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
parseIniLine := func(d *caddyfile.Dispenser) error {
key := d.Val()
if !d.NextArg() {
return iniError
return d.WrapErr(iniError)
}
if f.PhpIni == nil {
f.PhpIni = make(map[string]string)
}
f.PhpIni[key] = d.Val()
if d.NextArg() {
return iniError
return d.WrapErr(iniError)
}
return nil
@@ -226,7 +270,7 @@ func (f *FrankenPHPApp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
if !isBlock {
if !d.NextArg() {
return iniError
return d.WrapErr(iniError)
}
err := parseIniLine(d)
if err != nil {
@@ -235,7 +279,7 @@ func (f *FrankenPHPApp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
}
case "worker":
wc, err := parseWorkerConfig(d)
wc, err := unmarshalWorker(d)
if err != nil {
return err
}
@@ -243,25 +287,24 @@ func (f *FrankenPHPApp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
wc.FileName = filepath.Join(frankenphp.EmbeddedAppPath, wc.FileName)
}
if strings.HasPrefix(wc.Name, "m#") {
return fmt.Errorf(`global worker names must not start with "m#": %q`, wc.Name)
return d.Errf(`global worker names must not start with "m#": %q`, wc.Name)
}
// check for duplicate workers
for _, existingWorker := range f.Workers {
if existingWorker.FileName == wc.FileName {
return fmt.Errorf("global workers must not have duplicate filenames: %q", wc.FileName)
return d.Errf("global workers must not have duplicate filenames: %q", wc.FileName)
}
}
f.Workers = append(f.Workers, wc)
default:
allowedDirectives := "num_threads, max_threads, php_ini, worker, max_wait_time"
return wrongSubDirectiveError("frankenphp", allowedDirectives, d.Val())
return wrongSubDirectiveError("frankenphp", "num_threads, max_threads, php_ini, worker, max_wait_time", d.Val())
}
}
}
if f.MaxThreads > 0 && f.NumThreads > 0 && f.MaxThreads < f.NumThreads {
return errors.New(`"max_threads"" must be greater than or equal to "num_threads"`)
return d.Err(`"max_threads"" must be greater than or equal to "num_threads"`)
}
return nil
@@ -279,3 +322,8 @@ func parseGlobalOption(d *caddyfile.Dispenser, _ any) (any, error) {
Value: caddyconfig.JSON(app, nil),
}, nil
}
var (
_ caddy.App = (*FrankenPHPApp)(nil)
_ caddy.Provisioner = (*FrankenPHPApp)(nil)
)

View File

@@ -7,14 +7,12 @@ import (
"fmt"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
)
const (
defaultDocumentRoot = "public"
defaultWatchPattern = "./**/*.{php,yaml,yml,twig,env}"
defaultWatchPattern = "./**/*.{env,php,twig,yaml,yml}"
)
func init() {
@@ -31,16 +29,7 @@ func init() {
httpcaddyfile.RegisterDirectiveOrder("php_server", "before", "file_server")
}
// return a nice error message
func wrongSubDirectiveError(module string, allowedDriectives string, wrongValue string) error {
return fmt.Errorf("unknown '%s' subdirective: '%s' (allowed directives are: %s)", module, wrongValue, allowedDriectives)
// wrongSubDirectiveError returns a nice error message.
func wrongSubDirectiveError(module string, allowedDirectives string, wrongValue string) error {
return fmt.Errorf("unknown %q subdirective: %s (allowed directives are: %s)", module, wrongValue, allowedDirectives)
}
// Interface guards
var (
_ caddy.App = (*FrankenPHPApp)(nil)
_ caddy.Provisioner = (*FrankenPHPApp)(nil)
_ caddy.Provisioner = (*FrankenPHPModule)(nil)
_ caddyhttp.MiddlewareHandler = (*FrankenPHPModule)(nil)
_ caddyfile.Unmarshaler = (*FrankenPHPModule)(nil)
)

View File

@@ -168,6 +168,28 @@ func TestGlobalAndModuleWorker(t *testing.T) {
wg.Wait()
}
func TestModuleWorkerInheritsEnv(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
skip_install_trust
admin localhost:2999
}
http://localhost:`+testPort+` {
route {
php {
root ../testdata
env APP_ENV inherit_this
worker worker-with-env.php
}
}
}
`, "caddyfile")
tester.AssertGetResponse("http://localhost:"+testPort+"/worker-with-env.php", http.StatusOK, "Worker has APP_ENV=inherit_this")
}
func TestNamedModuleWorkers(t *testing.T) {
var wg sync.WaitGroup
testPortNum, _ := strconv.Atoi(testPort)
@@ -385,7 +407,7 @@ func TestPHPServerDirective(t *testing.T) {
`, "caddyfile")
tester.AssertGetResponse("http://localhost:"+testPort, http.StatusOK, "I am by birth a Genevese (i not set)")
tester.AssertGetResponse("http://localhost:"+testPort+"/hello.txt", http.StatusOK, "Hello")
tester.AssertGetResponse("http://localhost:"+testPort+"/hello.txt", http.StatusOK, "Hello\n")
tester.AssertGetResponse("http://localhost:"+testPort+"/not-found.txt", http.StatusOK, "I am by birth a Genevese (i not set)")
}
@@ -943,7 +965,7 @@ func TestMaxWaitTime(t *testing.T) {
for range 10 {
go func() {
statusCode := getStatusCode("http://localhost:"+testPort+"/sleep.php?sleep=10", t)
if statusCode == http.StatusGatewayTimeout {
if statusCode == http.StatusServiceUnavailable {
success.Store(true)
}
wg.Done()
@@ -951,7 +973,7 @@ func TestMaxWaitTime(t *testing.T) {
}
wg.Wait()
require.True(t, success.Load(), "At least one request should have failed with a 504 Gateway Timeout status")
require.True(t, success.Load(), "At least one request should have failed with a 503 Service Unavailable status")
}
func TestMaxWaitTimeWorker(t *testing.T) {
@@ -990,23 +1012,26 @@ func TestMaxWaitTimeWorker(t *testing.T) {
for range 10 {
go func() {
statusCode := getStatusCode("http://localhost:"+testPort+"/sleep.php?sleep=10&iteration=1", t)
if statusCode == http.StatusGatewayTimeout {
if statusCode == http.StatusServiceUnavailable {
success.Store(true)
}
wg.Done()
}()
}
wg.Wait()
require.True(t, success.Load(), "At least one request should have failed with a 504 Gateway Timeout status")
require.True(t, success.Load(), "At least one request should have failed with a 503 Service Unavailable status")
// Fetch metrics
resp, err := http.Get("http://localhost:2999/metrics")
require.NoError(t, err, "failed to fetch metrics")
defer resp.Body.Close()
t.Cleanup(func() {
require.NoError(t, resp.Body.Close())
})
// Read and parse metrics
metrics := new(bytes.Buffer)
_, err = metrics.ReadFrom(resp.Body)
require.NoError(t, err)
expectedMetrics := `
# TYPE frankenphp_worker_queue_depth gauge
@@ -1337,7 +1362,7 @@ func TestWorkerMatchDirective(t *testing.T) {
}
`, "caddyfile")
// worker is outside of public directory, match anyways
// worker is outside public directory, match anyway
tester.AssertGetResponse("http://localhost:"+testPort+"/matched-path", http.StatusOK, "requests:1")
tester.AssertGetResponse("http://localhost:"+testPort+"/matched-path/anywhere", http.StatusOK, "requests:2")
@@ -1423,3 +1448,55 @@ func TestWorkerMatchDirectiveWithoutFileServer(t *testing.T) {
// the request should completely fall through the php_server module
tester.AssertGetResponse("http://localhost:"+testPort+"/static.txt", http.StatusNotFound, "Request falls through")
}
func TestDd(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
skip_install_trust
admin localhost:2999
}
http://localhost:`+testPort+` {
php {
worker ../testdata/dd.php 1 {
match *
}
}
`, "caddyfile")
// simulate Symfony's dd()
tester.AssertGetResponse(
"http://localhost:"+testPort+"/some-path?output=dump123",
http.StatusInternalServerError,
"dump123",
)
}
func TestLog(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
skip_install_trust
admin localhost:2999
}
http://localhost:`+testPort+` {
log {
output stdout
format json
}
root ../testdata
php_server {
worker ../testdata/log-frankenphp_log.php
}
}
`, "caddyfile")
tester.AssertGetResponse(
"http://localhost:"+testPort+"/log-frankenphp_log.php?i=0",
http.StatusOK,
"",
)
}

View File

@@ -181,7 +181,7 @@ func TestModuleWorkerWithWatchConfiguration(t *testing.T) {
// Verify that the watch directories were set correctly
require.Len(t, module.Workers[0].Watch, 3, "Expected three watch patterns")
require.Equal(t, "./**/*.{php,yaml,yml,twig,env}", module.Workers[0].Watch[0], "First watch pattern should be the default")
require.Equal(t, defaultWatchPattern, module.Workers[0].Watch[0], "First watch pattern should be the default")
require.Equal(t, "./src/**/*.php", module.Workers[0].Watch[1], "Second watch pattern should match the configuration")
require.Equal(t, "./config/**/*.yaml", module.Workers[0].Watch[2], "Third watch pattern should match the configuration")
}

View File

@@ -2,12 +2,13 @@ package caddy
import (
"errors"
"github.com/dunglas/frankenphp/internal/extgen"
"log"
"os"
"path/filepath"
"strings"
"github.com/dunglas/frankenphp/internal/extgen"
caddycmd "github.com/caddyserver/caddy/v2/cmd"
"github.com/spf13/cobra"
)
@@ -16,7 +17,7 @@ func init() {
caddycmd.RegisterCommand(caddycmd.Command{
Name: "extension-init",
Usage: "go_extension.go [--verbose]",
Short: "(Experimental) Initializes a PHP extension from a Go file",
Short: "Initializes a PHP extension from a Go file (EXPERIMENTAL)",
Long: `
Initializes 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.`,
CobraFunc: func(cmd *cobra.Command) {
@@ -27,27 +28,21 @@ Initializes a PHP extension from a Go file. This command generates the necessary
})
}
func cmdInitExtension(fs caddycmd.Flags) (int, error) {
func cmdInitExtension(_ caddycmd.Flags) (int, error) {
if len(os.Args) < 3 {
return 1, errors.New("the path to the Go source is required")
}
sourceFile := os.Args[2]
baseName := extgen.SanitizePackageName(strings.TrimSuffix(filepath.Base(sourceFile), ".go"))
baseName := strings.TrimSuffix(filepath.Base(sourceFile), ".go")
baseName = extgen.SanitizePackageName(baseName)
sourceDir := filepath.Dir(sourceFile)
buildDir := filepath.Join(sourceDir, "build")
generator := extgen.Generator{BaseName: baseName, SourceFile: sourceFile, BuildDir: buildDir}
generator := extgen.Generator{BaseName: baseName, SourceFile: sourceFile, BuildDir: filepath.Dir(sourceFile)}
if err := generator.Generate(); err != nil {
return 1, err
}
log.Printf("PHP extension %q initialized successfully in %q", baseName, generator.BuildDir)
log.Printf("PHP extension %q initialized successfully in directory %q", baseName, generator.BuildDir)
return 0, nil
}

View File

@@ -30,8 +30,6 @@
# Uncomment the following lines to enable Mercure and Vulcain modules
#mercure {
# # Publisher JWT key
# transport_url {$MERCURE_TRANSPORT_URL:bolt:///data/mercure.db}
# # Publisher JWT key
# publisher_jwt {env.MERCURE_PUBLISHER_JWT_KEY} {env.MERCURE_PUBLISHER_JWT_ALG}
# # Subscriber JWT key
# subscriber_jwt {env.MERCURE_SUBSCRIBER_JWT_KEY} {env.MERCURE_SUBSCRIBER_JWT_ALG}

View File

@@ -1,6 +1,6 @@
module github.com/dunglas/frankenphp/caddy
go 1.25.0
go 1.25.4
replace github.com/dunglas/frankenphp => ../
@@ -8,49 +8,49 @@ retract v1.0.0-rc.1 // Human error
require (
github.com/caddyserver/caddy/v2 v2.10.2
github.com/caddyserver/certmagic v0.24.0
github.com/caddyserver/certmagic v0.25.0
github.com/dunglas/caddy-cbrotli v1.0.1
github.com/dunglas/frankenphp v1.9.1
github.com/dunglas/mercure/caddy v0.20.2
github.com/dunglas/frankenphp v1.11.1
github.com/dunglas/mercure v0.21.4
github.com/dunglas/mercure/caddy v0.21.4
github.com/dunglas/vulcain/caddy v1.2.1
github.com/prometheus/client_golang v1.23.0
github.com/spf13/cobra v1.10.0
github.com/prometheus/client_golang v1.23.2
github.com/spf13/cobra v1.10.2
github.com/stretchr/testify v1.11.1
)
require github.com/smallstep/go-attestation v0.4.4-0.20241119153605-2306d5b464ca // indirect
require (
cel.dev/expr v0.24.0 // indirect
cloud.google.com/go/auth v0.16.5 // indirect
cel.dev/expr v0.25.1 // indirect
cloud.google.com/go/auth v0.18.0 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.8.0 // indirect
cloud.google.com/go/compute/metadata v0.9.0 // indirect
dario.cat/mergo v1.0.2 // indirect
filippo.io/edwards25519 v1.1.0 // indirect
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect
github.com/BurntSushi/toml v1.5.0 // indirect
github.com/KimMachineGun/automemlimit v0.7.4 // indirect
github.com/KimMachineGun/automemlimit v0.7.5 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.4.0 // indirect
github.com/Masterminds/sprig/v3 v3.3.0 // indirect
github.com/MauriceGit/skiplist v0.0.0-20211105230623-77f5c8d3e145 // indirect
github.com/MicahParks/jwkset v0.9.6 // indirect
github.com/MicahParks/keyfunc/v3 v3.6.1 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/RoaringBitmap/roaring v1.9.4 // indirect
github.com/alecthomas/chroma/v2 v2.20.0 // indirect
github.com/MicahParks/jwkset v0.11.0 // indirect
github.com/MicahParks/keyfunc/v3 v3.7.0 // indirect
github.com/RoaringBitmap/roaring/v2 v2.14.4 // indirect
github.com/alecthomas/chroma/v2 v2.21.0 // indirect
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bits-and-blooms/bitset v1.24.0 // indirect
github.com/bits-and-blooms/bitset v1.24.4 // indirect
github.com/caddyserver/zerossl v0.1.3 // indirect
github.com/ccoveille/go-safecast v1.6.1 // indirect
github.com/ccoveille/go-safecast/v2 v2.0.0 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash v1.1.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/chzyer/readline v1.5.1 // indirect
github.com/cloudflare/circl v1.6.1 // indirect
github.com/coreos/go-oidc/v3 v3.15.0 // indirect
github.com/coreos/go-oidc/v3 v3.17.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dgraph-io/badger v1.6.2 // indirect
@@ -58,64 +58,60 @@ require (
github.com/dgraph-io/ristretto v0.2.0 // indirect
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/dolthub/maphash v0.1.0 // indirect
github.com/dunglas/httpsfv v1.1.0 // indirect
github.com/dunglas/mercure v0.20.2 // indirect
github.com/dunglas/skipfilter v1.0.0 // indirect
github.com/dunglas/vulcain v1.2.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/e-dant/watcher/watcher-go v0.0.0-20251208164151-f88ec3b7e146 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/francoispqt/gojay v1.2.13 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/gammazero/deque v1.1.0 // indirect
github.com/getkin/kin-openapi v0.133.0 // indirect
github.com/go-chi/chi/v5 v5.2.3 // indirect
github.com/go-jose/go-jose/v3 v3.0.4 // indirect
github.com/go-jose/go-jose/v4 v4.1.2 // indirect
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.21.2 // indirect
github.com/go-openapi/swag v0.23.1 // indirect
github.com/go-openapi/jsonpointer v0.22.4 // indirect
github.com/go-openapi/swag/jsonname v0.25.4 // indirect
github.com/go-sql-driver/mysql v1.9.3 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/gofrs/uuid v4.4.0+incompatible // indirect
github.com/gofrs/uuid/v5 v5.3.2 // indirect
github.com/gofrs/uuid/v5 v5.4.0 // indirect
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/golang/snappy v1.0.0 // indirect
github.com/google/brotli/go/cbrotli v1.1.0 // indirect
github.com/google/cel-go v0.26.1 // indirect
github.com/google/certificate-transparency-go v1.3.2 // indirect
github.com/google/go-tpm v0.9.5 // indirect
github.com/google/go-tpm v0.9.7 // indirect
github.com/google/go-tspi v0.3.0 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
github.com/gorilla/handlers v1.5.2 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect
github.com/hashicorp/golang-lru v1.0.2 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect
github.com/huandu/xstrings v1.5.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.7.5 // indirect
github.com/jackc/pgx/v5 v5.7.6 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/kevburnsjr/skipfilter v0.0.1 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/compress v1.18.2 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/libdns/libdns v1.1.0 // indirect
github.com/mailru/easyjson v0.9.0 // indirect
github.com/libdns/libdns v1.1.1 // indirect
github.com/mailru/easyjson v0.9.1 // indirect
github.com/manifoldco/promptui v0.9.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/maypok86/otter v1.2.4 // indirect
github.com/maypok86/otter/v2 v2.2.1 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/mholt/acmez/v3 v3.1.2 // indirect
github.com/miekg/dns v1.1.68 // indirect
github.com/mholt/acmez/v3 v3.1.4 // indirect
github.com/miekg/dns v1.1.69 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-ps v1.0.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
@@ -131,34 +127,35 @@ require (
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.65.0 // indirect
github.com/prometheus/procfs v0.17.0 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.54.0 // indirect
github.com/prometheus/common v0.67.4 // indirect
github.com/prometheus/procfs v0.19.2 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.57.1 // indirect
github.com/rs/cors v1.11.1 // indirect
github.com/rs/xid v1.6.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sagikazarmark/locafero v0.10.0 // indirect
github.com/sagikazarmark/locafero v0.12.0 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/slackhq/nebula v1.9.6 // indirect
github.com/smallstep/certificates v0.28.4 // indirect
github.com/smallstep/cli-utils v0.12.1 // indirect
github.com/smallstep/linkedca v0.23.0 // indirect
github.com/slackhq/nebula v1.9.7 // indirect
github.com/smallstep/certificates v0.29.0 // indirect
github.com/smallstep/cli-utils v0.12.2 // indirect
github.com/smallstep/linkedca v0.25.0 // indirect
github.com/smallstep/nosql v0.7.0 // indirect
github.com/smallstep/pkcs7 v0.2.1 // indirect
github.com/smallstep/scep v0.0.0-20250318231241-a25cabb69492 // indirect
github.com/smallstep/truststore v0.13.0 // indirect
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
github.com/spf13/afero v1.14.0 // indirect
github.com/spf13/cast v1.9.2 // indirect
github.com/spf13/pflag v1.0.8 // indirect
github.com/spf13/viper v1.20.1 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/spf13/viper v1.21.0 // indirect
github.com/stoewer/go-strcase v1.3.1 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53 // indirect
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect
github.com/tailscale/tscert v0.0.0-20251216020129-aea342f6d747 // indirect
github.com/tidwall/gjson v1.18.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/match v1.2.0 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
github.com/unrolled/secure v1.17.0 // indirect
@@ -170,44 +167,45 @@ require (
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc // indirect
github.com/zeebo/blake3 v0.2.4 // indirect
go.etcd.io/bbolt v1.4.3 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 // indirect
go.opentelemetry.io/contrib/propagators/autoprop v0.62.0 // indirect
go.opentelemetry.io/contrib/propagators/aws v1.37.0 // indirect
go.opentelemetry.io/contrib/propagators/b3 v1.37.0 // indirect
go.opentelemetry.io/contrib/propagators/jaeger v1.37.0 // indirect
go.opentelemetry.io/contrib/propagators/ot v1.37.0 // indirect
go.opentelemetry.io/otel v1.37.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 // indirect
go.opentelemetry.io/otel/metric v1.37.0 // indirect
go.opentelemetry.io/otel/sdk v1.37.0 // indirect
go.opentelemetry.io/otel/trace v1.37.0 // indirect
go.opentelemetry.io/proto/otlp v1.7.1 // indirect
go.step.sm/crypto v0.70.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 // indirect
go.opentelemetry.io/contrib/propagators/autoprop v0.64.0 // indirect
go.opentelemetry.io/contrib/propagators/aws v1.39.0 // indirect
go.opentelemetry.io/contrib/propagators/b3 v1.39.0 // indirect
go.opentelemetry.io/contrib/propagators/jaeger v1.39.0 // indirect
go.opentelemetry.io/contrib/propagators/ot v1.39.0 // indirect
go.opentelemetry.io/otel v1.39.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 // indirect
go.opentelemetry.io/otel/metric v1.39.0 // indirect
go.opentelemetry.io/otel/sdk v1.39.0 // indirect
go.opentelemetry.io/otel/trace v1.39.0 // indirect
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
go.step.sm/crypto v0.75.0 // indirect
go.uber.org/automaxprocs v1.6.0 // indirect
go.uber.org/mock v0.6.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
go.uber.org/zap v1.27.1 // indirect
go.uber.org/zap/exp v0.3.0 // indirect
golang.org/x/crypto v0.41.0 // indirect
golang.org/x/crypto/x509roots/fallback v0.0.0-20250826074233-8f580defa01d // indirect
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b // indirect
golang.org/x/mod v0.27.0 // indirect
golang.org/x/net v0.43.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.35.0 // indirect
golang.org/x/term v0.34.0 // indirect
golang.org/x/text v0.28.0 // indirect
golang.org/x/time v0.12.0 // indirect
golang.org/x/tools v0.36.0 // indirect
google.golang.org/api v0.248.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250826171959-ef028d996bc1 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1 // indirect
google.golang.org/grpc v1.75.0 // indirect
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 // indirect
google.golang.org/protobuf v1.36.8 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.46.0 // indirect
golang.org/x/crypto/x509roots/fallback v0.0.0-20251210140736-7dacc380ba00 // indirect
golang.org/x/exp v0.0.0-20251209150349-8475f28825e9 // indirect
golang.org/x/mod v0.31.0 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/oauth2 v0.34.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/term v0.38.0 // indirect
golang.org/x/text v0.32.0 // indirect
golang.org/x/time v0.14.0 // indirect
golang.org/x/tools v0.40.0 // indirect
google.golang.org/api v0.257.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // indirect
google.golang.org/grpc v1.77.0 // indirect
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.6.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
howett.net/plist v1.0.1 // indirect

View File

@@ -1,119 +1,101 @@
cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY=
cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo=
cloud.google.com/go v0.120.0 h1:wc6bgG9DHyKqF5/vQvX1CiZrtHnxJjBlKUyF9nP6meA=
cloud.google.com/go v0.120.0/go.mod h1:/beW32s8/pGRuj4IILWQNd4uuebeT4dkOhKmkfit64Q=
cloud.google.com/go/auth v0.16.5 h1:mFWNQ2FEVWAliEQWpAdH80omXFokmrnbDhUS9cBywsI=
cloud.google.com/go/auth v0.16.5/go.mod h1:utzRfHMP+Vv0mpOkTRQoWD2q3BatTOoWbA7gCc2dUhQ=
cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
cloud.google.com/go v0.121.6 h1:waZiuajrI28iAf40cWgycWNgaXPO06dupuS+sgibK6c=
cloud.google.com/go v0.121.6/go.mod h1:coChdst4Ea5vUpiALcYKXEpR1S9ZgXbhEzzMcMR66vI=
cloud.google.com/go/auth v0.18.0 h1:wnqy5hrv7p3k7cShwAU/Br3nzod7fxoqG+k0VZ+/Pk0=
cloud.google.com/go/auth v0.18.0/go.mod h1:wwkPM1AgE1f2u6dG443MiWoD8C3BtOywNsUMcUTVDRo=
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
cloud.google.com/go/compute/metadata v0.8.0 h1:HxMRIbao8w17ZX6wBnjhcDkW6lTFpgcaobyVfZWqRLA=
cloud.google.com/go/compute/metadata v0.8.0/go.mod h1:sYOGTp851OV9bOFJ9CH7elVvyzopvWQFNNghtDQ/Biw=
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
cloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8=
cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE=
cloud.google.com/go/kms v1.22.0 h1:dBRIj7+GDeeEvatJeTB19oYZNV0aj6wEqSIT/7gLqtk=
cloud.google.com/go/kms v1.22.0/go.mod h1:U7mf8Sva5jpOb4bxYZdtw/9zsbIjrklYwPcvMk34AL8=
cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE=
cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY=
cloud.google.com/go/kms v1.23.2 h1:4IYDQL5hG4L+HzJBhzejUySoUOheh3Lk5YT4PCyyW6k=
cloud.google.com/go/kms v1.23.2/go.mod h1:rZ5kK0I7Kn9W4erhYVoIRPtpizjunlrfU4fUkumUp8g=
cloud.google.com/go/longrunning v0.7.0 h1:FV0+SYF1RIj59gyoWDRi45GiYUMM3K1qO51qoboQT1E=
cloud.google.com/go/longrunning v0.7.0/go.mod h1:ySn2yXmjbK9Ba0zsQqunhDkYi0+9rlXIwnoAf+h+TPY=
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU=
dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU=
dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4=
dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIoKjsnZuH8vjyaysT/ses3EvZeaV/1UkF2M=
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/KimMachineGun/automemlimit v0.7.4 h1:UY7QYOIfrr3wjjOAqahFmC3IaQCLWvur9nmfIn6LnWk=
github.com/KimMachineGun/automemlimit v0.7.4/go.mod h1:QZxpHaGOQoYvFhv/r4u3U0JTC2ZcOwbSr11UZF46UBM=
github.com/KimMachineGun/automemlimit v0.7.5 h1:RkbaC0MwhjL1ZuBKunGDjE/ggwAX43DwZrJqVwyveTk=
github.com/KimMachineGun/automemlimit v0.7.5/go.mod h1:QZxpHaGOQoYvFhv/r4u3U0JTC2ZcOwbSr11UZF46UBM=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=
github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
github.com/MauriceGit/skiplist v0.0.0-20191117202105-643e379adb62/go.mod h1:877WBceefKn14QwVVn4xRFUsHsZb9clICgdeTj4XsUg=
github.com/MauriceGit/skiplist v0.0.0-20211105230623-77f5c8d3e145 h1:1yw6O62BReQ+uA1oyk9XaQTvLhcoHWmoQAgXmDFXpIY=
github.com/MauriceGit/skiplist v0.0.0-20211105230623-77f5c8d3e145/go.mod h1:877WBceefKn14QwVVn4xRFUsHsZb9clICgdeTj4XsUg=
github.com/MicahParks/jwkset v0.9.6 h1:Tf8l2/MOby5Kh3IkrqzThPQKfLytMERoAsGZKlyYZxg=
github.com/MicahParks/jwkset v0.9.6/go.mod h1:U2oRhRaLgDCLjtpGL2GseNKGmZtLs/3O7p+OZaL5vo0=
github.com/MicahParks/keyfunc/v3 v3.6.1 h1:A8A5zGZ8XmRyxizSY7s5FLY/aSplrnEBLCOrC0D1ojM=
github.com/MicahParks/keyfunc/v3 v3.6.1/go.mod h1:y6Ed3dMgNKTcpxbaQHD8mmrYDUZWJAxteddA6OQj+ag=
github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/MicahParks/jwkset v0.11.0 h1:yc0zG+jCvZpWgFDFmvs8/8jqqVBG9oyIbmBtmjOhoyQ=
github.com/MicahParks/jwkset v0.11.0/go.mod h1:U2oRhRaLgDCLjtpGL2GseNKGmZtLs/3O7p+OZaL5vo0=
github.com/MicahParks/keyfunc/v3 v3.7.0 h1:pdafUNyq+p3ZlvjJX1HWFP7MA3+cLpDtg69U3kITJGM=
github.com/MicahParks/keyfunc/v3 v3.7.0/go.mod h1:z66bkCviwqfg2YUp+Jcc/xRE9IXLcMq6DrgV/+Htru0=
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/RoaringBitmap/roaring v0.9.4/go.mod h1:icnadbWcNyfEHlYdr+tDlOTih1Bf/h+rzPpv4sbomAA=
github.com/RoaringBitmap/roaring v1.9.4 h1:yhEIoH4YezLYT04s1nHehNO64EKFTop/wBhxv2QzDdQ=
github.com/RoaringBitmap/roaring v1.9.4/go.mod h1:6AXUsoIEzDTFFQCe1RbGA6uFONMhvejWj5rqITANK90=
github.com/RoaringBitmap/roaring/v2 v2.14.4 h1:4aKySrrg9G/5oRtJ3TrZLObVqxgQ9f1znCRBwEwjuVw=
github.com/RoaringBitmap/roaring/v2 v2.14.4/go.mod h1:oMvV6omPWr+2ifRdeZvVJyaz+aoEUopyv5iH0u/+wbY=
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
github.com/alecthomas/chroma/v2 v2.20.0 h1:sfIHpxPyR07/Oylvmcai3X/exDlE8+FA820NTz+9sGw=
github.com/alecthomas/chroma/v2 v2.20.0/go.mod h1:e7tViK0xh/Nf4BYHl00ycY6rV7b8iXBksI9E359yNmA=
github.com/alecthomas/chroma/v2 v2.21.0 h1:YVW9qQAFnQm2OFPPFQg6G/TpMxKSsUr/KUPDi/BEqtY=
github.com/alecthomas/chroma/v2 v2.21.0/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o=
github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
github.com/alecthomas/repr v0.5.1 h1:E3G4t2QbHTSNpPKBgMTln5KLkZHLOcU7r37J4pXBuIg=
github.com/alecthomas/repr v0.5.1/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b h1:uUXgbcPDK3KpW29o4iy7GtuappbWT0l5NaMo9H9pJDw=
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
github.com/aws/aws-sdk-go-v2 v1.38.0 h1:UCRQ5mlqcFk9HJDIqENSLR3wiG1VTWlyUfLDEvY7RxU=
github.com/aws/aws-sdk-go-v2 v1.38.0/go.mod h1:9Q0OoGQoboYIAJyslFyF1f5K1Ryddop8gqMhWx/n4Wg=
github.com/aws/aws-sdk-go-v2/config v1.31.0 h1:9yH0xiY5fUnVNLRWO0AtayqwU1ndriZdN78LlhruJR4=
github.com/aws/aws-sdk-go-v2/config v1.31.0/go.mod h1:VeV3K72nXnhbe4EuxxhzsDc/ByrCSlZwUnWH52Nde/I=
github.com/aws/aws-sdk-go-v2/credentials v1.18.4 h1:IPd0Algf1b+Qy9BcDp0sCUcIWdCQPSzDoMK3a8pcbUM=
github.com/aws/aws-sdk-go-v2/credentials v1.18.4/go.mod h1:nwg78FjH2qvsRM1EVZlX9WuGUJOL5od+0qvm0adEzHk=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.3 h1:GicIdnekoJsjq9wqnvyi2elW6CGMSYKhdozE7/Svh78=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.3/go.mod h1:R7BIi6WNC5mc1kfRM7XM/VHC3uRWkjc396sfabq4iOo=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.3 h1:o9RnO+YZ4X+kt5Z7Nvcishlz0nksIt2PIzDglLMP0vA=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.3/go.mod h1:+6aLJzOG1fvMOyzIySYjOFjcguGvVRL68R+uoRencN4=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.3 h1:joyyUFhiTQQmVK6ImzNU9TQSNRNeD9kOklqTzyk5v6s=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.3/go.mod h1:+vNIyZQP3b3B1tSLI0lxvrU9cfM7gpdRXMFfm67ZcPc=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.0 h1:6+lZi2JeGKtCraAj1rpoZfKqnQ9SptseRZioejfUOLM=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.0/go.mod h1:eb3gfbVIxIoGgJsi9pGne19dhCBpK6opTYpQqAmdy44=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.3 h1:ieRzyHXypu5ByllM7Sp4hC5f/1Fy5wqxqY0yB85hC7s=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.3/go.mod h1:O5ROz8jHiOAKAwx179v+7sHMhfobFVi6nZt8DEyiYoM=
github.com/aws/aws-sdk-go-v2/service/kms v1.44.0 h1:Z95XCqqSnwXr0AY7PgsiOUBhUG2GoDM5getw6RfD1Lg=
github.com/aws/aws-sdk-go-v2/service/kms v1.44.0/go.mod h1:DqcSngL7jJeU1fOzh5Ll5rSvX/MlMV6OZlE4mVdFAQc=
github.com/aws/aws-sdk-go-v2/service/sso v1.28.0 h1:Mc/MKBf2m4VynyJkABoVEN+QzkfLqGj0aiJuEe7cMeM=
github.com/aws/aws-sdk-go-v2/service/sso v1.28.0/go.mod h1:iS5OmxEcN4QIPXARGhavH7S8kETNL11kym6jhoS7IUQ=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.33.0 h1:6csaS/aJmqZQbKhi1EyEMM7yBW653Wy/B9hnBofW+sw=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.33.0/go.mod h1:59qHWaY5B+Rs7HGTuVGaC32m0rdpQ68N8QCN3khYiqs=
github.com/aws/aws-sdk-go-v2/service/sts v1.37.0 h1:MG9VFW43M4A8BYeAfaJJZWrroinxeTi2r3+SnmLQfSA=
github.com/aws/aws-sdk-go-v2/service/sts v1.37.0/go.mod h1:JdeBDPgpJfuS6rU/hNglmOigKhyEZtBmbraLE4GK1J8=
github.com/aws/smithy-go v1.22.5 h1:P9ATCXPMb2mPjYBgueqJNCA5S9UfktsW0tTxi+a7eqw=
github.com/aws/smithy-go v1.22.5/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/aws/aws-sdk-go-v2 v1.40.0 h1:/WMUA0kjhZExjOQN2z3oLALDREea1A7TobfuiBrKlwc=
github.com/aws/aws-sdk-go-v2 v1.40.0/go.mod h1:c9pm7VwuW0UPxAEYGyTmyurVcNrbF6Rt/wixFqDhcjE=
github.com/aws/aws-sdk-go-v2/config v1.32.1 h1:iODUDLgk3q8/flEC7ymhmxjfoAnBDwEEYEVyKZ9mzjU=
github.com/aws/aws-sdk-go-v2/config v1.32.1/go.mod h1:xoAgo17AGrPpJBSLg81W+ikM0cpOZG8ad04T2r+d5P0=
github.com/aws/aws-sdk-go-v2/credentials v1.19.1 h1:JeW+EwmtTE0yXFK8SmklrFh/cGTTXsQJumgMZNlbxfM=
github.com/aws/aws-sdk-go-v2/credentials v1.19.1/go.mod h1:BOoXiStwTF+fT2XufhO0Efssbi1CNIO/ZXpZu87N0pw=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.14 h1:WZVR5DbDgxzA0BJeudId89Kmgy6DIU4ORpxwsVHz0qA=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.14/go.mod h1:Dadl9QO0kHgbrH1GRqGiZdYtW5w+IXXaBNCHTIaheM4=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14 h1:PZHqQACxYb8mYgms4RZbhZG0a7dPW06xOjmaH0EJC/I=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14/go.mod h1:VymhrMJUWs69D8u0/lZ7jSB6WgaG/NqHi3gX0aYf6U0=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14 h1:bOS19y6zlJwagBfHxs0ESzr1XCOU2KXJCWcq3E2vfjY=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14/go.mod h1:1ipeGBMAxZ0xcTm6y6paC2C/J6f6OO7LBODV9afuAyM=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 h1:x2Ibm/Af8Fi+BH+Hsn9TXGdT+hKbDd5XOTZxTMxDk7o=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3/go.mod h1:IW1jwyrQgMdhisceG8fQLmQIydcT/jWY21rFhzgaKwo=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.14 h1:FIouAnCE46kyYqyhs0XEBDFFSREtdnr8HQuLPQPLCrY=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.14/go.mod h1:UTwDc5COa5+guonQU8qBikJo1ZJ4ln2r1MkF7Dqag1E=
github.com/aws/aws-sdk-go-v2/service/kms v1.48.0 h1:pQgVxqqNOacqb19+xaoih/wNLil4d8tgi+FxtBi/qQY=
github.com/aws/aws-sdk-go-v2/service/kms v1.48.0/go.mod h1:VJcNH6BLr+3VJwinRKdotLOMglHO8mIKlD3ea5c7hbw=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.1 h1:BDgIUYGEo5TkayOWv/oBLPphWwNm/A91AebUjAu5L5g=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.1/go.mod h1:iS6EPmNeqCsGo+xQmXv0jIMjyYtQfnwg36zl2FwEouk=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.4 h1:U//SlnkE1wOQiIImxzdY5PXat4Wq+8rlfVEw4Y7J8as=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.4/go.mod h1:av+ArJpoYf3pgyrj6tcehSFW+y9/QvAY8kMooR9bZCw=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.9 h1:LU8S9W/mPDAU9q0FjCLi0TrCheLMGwzbRpvUMwYspcA=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.9/go.mod h1:/j67Z5XBVDx8nZVp9EuFM9/BS5dvBznbqILGuu73hug=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.1 h1:GdGmKtG+/Krag7VfyOXV17xjTCz0i9NT+JnqLTOI5nA=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.1/go.mod h1:6TxbXoDSgBQ225Qd8Q+MbxUxUh6TtNKwbRt/EPS9xso=
github.com/aws/smithy-go v1.23.2 h1:Crv0eatJUQhaManss33hS5r40CG3ZFH+21XSkqMrIUM=
github.com/aws/smithy-go v1.23.2/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA=
github.com/bits-and-blooms/bitset v1.12.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/bits-and-blooms/bitset v1.24.0 h1:H4x4TuulnokZKvHLfzVRTHJfFfnHEeSYJizujEZvmAM=
github.com/bits-and-blooms/bitset v1.24.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
github.com/bits-and-blooms/bitset v1.24.4 h1:95H15Og1clikBrKr/DuzMXkQzECs1M6hhoGXLwLQOZE=
github.com/bits-and-blooms/bitset v1.24.4/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/caddyserver/caddy/v2 v2.10.2 h1:g/gTYjGMD0dec+UgMw8SnfmJ3I9+M2TdvoRL/Ovu6U8=
github.com/caddyserver/caddy/v2 v2.10.2/go.mod h1:TXLQHx+ev4HDpkO6PnVVHUbL6OXt6Dfe7VcIBdQnPL0=
github.com/caddyserver/certmagic v0.24.0 h1:EfXTWpxHAUKgDfOj6MHImJN8Jm4AMFfMT6ITuKhrDF0=
github.com/caddyserver/certmagic v0.24.0/go.mod h1:xPT7dC1DuHHnS2yuEQCEyks+b89sUkMENh8dJF+InLE=
github.com/caddyserver/certmagic v0.25.0 h1:VMleO/XA48gEWes5l+Fh6tRWo9bHkhwAEhx63i+F5ic=
github.com/caddyserver/certmagic v0.25.0/go.mod h1:m9yB7Mud24OQbPHOiipAoyKPn9pKHhpSJxXR1jydBxA=
github.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+YTAyA=
github.com/caddyserver/zerossl v0.1.3/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4=
github.com/ccoveille/go-safecast v1.6.1 h1:Nb9WMDR8PqhnKCVs2sCB+OqhohwO5qaXtCviZkIff5Q=
github.com/ccoveille/go-safecast v1.6.1/go.mod h1:QqwNjxQ7DAqY0C721OIO9InMk9zCwcsO7tnRuHytad8=
github.com/ccoveille/go-safecast/v2 v2.0.0 h1:+5eyITXAUj3wMjad6cRVJKGnC7vDS55zk0INzJagub0=
github.com/ccoveille/go-safecast/v2 v2.0.0/go.mod h1:JIYA4CAR33blIDuE6fSwCp2sz1oOBahXnvmdBhOAABs=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
@@ -129,15 +111,13 @@ github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObk
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
github.com/coreos/go-oidc/v3 v3.15.0 h1:R6Oz8Z4bqWR7VFQ+sPSvZPQv4x8M+sJkDO5ojgwlyAg=
github.com/coreos/go-oidc/v3 v3.15.0/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU=
github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc=
github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
@@ -161,16 +141,16 @@ github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55k
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dolthub/maphash v0.1.0 h1:bsQ7JsF4FkkWyrP3oCnFJgrCUAFbFf3kOl4L/QxPDyQ=
github.com/dolthub/maphash v0.1.0/go.mod h1:gkg4Ch4CdCDu5h6PMriVLawB7koZ+5ijb9puGMV50a4=
github.com/dunglas/caddy-cbrotli v1.0.1 h1:mkg7EB1GmoyfBt3kY3mq4o/0bfnBeq7ZLQjmVmdBE3Y=
github.com/dunglas/caddy-cbrotli v1.0.1/go.mod h1:uXABy3tjy1FABF+3JWKVh1ajFvIO/kfpwHaeZGSBaAY=
github.com/dunglas/httpsfv v1.1.0 h1:Jw76nAyKWKZKFrpMMcL76y35tOpYHqQPzHQiwDvpe54=
github.com/dunglas/httpsfv v1.1.0/go.mod h1:zID2mqw9mFsnt7YC3vYQ9/cjq30q41W+1AnDwH8TiMg=
github.com/dunglas/mercure v0.20.2 h1:zQRJ7QgjtQafOc9q3XtYVDgsfbUokvHeBhiFBpNTiIs=
github.com/dunglas/mercure v0.20.2/go.mod h1:sJtQEtezUTKlbDCK1Sqgt9sUrcwBNZJxVc6oHkqvt3w=
github.com/dunglas/mercure/caddy v0.20.2 h1:CGpcT+arSzxr39s2qRvPpS2/yjZV26cElgee+9Au7lg=
github.com/dunglas/mercure/caddy v0.20.2/go.mod h1:96MOkU7u42rFRIQSpzQibC2LT1vwo/AgLA5jivuw7aE=
github.com/dunglas/mercure v0.21.4 h1:mXPXHfB+4cYfFFCRRDY198mfef5+MQcdCpUnAHBUW2M=
github.com/dunglas/mercure v0.21.4/go.mod h1:l/dglCjp/OQx8/quRyceRPx2hqZQ3CNviwoLMRQiJ/k=
github.com/dunglas/mercure/caddy v0.21.4 h1:7o+6rDqfwj1EOmXOgfBFsayvJvOUP37xujQHaxuX4ps=
github.com/dunglas/mercure/caddy v0.21.4/go.mod h1:EM2s+OVGExbSXObUdAPDwPsbQw4t/icLtQv9CFylDvY=
github.com/dunglas/skipfilter v1.0.0 h1:JG9SgGg4n6BlFwuTYzb9RIqjH7PfwszvWehanrYWPF4=
github.com/dunglas/skipfilter v1.0.0/go.mod h1:ryhr8j7CAHSjzeN7wI6YEuwoArQ3OQmRqWWVCEAfb9w=
github.com/dunglas/vulcain v1.2.1 h1:pkPwvIfoa/xmWSVUyhntbIKT+XO2VFMyhLKv1gA61O8=
github.com/dunglas/vulcain v1.2.1/go.mod h1:Qv1hxHP8rMDp6ZrpQQPSOh1OibuAYScNNCL/46sCRXU=
github.com/dunglas/vulcain/caddy v1.2.1 h1:Bh5awZ9WoNqsBv7OZfs1SktJqRgJaF5avI5oDmxs6lI=
@@ -178,11 +158,10 @@ github.com/dunglas/vulcain/caddy v1.2.1/go.mod h1:8QrmLTfURmW2VgjTR6Gb9a53FrZjsp
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/e-dant/watcher/watcher-go v0.0.0-20251208164151-f88ec3b7e146 h1:h3vVM6X45PK0mAk8NqiYNQGXTyhvXy1HQ5GhuQN4eeA=
github.com/e-dant/watcher/watcher-go v0.0.0-20251208164151-f88ec3b7e146/go.mod h1:sVUOkwtftoj71nnJRG2S0oWNfXFdKpz/M9vK0z06nmM=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk=
github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
@@ -190,28 +169,25 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/gammazero/deque v1.1.0 h1:OyiyReBbnEG2PP0Bnv1AASLIYvyKqIFN5xfl1t8oGLo=
github.com/gammazero/deque v1.1.0/go.mod h1:JVrR+Bj1NMQbPnYclvDlvSX0nVGReLrQZ0aUMuWLctg=
github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ=
github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY=
github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
github.com/go-jose/go-jose/v4 v4.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI=
github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo=
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-openapi/jsonpointer v0.21.2 h1:AqQaNADVwq/VnkCmQg6ogE+M3FOsKTytwges0JdwVuA=
github.com/go-openapi/jsonpointer v0.21.2/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk=
github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4=
github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80=
github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI=
github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag=
github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls=
github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
@@ -220,16 +196,10 @@ github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9L
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gofrs/uuid/v5 v5.3.2 h1:2jfO8j3XgSwlz/wHqemAEugfnTlikAYHhnqQ8Xh4fE0=
github.com/gofrs/uuid/v5 v5.3.2/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gofrs/uuid/v5 v5.4.0 h1:EfbpCTjqMuGyq5ZJwxqzn3Cbr2d0rUZU7v5ycAk/e/0=
github.com/gofrs/uuid/v5 v5.4.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
@@ -238,7 +208,6 @@ github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/brotli/go/cbrotli v1.1.0 h1:YwHD/rwSgUSL4b2S3ZM2jnNymm+tmwKQqjUIC63nmHU=
github.com/google/brotli/go/cbrotli v1.1.0/go.mod h1:nOPhAkwVliJdNTkj3gXpljmWhjc4wCaVqbMJcPKWP4s=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/cel-go v0.26.1 h1:iPbVVEdkhTX++hpe3lzSk7D3G3QSYqLGoHOcEio+UXQ=
@@ -246,43 +215,30 @@ github.com/google/cel-go v0.26.1/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PU
github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg=
github.com/google/certificate-transparency-go v1.3.2 h1:9ahSNZF2o7SYMaKaXhAumVEzXB2QaayzII9C8rv7v+A=
github.com/google/certificate-transparency-go v1.3.2/go.mod h1:H5FpMUaGa5Ab2+KCYsxg6sELw3Flkl7pGZzWdBoYLXs=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/go-tpm v0.9.5 h1:ocUmnDebX54dnW+MQWGQRbdaAcJELsa6PqZhJ48KwVU=
github.com/google/go-tpm v0.9.5/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
github.com/google/go-tpm-tools v0.4.5 h1:3fhthtyMDbIZFR5/0y1hvUoZ1Kf4i1eZ7C73R4Pvd+k=
github.com/google/go-tpm-tools v0.4.5/go.mod h1:ktjTNq8yZFD6TzdBFefUfen96rF3NpYwpSb2d8bc+Y8=
github.com/google/go-tpm v0.9.7 h1:u89J4tUUeDTlH8xxC3CTW7OHZjbjKoHdQ9W7gCUhtxA=
github.com/google/go-tpm v0.9.7/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
github.com/google/go-tpm-tools v0.4.7 h1:J3ycC8umYxM9A4eF73EofRZu4BxY0jjQnUnkhIBbvws=
github.com/google/go-tpm-tools v0.4.7/go.mod h1:gSyXTZHe3fgbzb6WEGd90QucmsnT1SRdlye82gH8QjQ=
github.com/google/go-tspi v0.3.0 h1:ADtq8RKfP+jrTyIWIZDIYcKOMecRqNJFOew2IT0Inus=
github.com/google/go-tspi v0.3.0/go.mod h1:xfMGI3G0PhxCdNVcYr1C4C+EizojDg/TXuX5by8CiHI=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4=
github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg=
github.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ=
github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo=
github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=
github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs=
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
@@ -295,22 +251,16 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs=
github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk=
github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/kevburnsjr/skipfilter v0.0.1 h1:EWl1lWUJfIehrKYIEkps0Cl67lCfS2pUM9iZFNajp7g=
github.com/kevburnsjr/skipfilter v0.0.1/go.mod h1:jfaRyFOYVUtIa6IIC+0mB1qiZqhHw+DKvFowCBuijSk=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
@@ -318,35 +268,30 @@ github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/libdns/libdns v1.1.0 h1:9ze/tWvt7Df6sbhOJRB8jT33GHEHpEQXdtkE3hPthbU=
github.com/libdns/libdns v1.1.0/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
github.com/libdns/libdns v1.1.1 h1:wPrHrXILoSHKWJKGd0EiAVmiJbFShguILTg9leS/P/U=
github.com/libdns/libdns v1.1.1/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8=
github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/maypok86/otter v1.2.4 h1:HhW1Pq6VdJkmWwcZZq19BlEQkHtI8xgsQzBVXJU0nfc=
github.com/maypok86/otter v1.2.4/go.mod h1:mKLfoI7v1HOmQMwFgX4QkRk23mX6ge3RDvjdHOWG4R4=
github.com/maypok86/otter/v2 v2.2.1 h1:hnGssisMFkdisYcvQ8L019zpYQcdtPse+g0ps2i7cfI=
github.com/maypok86/otter/v2 v2.2.1/go.mod h1:1NKY9bY+kB5jwCXBJfE59u+zAwOt6C7ni1FTlFFMqVs=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mholt/acmez/v3 v3.1.2 h1:auob8J/0FhmdClQicvJvuDavgd5ezwLBfKuYmynhYzc=
github.com/mholt/acmez/v3 v3.1.2/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ=
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA=
github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
github.com/mholt/acmez/v3 v3.1.4 h1:DyzZe/RnAzT3rpZj/2Ii5xZpiEvvYk3cQEN/RmqxwFQ=
github.com/mholt/acmez/v3 v3.1.4/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ=
github.com/miekg/dns v1.1.69 h1:Kb7Y/1Jo+SG+a2GtfoFUfDkG//csdRPwRLkCsxDG9Sc=
github.com/miekg/dns v1.1.69/go.mod h1:7OyjD9nEba5OkqQ/hB4fy3PIoxafSZJtducccIelz3g=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
@@ -355,21 +300,16 @@ github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM=
github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY=
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw=
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c=
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o=
github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0=
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
@@ -389,75 +329,49 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc=
github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc=
github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI=
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.57.1 h1:25KAAR9QR8KZrCZRThWMKVAwGoiHIrNbT72ULHTuI10=
github.com/quic-go/quic-go v0.57.1/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagikazarmark/locafero v0.10.0 h1:FM8Cv6j2KqIhM2ZK7HZjm4mpj9NBktLgowT1aN9q5Cc=
github.com/sagikazarmark/locafero v0.10.0/go.mod h1:Ieo3EUsjifvQu4NZwV5sPd4dwvu0OCgEQV7vjc9yDjw=
github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4=
github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=
github.com/schollz/jsonstore v1.1.0 h1:WZBDjgezFS34CHI+myb4s8GGpir3UMpy7vWoCeO0n6E=
github.com/schollz/jsonstore v1.1.0/go.mod h1:15c6+9guw8vDRyozGjN3FoILt0wpruJk9Pi66vjaZfg=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY=
github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM=
github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0=
github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw=
github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI=
github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU=
github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag=
github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg=
github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw=
github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y=
github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q=
github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ=
github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I=
github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0=
github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ=
github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk=
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4=
github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/slackhq/nebula v1.9.6 h1:Fl0LE2dHDeVEK3R+un59Z3V4ZzbZ6q2e/zF4ClaD5yo=
github.com/slackhq/nebula v1.9.6/go.mod h1:1+4q4wd3dDAjO8rKCttSb9JIVbklQhuJiBp5I0lbIsQ=
github.com/slackhq/nebula v1.9.7 h1:v5u46efIyYHGdfjFnozQbRRhMdaB9Ma1SSTcUcE2lfE=
github.com/slackhq/nebula v1.9.7/go.mod h1:1+4q4wd3dDAjO8rKCttSb9JIVbklQhuJiBp5I0lbIsQ=
github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 h1:unQFBIznI+VYD1/1fApl1A+9VcBk+9dcqGfnePY87LY=
github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262/go.mod h1:MyOHs9Po2fbM1LHej6sBUT8ozbxmMOFG+E+rx/GSGuc=
github.com/smallstep/certificates v0.28.4 h1:JTU6/A5Xes6m+OsR6fw1RACSA362vJc9SOFVG7poBEw=
github.com/smallstep/certificates v0.28.4/go.mod h1:LUqo+7mKZE7FZldlTb0zhU4A0bq4G4+akieFMcTaWvA=
github.com/smallstep/cli-utils v0.12.1 h1:D9QvfbFqiKq3snGZ2xDcXEFrdFJ1mQfPHZMq/leerpE=
github.com/smallstep/cli-utils v0.12.1/go.mod h1:skV2Neg8qjiKPu2fphM89H9bIxNpKiiRTnX9Q6Lc+20=
github.com/smallstep/certificates v0.29.0 h1:f90szTKYTW62bmCc+qE5doGqIGPVxTQb8Ba37e/K8Zs=
github.com/smallstep/certificates v0.29.0/go.mod h1:27WI0od6gu84mvE4mYQ/QZGyYwHXvhsiSRNC+y3t+mo=
github.com/smallstep/cli-utils v0.12.2 h1:lGzM9PJrH/qawbzMC/s2SvgLdJPKDWKwKzx9doCVO+k=
github.com/smallstep/cli-utils v0.12.2/go.mod h1:uCPqefO29goHLGqFnwk0i8W7XJu18X3WHQFRtOm/00Y=
github.com/smallstep/go-attestation v0.4.4-0.20241119153605-2306d5b464ca h1:VX8L0r8vybH0bPeaIxh4NQzafKQiqvlOn8pmOXbFLO4=
github.com/smallstep/go-attestation v0.4.4-0.20241119153605-2306d5b464ca/go.mod h1:vNAduivU014fubg6ewygkAvQC0IQVXqdc8vaGl/0er4=
github.com/smallstep/linkedca v0.23.0 h1:5W/7EudlK1HcCIdZM68dJlZ7orqCCCyv6bm2l/0JmLU=
github.com/smallstep/linkedca v0.23.0/go.mod h1:7cyRM9soAYySg9ag65QwytcgGOM+4gOlkJ/YA58A9E8=
github.com/smallstep/linkedca v0.25.0 h1:txT9QHGbCsJq0MhAghBq7qhurGY727tQuqUi+n4BVBo=
github.com/smallstep/linkedca v0.25.0/go.mod h1:Q3jVAauFKNlF86W5/RFtgQeyDKz98GL/KN3KG4mJOvc=
github.com/smallstep/nosql v0.7.0 h1:YiWC9ZAHcrLCrayfaF+QJUv16I2bZ7KdLC3RpJcnAnE=
github.com/smallstep/nosql v0.7.0/go.mod h1:H5VnKMCbeq9QA6SRY5iqPylfxLfYcLwvUff3onQ8+HU=
github.com/smallstep/pkcs7 v0.2.1 h1:6Kfzr/QizdIuB6LSv8y1LJdZ3aPSfTNhTLqAx9CTLfA=
@@ -466,29 +380,26 @@ github.com/smallstep/scep v0.0.0-20250318231241-a25cabb69492 h1:k23+s51sgYix4Zgb
github.com/smallstep/scep v0.0.0-20250318231241-a25cabb69492/go.mod h1:QQhwLqCS13nhv8L5ov7NgusowENUtXdEzdytjmJHdZQ=
github.com/smallstep/truststore v0.13.0 h1:90if9htAOblavbMeWlqNLnO9bsjjgVv2hQeQJCi/py4=
github.com/smallstep/truststore v0.13.0/go.mod h1:3tmMp2aLKZ/OA/jnFUB0cYPcho402UG2knuJoPh4j7A=
github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA=
github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cast v1.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE=
github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
github.com/spf13/cobra v1.10.0 h1:a5/WeUlSDCvV5a45ljW2ZFtV0bTDpkfSAj3uqB6Sc+0=
github.com/spf13/cobra v1.10.0/go.mod h1:9dhySC7dnTtEiqzmqfkLj47BslqLCUPMXjG2lj/NgoE=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.8 h1:/v546uKZ4gFGHpyXvV6CNKDeJBu4l5PRvxwQvdWrc0I=
github.com/spf13/pflag v1.0.8/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
github.com/stoewer/go-strcase v1.3.1 h1:iS0MdW+kVTxgMoE1LAZyMiYJFKlOzLooE4MxjirtkAs=
github.com/stoewer/go-strcase v1.3.1/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@@ -508,14 +419,16 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53 h1:uxMgm0C+EjytfAqyfBG55ZONKQ7mvd7x4YYCWsf8QHQ=
github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53/go.mod h1:kNGUQ3VESx3VZwRwA9MSCUegIl6+saPL8Noq82ozCaU=
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 h1:Gzfnfk2TWrk8Jj4P4c1a3CtQyMaTVCznlkLZI++hok4=
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55/go.mod h1:4k4QO+dQ3R5FofL+SanAUZe+/QfeK0+OIuwDIRu2vSg=
github.com/tailscale/tscert v0.0.0-20251216020129-aea342f6d747 h1:RnBbFMmodYzhC6adOjTbtUQXyzV8dcvKYbolzs6Qch0=
github.com/tailscale/tscert v0.0.0-20251216020129-aea342f6d747/go.mod h1:ejPAJui3kVK4u5TgMtqtXlWf5HnKh9fLy5kvpaeuas0=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM=
github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
@@ -528,8 +441,6 @@ github.com/unrolled/secure v1.17.0 h1:Io7ifFgo99Bnh0J7+Q+qcMzWM6kaDPCA5FroFZEdbW
github.com/unrolled/secure v1.17.0/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40=
github.com/urfave/cli v1.22.17 h1:SYzXoiPfQjHBbkYxbew5prZHS1TOLT3ierW8SYLqtVQ=
github.com/urfave/cli v1.22.17/go.mod h1:b0ht0aqgH/6pBYzzxURyrM4xXNgsoT/n2ZzwQiEhNVo=
github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU=
github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM=
github.com/woodsbury/decimal128 v1.4.0 h1:xJATj7lLu4f2oObouMt2tgGiElE5gO6mSWUjQsBgUlc=
github.com/woodsbury/decimal128 v1.4.0/go.mod h1:BP46FUrVjVhdTbKT+XuQh2xfQaGki9LMIRJSFuh6THU=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
@@ -551,41 +462,40 @@ github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 h1:Hf9xI/XLML9ElpiHVDNwvqI0hIFlzV8dgIr35kV1kRU=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0/go.mod h1:NfchwuyNoMcZ5MLHwPrODwUF1HWCXWrL31s8gSAdIKY=
go.opentelemetry.io/contrib/propagators/autoprop v0.62.0 h1:1+EHlhAe/tukctfePZRrDruB9vn7MdwyC+rf36nUSPM=
go.opentelemetry.io/contrib/propagators/autoprop v0.62.0/go.mod h1:skzESZBY3IYcqJgImc+fwXQWflvVe+jZxoA/uw60NaI=
go.opentelemetry.io/contrib/propagators/aws v1.37.0 h1:cp8AFiM/qjBm10C/ATIRnEDXpD5MBknrA0ANw4T2/ss=
go.opentelemetry.io/contrib/propagators/aws v1.37.0/go.mod h1:Cy8Hk2E2iSGEbsLnPUdeigrexaAOAGIAmBFK919EQs0=
go.opentelemetry.io/contrib/propagators/b3 v1.37.0 h1:0aGKdIuVhy5l4GClAjl72ntkZJhijf2wg1S7b5oLoYA=
go.opentelemetry.io/contrib/propagators/b3 v1.37.0/go.mod h1:nhyrxEJEOQdwR15zXrCKI6+cJK60PXAkJ/jRyfhr2mg=
go.opentelemetry.io/contrib/propagators/jaeger v1.37.0 h1:pW+qDVo0jB0rLsNeaP85xLuz20cvsECUcN7TE+D8YTM=
go.opentelemetry.io/contrib/propagators/jaeger v1.37.0/go.mod h1:x7bd+t034hxLTve1hF9Yn9qQJlO/pP8H5pWIt7+gsFM=
go.opentelemetry.io/contrib/propagators/ot v1.37.0 h1:tVjnBF6EiTDMXoq2Xuc2vK0I7MTbEs05II/0j9mMK+E=
go.opentelemetry.io/contrib/propagators/ot v1.37.0/go.mod h1:MQjyNXtxAC8PGN9gzPtO4GY5zuP+RI3XX53uWbCTvEQ=
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 h1:EtFWSnwW9hGObjkIdmlnWSydO+Qs8OwzfzXLUPg4xOc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0/go.mod h1:QjUEoiGCPkvFZ/MjK6ZZfNOS6mfVEVKYE99dFhuN2LI=
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4=
go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=
go.step.sm/crypto v0.70.0 h1:Q9Ft7N637mucyZcHZd1+0VVQJVwDCKqcb9CYcYi7cds=
go.step.sm/crypto v0.70.0/go.mod h1:pzfUhS5/ue7ev64PLlEgXvhx1opwbhFCjkvlhsxVds0=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ=
go.opentelemetry.io/contrib/propagators/autoprop v0.64.0 h1:VVrb1ErDD0Tlh/0K0rUqjky1e8AekjspTFN9sU2ekaA=
go.opentelemetry.io/contrib/propagators/autoprop v0.64.0/go.mod h1:QCsOQk+9Ep8Mkp4/aPtSzUT0dc8SaPYzBAE6o1jYuSE=
go.opentelemetry.io/contrib/propagators/aws v1.39.0 h1:IvNR8pAVGpkK1CHMjU/YE6B6TlnAPGFvogkMWRWU6wo=
go.opentelemetry.io/contrib/propagators/aws v1.39.0/go.mod h1:TUsFCERuGM4IGhJG9w+9l0nzmHUKHuaDYYNF6mtNgjY=
go.opentelemetry.io/contrib/propagators/b3 v1.39.0 h1:PI7pt9pkSnimWcp5sQhUA9OzLbc3Ba4sL+VEUTNsxrk=
go.opentelemetry.io/contrib/propagators/b3 v1.39.0/go.mod h1:5gV/EzPnfYIwjzj+6y8tbGW2PKWhcsz5e/7twptRVQY=
go.opentelemetry.io/contrib/propagators/jaeger v1.39.0 h1:Gz3yKzfMSEFzF0Vy5eIpu9ndpo4DhXMCxsLMF0OOApo=
go.opentelemetry.io/contrib/propagators/jaeger v1.39.0/go.mod h1:2D/cxxCqTlrday0rZrPujjg5aoAdqk1NaNyoXn8FJn8=
go.opentelemetry.io/contrib/propagators/ot v1.39.0 h1:vKTve1W/WKPVp1fzJamhCDDECt+5upJJ65bPyWoddGg=
go.opentelemetry.io/contrib/propagators/ot v1.39.0/go.mod h1:FH5VB2N19duNzh1Q8ks6CsZFyu3LFhNLiA9lPxyEkvU=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 h1:in9O8ESIOlwJAEGTkkf34DesGRAc/Pn8qJ7k3r/42LM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0/go.mod h1:Rp0EXBm5tfnv0WL+ARyO/PHBEaEAT8UUHQ6AGJcSq6c=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
go.step.sm/crypto v0.75.0 h1:UAHYD6q6ggYyzLlIKHv1MCUVjZIesXRZpGTlRC/HSHw=
go.step.sm/crypto v0.75.0/go.mod h1:wwQ57+ajmDype9mrI/2hRyrvJd7yja5xVgWYqpUN3PE=
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
@@ -594,46 +504,34 @@ go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U=
go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ=
go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE=
golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw=
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
golang.org/x/crypto/x509roots/fallback v0.0.0-20250826074233-8f580defa01d h1:earVicuMOnaYH+t9T1PeRhC0LHHPGXXTDD7NEEP/Pb4=
golang.org/x/crypto/x509roots/fallback v0.0.0-20250826074233-8f580defa01d/go.mod h1:MEIPiCnxvQEjA4astfaKItNwEVZA5Ki+3+nyGbJ5N18=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b h1:DXr+pvt3nC887026GRP39Ej11UATqWDmWuS99x26cD0=
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/crypto/x509roots/fallback v0.0.0-20251210140736-7dacc380ba00 h1:qObov2/X4yIpr98j5t6samg3mMF12Rl4taUJd1rWj+c=
golang.org/x/crypto/x509roots/fallback v0.0.0-20251210140736-7dacc380ba00/go.mod h1:MEIPiCnxvQEjA4astfaKItNwEVZA5Ki+3+nyGbJ5N18=
golang.org/x/exp v0.0.0-20251209150349-8475f28825e9 h1:MDfG8Cvcqlt9XXrmEiD4epKn7VJHZO84hejP9Jmp0MM=
golang.org/x/exp v0.0.0-20251209150349-8475f28825e9/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
@@ -642,19 +540,10 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -662,17 +551,12 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -686,8 +570,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -697,10 +581,9 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
@@ -709,76 +592,46 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y=
google.golang.org/api v0.248.0 h1:hUotakSkcwGdYUqzCRc5yGYsg4wXxpkKlW5ryVqvC1Y=
google.golang.org/api v0.248.0/go.mod h1:yAFUAF56Li7IuIQbTFoLwXTCI6XCFKueOlS7S9e4F9k=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg=
google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/api v0.257.0 h1:8Y0lzvHlZps53PEaw+G29SsQIkuKrumGWs9puiexNAA=
google.golang.org/api v0.257.0/go.mod h1:4eJrr+vbVaZSqs7vovFd1Jb/A6ml6iw2e6FBYf3GAO4=
google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4=
google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s=
google.golang.org/genproto/googleapis/api v0.0.0-20250826171959-ef028d996bc1 h1:APHvLLYBhtZvsbnpkfknDZ7NyH4z5+ub/I0u8L3Oz6g=
google.golang.org/genproto/googleapis/api v0.0.0-20250826171959-ef028d996bc1/go.mod h1:xUjFWUnWDpZ/C0Gu0qloASKFb6f8/QXiiXhSPFsD668=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1 h1:pmJpJEvT846VzausCQ5d7KreSROcDqmO388w5YbnltA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1/go.mod h1:GmFNa4BdJZ2a8G+wCe9Bg3wwThLrJun751XstdJt5Og=
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4=
google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 h1:F29+wU6Ee6qgu9TddPgooOdaqsxTMunOoj8KA5yuS5A=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1/go.mod h1:5KF+wpkbTSbGcR9zteSqZV6fqFOWBl4Yde8En8MryZA=
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 h1:7LRqPCEdE4TP4/9psdaB7F2nhZFfBiGJomA5sojLWdU=
google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 h1:2I6GHUeJ/4shcDpoUlLs/2WPnhg7yJwvXtqcMJt9liA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.6.0 h1:6Al3kEFFP9VJhRz3DID6quisgPnTeZVr4lep9kkxdPA=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.6.0/go.mod h1:QLvsjh0OIR0TYBeiu2bkWGTJBUNQ64st52iWj/yA93I=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
howett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM=
howett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck=
sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=

20
caddy/hotreload-skip.go Normal file
View File

@@ -0,0 +1,20 @@
//go:build nowatcher || nomercure
package caddy
import (
"errors"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
)
type hotReloadContext struct {
}
func (_ *FrankenPHPModule) configureHotReload(_ *FrankenPHPApp) error {
return nil
}
func (_ *FrankenPHPModule) unmarshalHotReload(d *caddyfile.Dispenser) error {
return errors.New("hot reload support disabled")
}

104
caddy/hotreload.go Normal file
View File

@@ -0,0 +1,104 @@
//go:build !nowatcher && !nomercure
package caddy
import (
"bytes"
"encoding/gob"
"errors"
"fmt"
"hash/fnv"
"net/url"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/dunglas/frankenphp"
)
const defaultHotReloadPattern = "./**/*.{css,env,gif,htm,html,jpg,jpeg,js,mjs,php,png,svg,twig,webp,xml,yaml,yml}"
type hotReloadContext struct {
// HotReload specifies files to watch for file changes to trigger hot reloads updates. Supports the glob syntax.
HotReload *hotReloadConfig `json:"hot_reload,omitempty"`
}
type hotReloadConfig struct {
Topic string `json:"topic"`
Watch []string `json:"watch"`
}
func (f *FrankenPHPModule) configureHotReload(app *FrankenPHPApp) error {
if f.HotReload == nil {
return nil
}
if f.mercureHub == nil {
return errors.New("unable to enable hot reloading: no Mercure hub configured")
}
if len(f.HotReload.Watch) == 0 {
f.HotReload.Watch = []string{defaultHotReloadPattern}
}
if f.HotReload.Topic == "" {
uid, err := uniqueID(f)
if err != nil {
return err
}
f.HotReload.Topic = "https://frankenphp.dev/hot-reload/" + uid
}
app.opts = append(app.opts, frankenphp.WithHotReload(f.HotReload.Topic, f.mercureHub, f.HotReload.Watch))
f.preparedEnv["FRANKENPHP_HOT_RELOAD\x00"] = "/.well-known/mercure?topic=" + url.QueryEscape(f.HotReload.Topic)
return nil
}
func (f *FrankenPHPModule) unmarshalHotReload(d *caddyfile.Dispenser) error {
f.HotReload = &hotReloadConfig{
Watch: d.RemainingArgs(),
}
for d.NextBlock(1) {
switch v := d.Val(); v {
case "topic":
if !d.NextArg() {
return d.ArgErr()
}
if f.HotReload == nil {
f.HotReload = &hotReloadConfig{}
}
f.HotReload.Topic = d.Val()
case "watch":
patterns := d.RemainingArgs()
if len(patterns) == 0 {
return d.ArgErr()
}
f.HotReload.Watch = append(f.HotReload.Watch, patterns...)
default:
return wrongSubDirectiveError("hot_reload", "topic, watch", v)
}
}
return nil
}
func uniqueID(s any) (string, error) {
var b bytes.Buffer
if err := gob.NewEncoder(&b).Encode(s); err != nil {
return "", fmt.Errorf("unable to generate unique name: %w", err)
}
h := fnv.New64a()
if _, err := h.Write(b.Bytes()); err != nil {
return "", fmt.Errorf("unable to generate unique name: %w", err)
}
return fmt.Sprintf("%016x", h.Sum64()), nil
}

88
caddy/hotreload_test.go Normal file
View File

@@ -0,0 +1,88 @@
//go:build !nowatcher && !nomercure
package caddy_test
import (
"context"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"sync"
"testing"
"github.com/caddyserver/caddy/v2/caddytest"
"github.com/stretchr/testify/require"
)
func TestHotReload(t *testing.T) {
const topic = "https://frankenphp.dev/hot-reload/test"
u := "/.well-known/mercure?topic=" + url.QueryEscape(topic)
tmpDir := t.TempDir()
indexFile := filepath.Join(tmpDir, "index.php")
tester := caddytest.NewTester(t)
tester.InitServer(`
{
debug
skip_install_trust
admin localhost:2999
}
http://localhost:`+testPort+` {
mercure {
transport local
subscriber_jwt TestKey
anonymous
}
php_server {
root `+tmpDir+`
hot_reload {
topic `+topic+`
watch `+tmpDir+`/*.php
}
}
`, "caddyfile")
var connected, received sync.WaitGroup
connected.Add(1)
received.Go(func() {
cx, cancel := context.WithCancel(t.Context())
req, _ := http.NewRequest(http.MethodGet, "http://localhost:"+testPort+u, nil)
req = req.WithContext(cx)
resp := tester.AssertResponseCode(req, http.StatusOK)
connected.Done()
var receivedBody strings.Builder
buf := make([]byte, 1024)
for {
_, err := resp.Body.Read(buf)
require.NoError(t, err)
receivedBody.Write(buf)
if strings.Contains(receivedBody.String(), "index.php") {
cancel()
break
}
}
require.NoError(t, resp.Body.Close())
})
connected.Wait()
require.NoError(t, os.WriteFile(indexFile, []byte("<?=$_SERVER['FRANKENPHP_HOT_RELOAD'];"), 0644))
received.Wait()
tester.AssertGetResponse("http://localhost:"+testPort+"/index.php", http.StatusOK, u)
}

13
caddy/mercure-skip.go Normal file
View File

@@ -0,0 +1,13 @@
//go:build nomercure
package caddy
type mercureContext struct {
}
func (f *FrankenPHPModule) configureHotReload(_ *FrankenPHPApp) error {
return nil
}
func (f *FrankenPHPModule) assignMercureHub(_ caddy.Context) {
}

34
caddy/mercure.go Normal file
View File

@@ -0,0 +1,34 @@
//go:build !nomercure
package caddy
import (
"github.com/caddyserver/caddy/v2"
"github.com/dunglas/frankenphp"
"github.com/dunglas/mercure"
mercureCaddy "github.com/dunglas/mercure/caddy"
)
func init() {
mercureCaddy.AllowNoPublish = true
}
type mercureContext struct {
mercureHub *mercure.Hub
}
func (f *FrankenPHPModule) assignMercureHub(ctx caddy.Context) {
if f.mercureHub = mercureCaddy.FindHub(ctx.Modules()); f.mercureHub == nil {
return
}
opt := frankenphp.WithMercureHub(f.mercureHub)
f.mercureHubRequestOption = &opt
for i, wc := range f.Workers {
wc.mercureHub = f.mercureHub
wc.options = append(wc.options, frankenphp.WithWorkerMercureHub(wc.mercureHub))
f.Workers[i] = wc
}
}

View File

@@ -2,6 +2,7 @@ package caddy
import (
"encoding/json"
"errors"
"fmt"
"log/slog"
"net/http"
@@ -21,6 +22,8 @@ import (
"github.com/dunglas/frankenphp/internal/fastabs"
)
var serverHeader = []string{"FrankenPHP Caddy"}
// FrankenPHPModule represents the "php_server" and "php" directives in the Caddyfile
// they are responsible for forwarding requests to FrankenPHP via "ServeHTTP"
//
@@ -30,6 +33,9 @@ import (
// }
// }
type FrankenPHPModule struct {
mercureContext
hotReloadContext
// Root sets the root folder to the site. Default: `root` directive, or the path of the public directory of the embed app it exists.
Root string `json:"root,omitempty"`
// 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`.
@@ -45,6 +51,7 @@ type FrankenPHPModule struct {
preparedEnv frankenphp.PreparedEnv
preparedEnvNeedsReplacement bool
logger *slog.Logger
mercureHubRequestOption *frankenphp.RequestOption
}
// CaddyModule returns the Caddy module information.
@@ -70,10 +77,12 @@ func (f *FrankenPHPModule) Provision(ctx caddy.Context) error {
return fmt.Errorf(`expected ctx.App("frankenphp") to return *FrankenPHPApp, got nil`)
}
for i, wc := range f.Workers {
f.assignMercureHub(ctx)
loggerOpt := frankenphp.WithRequestLogger(f.logger)
for i, wc := range f.Workers {
// make the file path absolute from the public directory
// this can only be done if the root is definied inside php_server
// this can only be done if the root is defined inside php_server
if !filepath.IsAbs(wc.FileName) && f.Root != "" {
wc.FileName = filepath.Join(f.Root, wc.FileName)
}
@@ -82,6 +91,8 @@ func (f *FrankenPHPModule) Provision(ctx caddy.Context) error {
if f.Env != nil {
wc.inheritEnv(f.Env)
}
wc.requestOptions = append(wc.requestOptions, loggerOpt)
f.Workers[i] = wc
}
@@ -95,14 +106,13 @@ func (f *FrankenPHPModule) Provision(ctx caddy.Context) error {
if frankenphp.EmbeddedAppPath == "" {
f.Root = "{http.vars.root}"
} else {
rrs := false
f.Root = filepath.Join(frankenphp.EmbeddedAppPath, defaultDocumentRoot)
var rrs bool
f.ResolveRootSymlink = &rrs
}
} else {
if frankenphp.EmbeddedAppPath != "" && filepath.IsLocal(f.Root) {
f.Root = filepath.Join(frankenphp.EmbeddedAppPath, f.Root)
}
} else if frankenphp.EmbeddedAppPath != "" && filepath.IsLocal(f.Root) {
f.Root = filepath.Join(frankenphp.EmbeddedAppPath, f.Root)
}
if len(f.SplitPath) == 0 {
@@ -143,6 +153,10 @@ func (f *FrankenPHPModule) Provision(ctx caddy.Context) error {
}
}
if err := f.configureHotReload(fapp); err != nil {
return err
}
return nil
}
@@ -153,11 +167,15 @@ func needReplacement(s string) bool {
// ServeHTTP implements caddyhttp.MiddlewareHandler.
func (f *FrankenPHPModule) ServeHTTP(w http.ResponseWriter, r *http.Request, _ caddyhttp.Handler) error {
origReq := r.Context().Value(caddyhttp.OriginalRequestCtxKey).(http.Request)
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
ctx := r.Context()
origReq := ctx.Value(caddyhttp.OriginalRequestCtxKey).(http.Request)
repl := ctx.Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
var (
documentRootOption frankenphp.RequestOption
documentRoot string
)
var documentRootOption frankenphp.RequestOption
var documentRoot string
if f.resolvedDocumentRoot == "" {
documentRoot = repl.ReplaceKnown(f.Root, "")
if documentRoot == "" && frankenphp.EmbeddedAppPath != "" {
@@ -185,16 +203,39 @@ func (f *FrankenPHPModule) ServeHTTP(w http.ResponseWriter, r *http.Request, _ c
}
}
fr, err := frankenphp.NewRequestWithContext(
r,
documentRootOption,
frankenphp.WithRequestSplitPath(f.SplitPath),
frankenphp.WithRequestPreparedEnv(env),
frankenphp.WithOriginalRequest(&origReq),
frankenphp.WithWorkerName(workerName),
var (
err error
fr *http.Request
)
if err = frankenphp.ServeHTTP(w, fr); err != nil {
if f.mercureHubRequestOption == nil {
fr, err = frankenphp.NewRequestWithContext(
r,
documentRootOption,
frankenphp.WithRequestSplitPath(f.SplitPath),
frankenphp.WithRequestPreparedEnv(env),
frankenphp.WithOriginalRequest(&origReq),
frankenphp.WithWorkerName(workerName),
)
} else {
fr, err = frankenphp.NewRequestWithContext(
r,
documentRootOption,
frankenphp.WithRequestSplitPath(f.SplitPath),
frankenphp.WithRequestPreparedEnv(env),
frankenphp.WithOriginalRequest(&origReq),
frankenphp.WithWorkerName(workerName),
*f.mercureHubRequestOption,
)
}
if err != nil {
return caddyhttp.Error(http.StatusInternalServerError, err)
}
// TODO: set caddyhttp.ServerHeader when https://github.com/caddyserver/caddy/pull/7338 will be released
w.Header()["Server"] = serverHeader
if err = frankenphp.ServeHTTP(w, fr); err != nil && !errors.As(err, &frankenphp.ErrRejected{}) {
return caddyhttp.Error(http.StatusInternalServerError, err)
}
@@ -203,7 +244,6 @@ func (f *FrankenPHPModule) ServeHTTP(w http.ResponseWriter, r *http.Request, _ c
// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
func (f *FrankenPHPModule) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
// First pass: Parse all directives except "worker"
for d.Next() {
for d.NextBlock(0) {
switch d.Val() {
@@ -245,15 +285,20 @@ func (f *FrankenPHPModule) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
f.ResolveRootSymlink = &v
case "worker":
wc, err := parseWorkerConfig(d)
wc, err := unmarshalWorker(d)
if err != nil {
return err
}
f.Workers = append(f.Workers, wc)
case "hot_reload":
if err := f.unmarshalHotReload(d); err != nil {
return err
}
default:
allowedDirectives := "root, split, env, resolve_root_symlink, worker"
return wrongSubDirectiveError("php or php_server", allowedDirectives, d.Val())
return wrongSubDirectiveError("php or php_server", "hot_reload, name, root, split, env, resolve_root_symlink, worker", d.Val())
}
}
}
@@ -262,7 +307,7 @@ func (f *FrankenPHPModule) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
fileNames := make(map[string]struct{}, len(f.Workers))
for _, w := range f.Workers {
if _, ok := fileNames[w.FileName]; ok {
return fmt.Errorf(`workers in a single "php_server" block must not have duplicate filenames: %q`, w.FileName)
return fmt.Errorf(`workers in a single "php" or "php_server" block must not have duplicate filenames: %q`, w.FileName)
}
if len(w.MatchPath) == 0 {
@@ -566,7 +611,7 @@ func parsePhpServer(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error)
// workers can also match a path without being in the public directory
// in this case we need to prepend the worker routes to the existing routes
func prependWorkerRoutes(routes caddyhttp.RouteList, h httpcaddyfile.Helper, f FrankenPHPModule, fsrv caddy.Module, disableFsrv bool) caddyhttp.RouteList {
allWorkerMatches := caddyhttp.MatchPath{}
var allWorkerMatches caddyhttp.MatchPath
for _, w := range f.Workers {
for _, path := range w.MatchPath {
allWorkerMatches = append(allWorkerMatches, path)
@@ -581,7 +626,7 @@ func prependWorkerRoutes(routes caddyhttp.RouteList, h httpcaddyfile.Helper, f F
if !disableFsrv {
routes = append(routes, caddyhttp.Route{
MatcherSetsRaw: []caddy.ModuleMap{
caddy.ModuleMap{
{
"file": h.JSON(fileserver.MatchFile{
TryFiles: []string{"{http.request.uri.path}"},
Root: f.Root,
@@ -602,7 +647,7 @@ func prependWorkerRoutes(routes caddyhttp.RouteList, h httpcaddyfile.Helper, f F
// forward matching routes to the PHP handler
routes = append(routes, caddyhttp.Route{
MatcherSetsRaw: []caddy.ModuleMap{
caddy.ModuleMap{"path": h.JSON(allWorkerMatches)},
{"path": h.JSON(allWorkerMatches)},
},
HandlersRaw: []json.RawMessage{
caddyconfig.JSONModuleObject(f, "handler", "php", nil),
@@ -611,3 +656,10 @@ func prependWorkerRoutes(routes caddyhttp.RouteList, h httpcaddyfile.Helper, f F
return routes
}
// Interface guards
var (
_ caddy.Provisioner = (*FrankenPHPModule)(nil)
_ caddyhttp.MiddlewareHandler = (*FrankenPHPModule)(nil)
_ caddyfile.Unmarshaler = (*FrankenPHPModule)(nil)
)

View File

@@ -1,7 +1,6 @@
package caddy
import (
"errors"
"net/http"
"path/filepath"
"strconv"
@@ -23,12 +22,16 @@ import (
// }
// }
type workerConfig struct {
mercureContext
// Name for the worker. Default: the filename for FrankenPHPApp workers, always prefixed with "m#" for FrankenPHPModule workers.
Name string `json:"name,omitempty"`
// FileName sets the path to the worker script.
FileName string `json:"file_name,omitempty"`
// Num sets the number of workers to start.
Num int `json:"num,omitempty"`
// MaxThreads sets the maximum number of threads for this worker.
MaxThreads int `json:"max_threads,omitempty"`
// Env sets an extra environment variable to the given value. Can be specified more than once for multiple environment variables.
Env map[string]string `json:"env,omitempty"`
// Directories to watch for file changes
@@ -37,9 +40,12 @@ type workerConfig struct {
MatchPath []string `json:"match_path,omitempty"`
// MaxConsecutiveFailures sets the maximum number of consecutive failures before panicking (defaults to 6, set to -1 to never panick)
MaxConsecutiveFailures int `json:"max_consecutive_failures,omitempty"`
options []frankenphp.WorkerOption
requestOptions []frankenphp.RequestOption
}
func parseWorkerConfig(d *caddyfile.Dispenser) (workerConfig, error) {
func unmarshalWorker(d *caddyfile.Dispenser) (workerConfig, error) {
wc := workerConfig{}
if d.NextArg() {
wc.FileName = d.Val()
@@ -59,12 +65,11 @@ func parseWorkerConfig(d *caddyfile.Dispenser) (workerConfig, error) {
}
if d.NextArg() {
return wc, errors.New(`FrankenPHP: too many "worker" arguments: ` + d.Val())
return wc, d.Errf(`FrankenPHP: too many "worker" arguments: %s`, d.Val())
}
for d.NextBlock(1) {
v := d.Val()
switch v {
switch v := d.Val(); v {
case "name":
if !d.NextArg() {
return wc, d.ArgErr()
@@ -82,10 +87,21 @@ func parseWorkerConfig(d *caddyfile.Dispenser) (workerConfig, error) {
v, err := strconv.ParseUint(d.Val(), 10, 32)
if err != nil {
return wc, err
return wc, d.WrapErr(err)
}
wc.Num = int(v)
case "max_threads":
if !d.NextArg() {
return wc, d.ArgErr()
}
v, err := strconv.ParseUint(d.Val(), 10, 32)
if err != nil {
return wc, d.WrapErr(err)
}
wc.MaxThreads = int(v)
case "env":
args := d.RemainingArgs()
if len(args) != 2 {
@@ -96,18 +112,22 @@ func parseWorkerConfig(d *caddyfile.Dispenser) (workerConfig, error) {
}
wc.Env[args[0]] = args[1]
case "watch":
if !d.NextArg() {
patterns := d.RemainingArgs()
if len(patterns) == 0 {
// the default if the watch directory is left empty:
wc.Watch = append(wc.Watch, defaultWatchPattern)
} else {
wc.Watch = append(wc.Watch, d.Val())
wc.Watch = append(wc.Watch, patterns...)
}
case "match":
// provision the path so it's identical to Caddy match rules
// see: https://github.com/caddyserver/caddy/blob/master/modules/caddyhttp/matchers.go
caddyMatchPath := (caddyhttp.MatchPath)(d.RemainingArgs())
caddyMatchPath.Provision(caddy.Context{})
wc.MatchPath = ([]string)(caddyMatchPath)
if err := caddyMatchPath.Provision(caddy.Context{}); err != nil {
return wc, d.WrapErr(err)
}
wc.MatchPath = caddyMatchPath
case "max_consecutive_failures":
if !d.NextArg() {
return wc, d.ArgErr()
@@ -115,21 +135,20 @@ func parseWorkerConfig(d *caddyfile.Dispenser) (workerConfig, error) {
v, err := strconv.Atoi(d.Val())
if err != nil {
return wc, err
return wc, d.WrapErr(err)
}
if v < -1 {
return wc, errors.New("max_consecutive_failures must be >= -1")
return wc, d.Errf("max_consecutive_failures must be >= -1")
}
wc.MaxConsecutiveFailures = int(v)
wc.MaxConsecutiveFailures = v
default:
allowedDirectives := "name, file, num, env, watch, match, max_consecutive_failures"
return wc, wrongSubDirectiveError("worker", allowedDirectives, v)
return wc, wrongSubDirectiveError("worker", "name, file, num, env, watch, match, max_consecutive_failures, max_threads", v)
}
}
if wc.FileName == "" {
return wc, errors.New(`the "file" argument must be specified`)
return wc, d.Err(`the "file" argument must be specified`)
}
if frankenphp.EmbeddedAppPath != "" && filepath.IsLocal(wc.FileName) {
@@ -139,7 +158,7 @@ func parseWorkerConfig(d *caddyfile.Dispenser) (workerConfig, error) {
return wc, nil
}
func (wc workerConfig) inheritEnv(env map[string]string) {
func (wc *workerConfig) inheritEnv(env map[string]string) {
if wc.Env == nil {
wc.Env = make(map[string]string, len(env))
}
@@ -151,7 +170,7 @@ func (wc workerConfig) inheritEnv(env map[string]string) {
}
}
func (wc workerConfig) matchesPath(r *http.Request, documentRoot string) bool {
func (wc *workerConfig) matchesPath(r *http.Request, documentRoot string) bool {
// try to match against a pattern if one is assigned
if len(wc.MatchPath) != 0 {

34
cgi.go
View File

@@ -12,8 +12,10 @@ package frankenphp
// #include "frankenphp.h"
import "C"
import (
"context"
"crypto/tls"
"net"
"net/http"
"path/filepath"
"strings"
"unsafe"
@@ -66,7 +68,7 @@ var knownServerKeys = []string{
//
// TODO: handle this case https://github.com/caddyserver/caddy/issues/3718
// Inspired by https://github.com/caddyserver/caddy/blob/master/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go
func addKnownVariablesToServer(thread *phpThread, fc *frankenPHPContext, trackVarsArray *C.zval) {
func addKnownVariablesToServer(fc *frankenPHPContext, trackVarsArray *C.zval) {
request := fc.request
keys := mainThread.knownServerKeys
// Separate remote IP and port; more lenient than net.SplitHostPort
@@ -82,10 +84,8 @@ func addKnownVariablesToServer(thread *phpThread, fc *frankenPHPContext, trackVa
ip = strings.Replace(ip, "[", "", 1)
ip = strings.Replace(ip, "]", "", 1)
var https string
var sslProtocol string
var sslCipher string
var rs string
var https, sslProtocol, sslCipher, rs string
if request.TLS == nil {
rs = "http"
https = ""
@@ -186,8 +186,8 @@ func packCgiVariable(key *C.zend_string, value string) C.ht_key_value_pair {
return C.ht_key_value_pair{key, toUnsafeChar(value), C.size_t(len(value))}
}
func addHeadersToServer(fc *frankenPHPContext, trackVarsArray *C.zval) {
for field, val := range fc.request.Header {
func addHeadersToServer(ctx context.Context, request *http.Request, trackVarsArray *C.zval) {
for field, val := range request.Header {
if k := mainThread.commonHeaders[field]; k != nil {
v := strings.Join(val, ", ")
C.frankenphp_register_single(k, toUnsafeChar(v), C.size_t(len(v)), trackVarsArray)
@@ -196,7 +196,7 @@ func addHeadersToServer(fc *frankenPHPContext, trackVarsArray *C.zval) {
// if the header name could not be cached, it needs to be registered safely
// this is more inefficient but allows additional sanitizing by PHP
k := phpheaders.GetUnCommonHeader(field)
k := phpheaders.GetUnCommonHeader(ctx, field)
v := strings.Join(val, ", ")
C.frankenphp_register_variable_safe(toUnsafeChar(k), toUnsafeChar(v), C.size_t(len(v)), trackVarsArray)
}
@@ -212,10 +212,12 @@ func addPreparedEnvToServer(fc *frankenPHPContext, trackVarsArray *C.zval) {
//export go_register_variables
func go_register_variables(threadIndex C.uintptr_t, trackVarsArray *C.zval) {
thread := phpThreads[threadIndex]
fc := thread.getRequestContext()
fc := thread.frankenPHPContext()
addKnownVariablesToServer(thread, fc, trackVarsArray)
addHeadersToServer(fc, trackVarsArray)
if fc.request != nil {
addKnownVariablesToServer(fc, trackVarsArray)
addHeadersToServer(thread.context(), fc.request, trackVarsArray)
}
// The Prepared Environment is registered last and can overwrite any previous values
addPreparedEnvToServer(fc, trackVarsArray)
@@ -275,11 +277,15 @@ func splitPos(path string, splitPath []string) int {
// See: https://github.com/php/php-src/blob/345e04b619c3bc11ea17ee02cdecad6ae8ce5891/main/SAPI.h#L72
//
//export go_update_request_info
func go_update_request_info(threadIndex C.uintptr_t, info *C.sapi_request_info) C.bool {
func go_update_request_info(threadIndex C.uintptr_t, info *C.sapi_request_info) {
thread := phpThreads[threadIndex]
fc := thread.getRequestContext()
fc := thread.frankenPHPContext()
request := fc.request
if request == nil {
return
}
authUser, authPassword, ok := request.BasicAuth()
if ok {
if authPassword != "" {
@@ -305,8 +311,6 @@ func go_update_request_info(threadIndex C.uintptr_t, info *C.sapi_request_info)
info.request_uri = thread.pinCString(request.URL.RequestURI())
info.proto_num = C.int(request.ProtoMajor*1000 + request.ProtoMinor)
return C.bool(fc.worker != nil)
}
// SanitizedPathJoin performs filepath.Join(root, reqPath) that

View File

@@ -2,6 +2,8 @@ package frankenphp
import (
"context"
"errors"
"fmt"
"log/slog"
"net/http"
"os"
@@ -12,6 +14,8 @@ import (
// frankenPHPContext provides contextual information about the Request to handle.
type frankenPHPContext struct {
mercureContext
documentRoot string
splitPath []string
env PreparedEnv
@@ -28,25 +32,37 @@ type frankenPHPContext struct {
// Whether the request is already closed by us
isDone bool
responseWriter http.ResponseWriter
responseWriter http.ResponseWriter
handlerParameters any
handlerReturn any
done chan any
startedAt time.Time
}
type contextHolder struct {
ctx context.Context
frankenPHPContext *frankenPHPContext
}
// fromContext extracts the frankenPHPContext from a context.
func fromContext(ctx context.Context) (fctx *frankenPHPContext, ok bool) {
fctx, ok = ctx.Value(contextKey).(*frankenPHPContext)
return
}
// NewRequestWithContext creates a new FrankenPHP request context.
func NewRequestWithContext(r *http.Request, opts ...RequestOption) (*http.Request, error) {
fc := &frankenPHPContext{
func newFrankenPHPContext() *frankenPHPContext {
return &frankenPHPContext{
done: make(chan any),
startedAt: time.Now(),
request: r,
}
}
// NewRequestWithContext creates a new FrankenPHP request context.
func NewRequestWithContext(r *http.Request, opts ...RequestOption) (*http.Request, error) {
fc := newFrankenPHPContext()
fc.request = r
for _, o := range opts {
if err := o(fc); err != nil {
return nil, err
@@ -54,7 +70,7 @@ func NewRequestWithContext(r *http.Request, opts ...RequestOption) (*http.Reques
}
if fc.logger == nil {
fc.logger = logger
fc.logger = globalLogger
}
if fc.documentRoot == "" {
@@ -84,7 +100,7 @@ func NewRequestWithContext(r *http.Request, opts ...RequestOption) (*http.Reques
// newDummyContext creates a fake context from a request path
func newDummyContext(requestPath string, opts ...RequestOption) (*frankenPHPContext, error) {
r, err := http.NewRequest(http.MethodGet, requestPath, nil)
r, err := http.NewRequestWithContext(globalCtx, http.MethodGet, requestPath, nil)
if err != nil {
return nil, err
}
@@ -110,26 +126,32 @@ func (fc *frankenPHPContext) closeContext() {
}
// validate checks if the request should be outright rejected
func (fc *frankenPHPContext) validate() bool {
func (fc *frankenPHPContext) validate() error {
if strings.Contains(fc.request.URL.Path, "\x00") {
fc.rejectBadRequest("Invalid request path")
fc.reject(ErrInvalidRequestPath)
return false
return ErrInvalidRequestPath
}
contentLengthStr := fc.request.Header.Get("Content-Length")
if contentLengthStr != "" {
if contentLength, err := strconv.Atoi(contentLengthStr); err != nil || contentLength < 0 {
fc.rejectBadRequest("invalid Content-Length header: " + contentLengthStr)
e := fmt.Errorf("%w: %q", ErrInvalidContentLengthHeader, contentLengthStr)
return false
fc.reject(e)
return e
}
}
return true
return nil
}
func (fc *frankenPHPContext) clientHasClosed() bool {
if fc.request == nil {
return false
}
select {
case <-fc.request.Context().Done():
return true
@@ -138,16 +160,22 @@ func (fc *frankenPHPContext) clientHasClosed() bool {
}
}
// reject sends a response with the given status code and message
func (fc *frankenPHPContext) reject(statusCode int, message string) {
// reject sends a response with the given status code and error
func (fc *frankenPHPContext) reject(err error) {
if fc.isDone {
return
}
re := &ErrRejected{}
if !errors.As(err, re) {
// Should never happen
panic("only instance of ErrRejected can be passed to reject")
}
rw := fc.responseWriter
if rw != nil {
rw.WriteHeader(statusCode)
_, _ = rw.Write([]byte(message))
rw.WriteHeader(re.status)
_, _ = rw.Write([]byte(err.Error()))
if f, ok := rw.(http.Flusher); ok {
f.Flush()
@@ -156,7 +184,3 @@ func (fc *frankenPHPContext) reject(statusCode int, message string) {
fc.closeContext()
}
func (fc *frankenPHPContext) rejectBadRequest(message string) {
fc.reject(http.StatusBadRequest, message)
}

View File

@@ -1,5 +1,9 @@
package frankenphp
import (
"github.com/dunglas/frankenphp/internal/state"
)
// EXPERIMENTAL: ThreadDebugState prints the state of a single PHP thread - debugging purposes only
type ThreadDebugState struct {
Index int
@@ -23,7 +27,7 @@ func DebugState() FrankenPHPDebugState {
ReservedThreadCount: 0,
}
for _, thread := range phpThreads {
if thread.state.is(stateReserved) {
if thread.state.Is(state.Reserved) {
fullState.ReservedThreadCount++
continue
}
@@ -38,9 +42,9 @@ func threadDebugState(thread *phpThread) ThreadDebugState {
return ThreadDebugState{
Index: thread.threadIndex,
Name: thread.name(),
State: thread.state.name(),
IsWaiting: thread.state.isInWaitingState(),
IsBusy: !thread.state.isInWaitingState(),
WaitingSinceMilliseconds: thread.state.waitTime(),
State: thread.state.Name(),
IsWaiting: thread.state.IsInWaitingState(),
IsBusy: !thread.state.IsInWaitingState(),
WaitingSinceMilliseconds: thread.state.WaitTime(),
}
}

View File

@@ -32,7 +32,7 @@ RUN apk add --no-cache \
zlib-dev \
bison \
nss-tools \
# file watcher
# file watcher \
libstdc++ \
linux-headers \
# Dev tools \
@@ -48,8 +48,8 @@ RUN apk add --no-cache \
echo 'set auto-load safe-path /' > /root/.gdbinit
WORKDIR /usr/local/src/php
RUN git clone --branch=PHP-8.4 https://github.com/php/php-src.git . && \
# --enable-embed is only necessary to generate libphp.so, we don't use this SAPI directly
RUN git clone --branch=PHP-8.5 https://github.com/php/php-src.git . && \
# --enable-embed is necessary to generate libphp.so, but we don't use this SAPI directly
./buildconf --force && \
EXTENSION_DIR=/usr/lib/frankenphp/modules ./configure \
--enable-embed \
@@ -71,7 +71,7 @@ RUN git clone --branch=PHP-8.4 https://github.com/php/php-src.git . && \
# Install e-dant/watcher (necessary for file watching)
WORKDIR /usr/local/src/watcher
RUN git clone https://github.com/e-dant/watcher . && \
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release && \
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release && \
cmake --build build/ && \
cmake --install build

View File

@@ -50,7 +50,7 @@ RUN apt-get update && \
apt-get clean
WORKDIR /usr/local/src/php
RUN git clone --branch=PHP-8.4 https://github.com/php/php-src.git . && \
RUN git clone --branch=PHP-8.5 https://github.com/php/php-src.git . && \
# --enable-embed is only necessary to generate libphp.so, we don't use this SAPI directly
./buildconf --force && \
EXTENSION_DIR=/usr/lib/frankenphp/modules ./configure \

View File

@@ -7,13 +7,17 @@ variable "VERSION" {
}
variable "PHP_VERSION" {
default = "8.2,8.3,8.4"
default = "8.2,8.3,8.4,8.5"
}
variable "GO_VERSION" {
default = "1.25"
}
variable "SPC_OPT_BUILD_ARGS" {
default = ""
}
variable "SHA" {}
variable "LATEST" {
@@ -24,8 +28,13 @@ variable "CACHE" {
default = ""
}
variable "CI" {
# CI flag coming from the environment or --set; empty by default
default = ""
}
variable DEFAULT_PHP_VERSION {
default = "8.4"
default = "8.5"
}
function "tag" {
@@ -91,8 +100,7 @@ target "default" {
platforms = os == "alpine" ? [
"linux/amd64",
"linux/386",
# FIXME: armv6 doesn't build in GitHub actions because we use a custom Go build
#"linux/arm/v6",
"linux/arm/v6",
"linux/arm/v7",
"linux/arm64",
] : [
@@ -141,6 +149,8 @@ target "static-builder-musl" {
}
args = {
FRANKENPHP_VERSION = VERSION
CI = CI
SPC_OPT_BUILD_ARGS = SPC_OPT_BUILD_ARGS
}
secret = ["id=github-token,env=GITHUB_TOKEN"]
}
@@ -165,6 +175,8 @@ target "static-builder-gnu" {
args = {
FRANKENPHP_VERSION = VERSION
GO_VERSION = GO_VERSION
CI = CI
SPC_OPT_BUILD_ARGS = SPC_OPT_BUILD_ARGS
}
secret = ["id=github-token,env=GITHUB_TOKEN"]
}

View File

@@ -115,22 +115,22 @@ docker buildx bake -f docker-bake.hcl --pull --no-cache --push
1. 从 GitHub 下载 FrankenPHP 二进制文件的调试版本或创建包含调试符号的自定义静态构建:
```console
docker buildx bake \
--load \
--set static-builder.args.DEBUG_SYMBOLS=1 \
--set "static-builder.platform=linux/amd64" \
static-builder
docker cp $(docker create --name static-builder-musl dunglas/frankenphp:static-builder-musl):/go/src/app/dist/frankenphp-linux-$(uname -m) frankenphp
```
```console
docker buildx bake \
--load \
--set static-builder.args.DEBUG_SYMBOLS=1 \
--set "static-builder.platform=linux/amd64" \
static-builder
docker cp $(docker create --name static-builder-musl dunglas/frankenphp:static-builder-musl):/go/src/app/dist/frankenphp-linux-$(uname -m) frankenphp
```
2. 将当前版本的 `frankenphp` 替换为 debug FrankenPHP 可执行文件
3. 照常启动 FrankenPHP或者你可以直接使用 GDB 启动 FrankenPHP `gdb --args frankenphp run`
4. 使用 GDB 附加到进程:
```console
gdb -p `pidof frankenphp`
```
```console
gdb -p `pidof frankenphp`
```
5. 如有必要,请在 GDB shell 中输入 `continue`
6. 使 FrankenPHP 崩溃
@@ -142,13 +142,13 @@ docker buildx bake -f docker-bake.hcl --pull --no-cache --push
1. 打开 `.github/workflows/tests.yml`
2. 启用 PHP 调试符号
```patch
- uses: shivammathur/setup-php@v2
# ...
env:
phpts: ts
+ debug: true
```
```patch
- uses: shivammathur/setup-php@v2
# ...
env:
phpts: ts
+ debug: true
```
3. 启用 `tmate` 以连接到容器
@@ -166,18 +166,18 @@ docker buildx bake -f docker-bake.hcl --pull --no-cache --push
5. 打开 `frankenphp.go`
6. 启用 `cgosymbolizer`
```patch
- //_ "github.com/ianlancetaylor/cgosymbolizer"
+ _ "github.com/ianlancetaylor/cgosymbolizer"
```
```patch
- //_ "github.com/ianlancetaylor/cgosymbolizer"
+ _ "github.com/ianlancetaylor/cgosymbolizer"
```
7. 下载模块: `go get`
8. 在容器中,可以使用 GDB 和以下:
```console
go test -tags watcher -c -ldflags=-w
gdb --args frankenphp.test -test.run ^MyTest$
```
```console
go test -tags watcher -c -ldflags=-w
gdb --args frankenphp.test -test.run ^MyTest$
```
9. 当错误修复后,恢复所有这些更改

View File

@@ -16,29 +16,62 @@ FrankenPHP 也可以用作独立的 Go 库,将 PHP 嵌入到任何使用 `net/
## 开始
### 独立二进制
我们为 Linux 和 macOS 提供包含 [PHP 8.4](https://www.php.net/releases/8.4/zh.php) 以及大多数常用 PHP 扩展的 FrankenPHP 静态二进制文件。
在 Windows 上,请使用 [WSL](https://learn.microsoft.com/windows/wsl/) 运行 FrankenPHP。
你可以 [下载 FrankenPHP](https://github.com/dunglas/frankenphp/releases),或将以下命令复制到终端中,自动安装适用于你平台的版本:
### 安装脚本
你可以将以下命令复制到终端中,自动安装适用于你平台的版本:
```console
curl https://frankenphp.dev/install.sh | sh
mv frankenphp /usr/local/bin/
```
要提供当前目录的内容,请运行:
### 独立二进制
我们为 Linux 和 macOS 提供用于开发的 FrankenPHP 静态二进制文件,
包含 [PHP 8.4](https://www.php.net/releases/8.4/zh.php) 以及大多数常用 PHP 扩展。
[下载 FrankenPHP](https://github.com/dunglas/frankenphp/releases)
**安装扩展:** 常见扩展已内置,无法再安装更多扩展。
### rpm 软件包
我们的维护者为所有使用 `dnf` 的系统提供 rpm 包。安装方式:
```console
frankenphp php-server
sudo dnf install https://rpm.henderkes.com/static-php-1-0.noarch.rpm
sudo dnf module enable php-zts:static-8.4 # 可用 8.2-8.5
sudo dnf install frankenphp
```
你还可以使用以下命令运行命令行脚本:
**安装扩展:** `sudo dnf install php-zts-<extension>`
对于默认不可用的扩展,请使用 [PIE](https://github.com/php/pie)
```console
frankenphp php-cli /path/to/your/script.php
sudo dnf install pie-zts
sudo pie-zts install asgrim/example-pie-extension
```
### deb 软件包
我们的维护者为所有使用 `apt` 的系统提供 deb 包。安装方式:
```console
sudo curl -fsSL https://key.henderkes.com/static-php.gpg -o /usr/share/keyrings/static-php.gpg && \
echo "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 && \
sudo apt update
sudo apt install frankenphp
```
**安装扩展:** `sudo apt install php-zts-<extension>`
对于默认不可用的扩展,请使用 [PIE](https://github.com/php/pie)
```console
sudo apt install pie-zts
sudo pie-zts install asgrim/example-pie-extension
```
### Docker
@@ -68,12 +101,28 @@ FrankenPHP 也作为 [Homebrew](https://brew.sh) 软件包提供,适用于 mac
brew install dunglas/frankenphp/frankenphp
```
**安装扩展:** 使用 [PIE](https://github.com/php/pie)。
### 用法
要提供当前目录的内容,请运行:
```console
frankenphp php-server
```
你还可以使用以下命令运行命令行脚本:
```console
frankenphp php-cli /path/to/your/script.php
```
对于 deb 和 rpm 软件包,还可以启动 systemd 服务:
```console
sudo systemctl start frankenphp
```
## 文档
- [Classic 模式](classic.md)

View File

@@ -79,10 +79,10 @@ sudo make install
某些 FrankenPHP 功能依赖于必须安装的可选系统依赖项。
或者,可以通过向 Go 编译器传递构建标签来禁用这些功能。
| 功能 | 依赖项 | 用于禁用的构建标签 |
|--------------------------|------------------------------------------------------------------------|-------------------|
| Brotli 压缩 | [Brotli](https://github.com/google/brotli) | nobrotli |
| 文件更改时重启 worker | [Watcher C](https://github.com/e-dant/watcher/tree/release/watcher-c) | nowatcher |
| 功能 | 依赖项 | 用于禁用的构建标签 |
| --------------------- | --------------------------------------------------------------------- | ------------------ |
| Brotli 压缩 | [Brotli](https://github.com/google/brotli) | nobrotli |
| 文件更改时重启 worker | [Watcher C](https://github.com/e-dant/watcher/tree/release/watcher-c) | nowatcher |
## 编译 Go 应用

View File

@@ -2,7 +2,7 @@
[FrankenPHP Docker 镜像](https://hub.docker.com/r/dunglas/frankenphp) 基于 [官方 PHP 镜像](https://hub.docker.com/_/php/)。提供适用于流行架构的 Debian 和 Alpine Linux 变体。推荐使用 Debian 变体。
提供 PHP 8.2、8.3 和 8.4 的变体。
提供 PHP 8.2、8.3、8.4 和 8.5 的变体。
标签遵循此模式:`dunglas/frankenphp:<frankenphp-version>-php<php-version>-<os>`

View File

@@ -14,10 +14,10 @@ FrankenPHP 能够将 PHP 应用程序的源代码和资源文件嵌入到静态
例如,你可能希望:
* 给应用安装生产环境的依赖
* 导出 autoloader
* 如果可能,为应用启用生产模式
* 丢弃不需要的文件,例如 `.git` 或测试文件,以减小最终二进制文件的大小
- 给应用安装生产环境的依赖
- 导出 autoloader
- 如果可能,为应用启用生产模式
- 丢弃不需要的文件,例如 `.git` 或测试文件,以减小最终二进制文件的大小
例如,对于 Symfony 应用程序,你可以使用以下命令:
@@ -53,34 +53,35 @@ composer dump-env prod
1. 在准备好的应用的存储库中创建一个名为 `static-build.Dockerfile` 的文件。
```dockerfile
FROM --platform=linux/amd64 dunglas/frankenphp:static-builder
```dockerfile
FROM --platform=linux/amd64 dunglas/frankenphp:static-builder-gnu
# 如果你打算在 glibc 系统上运行该二进制文件,请使用 static-builder-gnu
# 复制应用代码
WORKDIR /go/src/app/dist/app
COPY . .
# 复制应用代码
WORKDIR /go/src/app/dist/app
COPY . .
# 构建静态二进制文件
# 构建静态二进制文件
WORKDIR /go/src/app/
RUN EMBED=dist/app/ ./build-static.sh
```
> [!CAUTION]
>
> 某些 `.dockerignore` 文件(例如默认的 [Symfony Docker `.dockerignore`](https://github.com/dunglas/symfony-docker/blob/main/.dockerignore)
> 会忽略 `vendor/` 文件夹和 `.env` 文件。在构建之前,请务必调整或删除 `.dockerignore` 文件。
> [!CAUTION]
>
> 某些 `.dockerignore` 文件(例如默认的 [Symfony Docker `.dockerignore`](https://github.com/dunglas/symfony-docker/blob/main/.dockerignore)
> 会忽略 `vendor/` 文件夹和 `.env` 文件。在构建之前,请务必调整或删除 `.dockerignore` 文件。
2. 构建:
```console
docker build -t static-app -f static-build.Dockerfile .
```
```console
docker build -t static-app -f static-build.Dockerfile .
```
3. 提取二进制文件
```console
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
```
```console
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
```
生成的二进制文件是当前目录中名为 `my-app` 的文件。

View File

@@ -72,8 +72,8 @@ func repeat_this(s *C.zend_string, count int64, reverse bool) unsafe.Pointer {
这里有两个重要的事情要注意:
* 指令注释 `//export_php:function` 定义了 PHP 中的函数签名。这是生成器知道如何使用正确的参数和返回类型生成 PHP 函数的方式;
* 函数必须返回 `unsafe.Pointer`。FrankenPHP 提供了一个 API 来帮助你在 C 和 Go 之间进行类型转换。
- 指令注释 `//export_php:function` 定义了 PHP 中的函数签名。这是生成器知道如何使用正确的参数和返回类型生成 PHP 函数的方式;
- 函数必须返回 `unsafe.Pointer`。FrankenPHP 提供了一个 API 来帮助你在 C 和 Go 之间进行类型转换。
虽然第一点不言自明,但第二点可能更难理解。让我们在下一节中深入了解类型转换。
@@ -82,16 +82,17 @@ func repeat_this(s *C.zend_string, count int64, reverse bool) unsafe.Pointer {
虽然一些变量类型在 C/PHP 和 Go 之间具有相同的内存表示,但某些类型需要更多逻辑才能直接使用。这可能是编写扩展时最困难的部分,因为它需要了解 Zend 引擎的内部结构以及变量在 PHP 中的内部存储方式。此表总结了你需要知道的内容:
| PHP 类型 | Go 类型 | 直接转换 | C 到 Go 助手 | Go 到 C 助手 | 类方法支持 |
|--------------------|---------------------|----------|----------------------|----------------------|------------|
| `int` | `int64` | ✅ | - | - | ✅ |
| `?int` | `*int64` | ✅ | - | - | ✅ |
| `float` | `float64` | ✅ | - | - | ✅ |
| `?float` | `*float64` | ✅ | - | - | ✅ |
| `bool` | `bool` | ✅ | - | - | ✅ |
| `?bool` | `*bool` | ✅ | - | - | ✅ |
| `string`/`?string` | `*C.zend_string` | ❌ | frankenphp.GoString() | frankenphp.PHPString() | ✅ |
| `array` | `*frankenphp.Array` | ❌ | frankenphp.GoArray() | frankenphp.PHPArray() | ✅ |
| `object` | `struct` | ❌ | _尚未实现_ | _尚未实现_ | ❌ |
| ------------------ | ------------------- | -------- | --------------------- | ---------------------- | ---------- |
| `int` | `int64` | ✅ | - | - | ✅ |
| `?int` | `*int64` | ✅ | - | - | ✅ |
| `float` | `float64` | ✅ | - | - | ✅ |
| `?float` | `*float64` | ✅ | - | - | ✅ |
| `bool` | `bool` | ✅ | - | - | ✅ |
| `?bool` | `*bool` | ✅ | - | - | ✅ |
| `string`/`?string` | `*C.zend_string` | ❌ | frankenphp.GoString() | frankenphp.PHPString() | ✅ |
| `array` | `*frankenphp.Array` | ❌ | frankenphp.GoArray() | frankenphp.PHPArray() | ✅ |
| `mixed` | `any` | ❌ | `GoValue()` | `PHPValue()` | ❌ |
| `object` | `struct` | ❌ | _尚未实现_ | _尚未实现_ | ❌ |
> [!NOTE]
> 此表尚不详尽,将随着 FrankenPHP 类型 API 变得更加完整而完善。
@@ -111,16 +112,16 @@ FrankenPHP 通过 `frankenphp.Array` 类型为 PHP 数组提供原生支持。
func process_data(arr *C.zval) unsafe.Pointer {
// 将 PHP 数组转换为 Go
goArray := frankenphp.GoArray(unsafe.Pointer(arr))
result := &frankenphp.Array{}
result.SetInt(0, "first")
result.SetInt(1, "second")
result.Append("third") // 自动分配下一个整数键
result.SetString("name", "John")
result.SetString("age", int64(30))
for i := uint32(0); i < goArray.Len(); i++ {
key, value := goArray.At(i)
if key.Type == frankenphp.PHPStringKey {
@@ -129,7 +130,7 @@ func process_data(arr *C.zval) unsafe.Pointer {
result.SetInt(key.Int+100, value)
}
}
// 转换回 PHP 数组
return frankenphp.PHPArray(result)
}
@@ -137,20 +138,20 @@ func process_data(arr *C.zval) unsafe.Pointer {
**`frankenphp.Array` 的关键特性:**
* **有序键值对** - 像 PHP 数组一样维护插入顺序
* **混合键类型** - 在同一数组中支持整数和字符串键
* **类型安全** - `PHPKey` 类型确保正确的键处理
* **自动列表检测** - 转换为 PHP 时,自动检测数组应该是打包列表还是哈希映射
* **不支持对象** - 目前,只有标量类型和数组可以用作值。提供对象将导致 PHP 数组中的 `null` 值。
- **有序键值对** - 像 PHP 数组一样维护插入顺序
- **混合键类型** - 在同一数组中支持整数和字符串键
- **类型安全** - `PHPKey` 类型确保正确的键处理
- **自动列表检测** - 转换为 PHP 时,自动检测数组应该是打包列表还是哈希映射
- **不支持对象** - 目前,只有标量类型和数组可以用作值。提供对象将导致 PHP 数组中的 `null` 值。
**可用方法:**
* `SetInt(key int64, value interface{})` - 使用整数键设置值
* `SetString(key string, value interface{})` - 使用字符串键设置值
* `Append(value interface{})` - 使用下一个可用整数键添加值
* `Len() uint32` - 获取元素数量
* `At(index uint32) (PHPKey, interface{})` - 获取索引处的键值对
* `frankenphp.PHPArray(arr *frankenphp.Array) unsafe.Pointer` - 转换为 PHP 数组
- `SetInt(key int64, value any)` - 使用整数键设置值
- `SetString(key string, value any)` - 使用字符串键设置值
- `Append(value any)` - 使用下一个可用整数键添加值
- `Len() uint32` - 获取元素数量
- `At(index uint32) (PHPKey, any)` - 获取索引处的键值对
- `frankenphp.PHPArray(arr *frankenphp.Array) unsafe.Pointer` - 转换为 PHP 数组
### 声明原生 PHP 类
@@ -168,11 +169,11 @@ type UserStruct struct {
**不透明类**是内部结构(属性)对 PHP 代码隐藏的类。这意味着:
* **无直接属性访问**:你不能直接从 PHP 读取或写入属性(`$user->name` 不起作用)
* **仅方法接口** - 所有交互必须通过你定义的方法进行
* **更好的封装** - 内部数据结构完全由 Go 代码控制
* **类型安全** - 没有 PHP 代码使用错误类型破坏内部状态的风险
* **更清晰的 API** - 强制设计适当的公共接口
- **无直接属性访问**:你不能直接从 PHP 读取或写入属性(`$user->name` 不起作用)
- **仅方法接口** - 所有交互必须通过你定义的方法进行
- **更好的封装** - 内部数据结构完全由 Go 代码控制
- **类型安全** - 没有 PHP 代码使用错误类型破坏内部状态的风险
- **更清晰的 API** - 强制设计适当的公共接口
这种方法提供了更好的封装,并防止 PHP 代码意外破坏 Go 对象的内部状态。与对象的所有交互都必须通过你明确定义的方法进行。
@@ -219,12 +220,12 @@ func (us *UserStruct) UpdateInfo(name *C.zend_string, age *int64, active *bool)
if name != nil {
us.Name = frankenphp.GoString(unsafe.Pointer(name))
}
// 检查是否提供了 age不为 null
if age != nil {
us.Age = int(*age)
}
// 检查是否提供了 active不为 null
if active != nil {
us.Active = *active
@@ -234,10 +235,10 @@ func (us *UserStruct) UpdateInfo(name *C.zend_string, age *int64, active *bool)
**关于可空参数的要点:**
* **可空原始类型**`?int``?float``?bool`)在 Go 中变成指针(`*int64``*float64``*bool`
* **可空字符串**`?string`)仍然是 `*C.zend_string`,但可以是 `nil`
* **在解引用指针值之前检查 `nil`**
* **PHP `null` 变成 Go `nil`** - 当 PHP 传递 `null` 时,你的 Go 函数接收 `nil` 指针
- **可空原始类型**`?int``?float``?bool`)在 Go 中变成指针(`*int64``*float64``*bool`
- **可空字符串**`?string`)仍然是 `*C.zend_string`,但可以是 `nil`
- **在解引用指针值之前检查 `nil`**
- **PHP `null` 变成 Go `nil`** - 当 PHP 传递 `null` 时,你的 Go 函数接收 `nil` 指针
> [!WARNING]
> 目前,类方法有以下限制。**不支持对象**作为参数类型或返回类型。**完全支持数组**作为参数和返回类型。支持的类型:`string`、`int`、`float`、`bool`、`array` 和 `void`(用于返回类型)。**完全支持可空参数类型**,适用于所有标量类型(`?string`、`?int`、`?float`、`?bool`)。
@@ -269,7 +270,7 @@ $user->updateInfo(null, 25, null); // Name 和 active 为 null
### 声明常量
生成器支持使用两个指令将 Go 常量导出到 PHP`//export_php:const` 用于全局常量,`//export_php:classconstant` 用于类常量。这允许你在 Go 和 PHP 代码之间共享配置值、状态代码和其他常量。
生成器支持使用两个指令将 Go 常量导出到 PHP`//export_php:const` 用于全局常量,`//export_php:classconst` 用于类常量。这允许你在 Go 和 PHP 代码之间共享配置值、状态代码和其他常量。
#### 全局常量
@@ -291,25 +292,25 @@ const STATUS_ERROR = iota
#### 类常量
使用 `//export_php:classconstant ClassName` 指令创建属于特定 PHP 类的常量:
使用 `//export_php:classconst ClassName` 指令创建属于特定 PHP 类的常量:
```go
//export_php:classconstant User
//export_php:classconst User
const STATUS_ACTIVE = 1
//export_php:classconstant User
//export_php:classconst User
const STATUS_INACTIVE = 0
//export_php:classconstant User
//export_php:classconst User
const ROLE_ADMIN = "admin"
//export_php:classconstant Order
//export_php:classconst Order
const STATE_PENDING = iota
//export_php:classconstant Order
//export_php:classconst Order
const STATE_PROCESSING = iota
//export_php:classconstant Order
//export_php:classconst Order
const STATE_COMPLETED = iota
```
@@ -345,10 +346,10 @@ const STR_REVERSE = iota
//export_php:const
const STR_NORMAL = iota
//export_php:classconstant StringProcessor
//export_php:classconst StringProcessor
const MODE_LOWERCASE = 1
//export_php:classconstant StringProcessor
//export_php:classconst StringProcessor
const MODE_UPPERCASE = 2
//export_php:function repeat_this(string $str, int $count, int $mode): string
@@ -356,7 +357,7 @@ func repeat_this(s *C.zend_string, count int64, mode int) unsafe.Pointer {
str := frankenphp.GoString(unsafe.Pointer(s))
result := strings.Repeat(str, int(count))
if mode == STR_REVERSE {
if mode == STR_REVERSE {
// 反转字符串
}
@@ -375,14 +376,14 @@ type StringProcessorStruct struct {
//export_php:method StringProcessor::process(string $input, int $mode): string
func (sp *StringProcessorStruct) Process(input *C.zend_string, mode int64) unsafe.Pointer {
str := frankenphp.GoString(unsafe.Pointer(input))
switch mode {
case MODE_LOWERCASE:
str = strings.ToLower(str)
case MODE_UPPERCASE:
str = strings.ToUpper(str)
}
return frankenphp.PHPString(str, false)
}
```
@@ -437,17 +438,17 @@ echo My\Extension\STATUS_ACTIVE; // 1
#### 重要说明
* 每个文件只允许**一个**命名空间指令。如果找到多个命名空间指令,生成器将返回错误。
* 命名空间适用于文件中的**所有**导出符号:函数、类、方法和常量。
* 命名空间名称遵循 PHP 命名空间约定,使用反斜杠(`\`)作为分隔符。
* 如果没有声明命名空间,符号将照常导出到全局命名空间。
- 每个文件只允许**一个**命名空间指令。如果找到多个命名空间指令,生成器将返回错误。
- 命名空间适用于文件中的**所有**导出符号:函数、类、方法和常量。
- 命名空间名称遵循 PHP 命名空间约定,使用反斜杠(`\`)作为分隔符。
- 如果没有声明命名空间,符号将照常导出到全局命名空间。
### 生成扩展
这就是魔法发生的地方,现在可以生成你的扩展。你可以使用以下命令运行生成器:
```console
GEN_STUB_SCRIPT=php-src/build/gen_stub.php frankenphp extension-init my_extension.go
GEN_STUB_SCRIPT=php-src/build/gen_stub.php frankenphp extension-init my_extension.go
```
> [!NOTE]
@@ -567,9 +568,9 @@ extern zend_module_entry ext_module_entry;
接下来,创建一个名为 `extension.c` 的文件,该文件将执行以下步骤:
* 包含 PHP 头文件;
* 声明我们的新原生 PHP 函数 `go_print()`
* 声明扩展元数据。
- 包含 PHP 头文件;
- 声明我们的新原生 PHP 函数 `go_print()`
- 声明扩展元数据。
让我们首先包含所需的头文件:
@@ -701,9 +702,9 @@ import "strings"
//export go_upper
func go_upper(s *C.zend_string) *C.zend_string {
str := frankenphp.GoString(unsafe.Pointer(s))
upper := strings.ToUpper(str)
return (*C.zend_string)(frankenphp.PHPString(upper, false))
}
```

View File

@@ -4,8 +4,8 @@
已知以下扩展与 FrankenPHP 不兼容:
| 名称 | 原因 | 替代方案 |
| ----------------------------------------------------------------------------------------------------------- | --------------- | -------------------------------------------------------------------------------------------------------------------- |
| 名称 | 原因 | 替代方案 |
| ----------------------------------------------------------------------------------------------------------- | ------------ | -------------------------------------------------------------------------------------------------------------------- |
| [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) |
| [newrelic](https://docs.newrelic.com/docs/apm/agents/php-agent/getting-started/introduction-new-relic-php/) | 不安全的线程 | - |
@@ -13,8 +13,8 @@
以下扩展在与 FrankenPHP 一起使用时已知存在错误和意外行为:
| 名称 | 问题 |
| ------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 名称 | 问题 |
| ------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [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)。 |
## get_browser

View File

@@ -19,23 +19,23 @@ docker run -p 80:80 -p 443:443 -p 443:443/udp -v $PWD:/app dunglas/frankenphp
1. [下载与你的系统相对应的二进制文件](https://github.com/php/frankenphp/releases)
2. 将以下配置添加到 Laravel 项目根目录中名为 `Caddyfile` 的文件中:
```caddyfile
{
frankenphp
}
```caddyfile
{
frankenphp
}
# 服务器的域名
localhost {
# 将 webroot 设置为 public/ 目录
root public/
# 启用压缩(可选)
encode zstd br gzip
# 执行当前目录中的 PHP 文件并提供资源
php_server {
try_files {path} index.php
}
}
```
# 服务器的域名
localhost {
# 将 webroot 设置为 public/ 目录
root public/
# 启用压缩(可选)
encode zstd br gzip
# 执行当前目录中的 PHP 文件并提供资源
php_server {
try_files {path} index.php
}
}
```
3. 从 Laravel 项目的根目录启动 FrankenPHP`frankenphp run`
@@ -61,17 +61,17 @@ php artisan octane:frankenphp
`octane:frankenphp` 命令可以采用以下选项:
* `--host`: 服务器应绑定到的 IP 地址(默认值: `127.0.0.1`
* `--port`: 服务器应可用的端口(默认值: `8000`
* `--admin-port`: 管理服务器应可用的端口(默认值: `2019`
* `--workers`: 应可用于处理请求的 worker 数(默认值: `auto`
* `--max-requests`: 在 worker 重启之前要处理的请求数(默认值: `500`
* `--caddyfile`FrankenPHP `Caddyfile` 文件的路径(默认: [Laravel Octane 中的存根 `Caddyfile`](https://github.com/laravel/octane/blob/2.x/src/Commands/stubs/Caddyfile)
* `--https`: 开启 HTTPS、HTTP/2 和 HTTP/3自动生成和延长证书
* `--http-redirect`: 启用 HTTP 到 HTTPS 重定向(仅在使用 `--https` 时启用)
* `--watch`: 修改应用程序时自动重新加载服务器
* `--poll`: 在监视时使用文件系统轮询,以便通过网络监视文件
* `--log-level`: 在指定日志级别或高于指定日志级别的日志消息
- `--host`: 服务器应绑定到的 IP 地址(默认值: `127.0.0.1`
- `--port`: 服务器应可用的端口(默认值: `8000`
- `--admin-port`: 管理服务器应可用的端口(默认值: `2019`
- `--workers`: 应可用于处理请求的 worker 数(默认值: `auto`
- `--max-requests`: 在 worker 重启之前要处理的请求数(默认值: `500`
- `--caddyfile`FrankenPHP `Caddyfile` 文件的路径(默认: [Laravel Octane 中的存根 `Caddyfile`](https://github.com/laravel/octane/blob/2.x/src/Commands/stubs/Caddyfile)
- `--https`: 开启 HTTPS、HTTP/2 和 HTTP/3自动生成和延长证书
- `--http-redirect`: 启用 HTTP 到 HTTPS 重定向(仅在使用 `--https` 时启用)
- `--watch`: 修改应用程序时自动重新加载服务器
- `--poll`: 在监视时使用文件系统轮询,以便通过网络监视文件
- `--log-level`: 在指定日志级别或高于指定日志级别的日志消息
> [!TIP]
> 要获取结构化的 JSON 日志(在使用日志分析解决方案时非常有用),请明确传递 `--log-level` 选项。
@@ -88,7 +88,8 @@ php artisan octane:frankenphp
1. 在您的应用程序的存储库中创建一个名为 `static-build.Dockerfile` 的文件:
```dockerfile
FROM --platform=linux/amd64 dunglas/frankenphp:static-builder
FROM --platform=linux/amd64 dunglas/frankenphp:static-builder-gnu
# 如果你打算在 musl-libc 系统上运行该二进制文件,请使用 static-builder-musl
# 复制你的应用
WORKDIR /go/src/app/dist/app

View File

@@ -117,17 +117,17 @@ cd frankenphp
以下环境变量可以传递给 `docker build``build-static.sh`
脚本来自定义静态构建:
* `FRANKENPHP_VERSION`: 要使用的 FrankenPHP 版本
* `PHP_VERSION`: 要使用的 PHP 版本
* `PHP_EXTENSIONS`: 要构建的 PHP 扩展([支持的扩展列表](https://static-php.dev/zh/guide/extensions.html)
* `PHP_EXTENSION_LIBS`: 要构建的额外库,为扩展添加额外的功能
* `XCADDY_ARGS`:传递给 [xcaddy](https://github.com/caddyserver/xcaddy) 的参数,例如用于添加额外的 Caddy 模块
* `EMBED`: 要嵌入二进制文件的 PHP 应用程序的路径
* `CLEAN`: 设置后libphp 及其所有依赖项都是重新构建的(不使用缓存)
* `NO_COMPRESS`: 不要使用UPX压缩生成的二进制文件
* `DEBUG_SYMBOLS`: 设置后,调试符号将被保留在二进制文件内
* `MIMALLOC`: (实验性仅限Linux) 用[mimalloc](https://github.com/microsoft/mimalloc)替换musl的mallocng以提高性能。我们仅建议在musl目标构建中使用此选项对于glibc建议禁用此选项并在运行二进制文件时使用[`LD_PRELOAD`](https://microsoft.github.io/mimalloc/overrides.html)。
* `RELEASE`: (仅限维护者)设置后,生成的二进制文件将上传到 GitHub 上
- `FRANKENPHP_VERSION`: 要使用的 FrankenPHP 版本
- `PHP_VERSION`: 要使用的 PHP 版本
- `PHP_EXTENSIONS`: 要构建的 PHP 扩展([支持的扩展列表](https://static-php.dev/zh/guide/extensions.html)
- `PHP_EXTENSION_LIBS`: 要构建的额外库,为扩展添加额外的功能
- `XCADDY_ARGS`:传递给 [xcaddy](https://github.com/caddyserver/xcaddy) 的参数,例如用于添加额外的 Caddy 模块
- `EMBED`: 要嵌入二进制文件的 PHP 应用程序的路径
- `CLEAN`: 设置后libphp 及其所有依赖项都是重新构建的(不使用缓存)
- `NO_COMPRESS`: 不要使用UPX压缩生成的二进制文件
- `DEBUG_SYMBOLS`: 设置后,调试符号将被保留在二进制文件内
- `MIMALLOC`: (实验性仅限Linux) 用[mimalloc](https://github.com/microsoft/mimalloc)替换musl的mallocng以提高性能。我们仅建议在musl目标构建中使用此选项对于glibc建议禁用此选项并在运行二进制文件时使用[`LD_PRELOAD`](https://microsoft.github.io/mimalloc/overrides.html)。
- `RELEASE`: (仅限维护者)设置后,生成的二进制文件将上传到 GitHub 上
## 扩展

View File

@@ -79,10 +79,11 @@ sudo make install
Some FrankenPHP features depend on optional system dependencies that must be installed.
Alternatively, these features can be disabled by passing build tags to the Go compiler.
| Feature | Dependency | Build tag to disable it |
|--------------------------------|-----------------------------------------------------------------------|-------------------------|
| Brotli compression | [Brotli](https://github.com/google/brotli) | nobrotli |
| Restart workers on file change | [Watcher C](https://github.com/e-dant/watcher/tree/release/watcher-c) | nowatcher |
| Feature | Dependency | Build tag to disable it |
| ------------------------------ | ------------------------------------------------------------------------------------------------------------ | ----------------------- |
| Brotli compression | [Brotli](https://github.com/google/brotli) | nobrotli |
| Restart workers on file change | [Watcher C](https://github.com/e-dant/watcher/tree/release/watcher-c) | nowatcher |
| [Mercure](mercure.md) | [Mercure Go library](https://pkg.go.dev/github.com/dunglas/mercure) (automatically installed, AGPL licensed) | nomercure |
## Compile the Go App
@@ -102,8 +103,13 @@ xcaddy build \
--output frankenphp \
--with github.com/dunglas/frankenphp/caddy \
--with github.com/dunglas/mercure/caddy \
--with github.com/dunglas/vulcain/caddy
--with github.com/dunglas/vulcain/caddy \
--with github.com/dunglas/caddy-cbrotli
# Add extra Caddy modules and FrankenPHP extensions here
# optionally, if you would like to compile from your frankenphp sources:
# --with github.com/dunglas/frankenphp=$(pwd) \
# --with github.com/dunglas/frankenphp/caddy=$(pwd)/caddy
```
> [!TIP]

View File

@@ -1,17 +1,38 @@
# Configuration
FrankenPHP, Caddy as well as the Mercure and Vulcain modules can be configured using [the formats supported by Caddy](https://caddyserver.com/docs/getting-started#your-first-config).
FrankenPHP, 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).
In [the Docker images](docker.md), the `Caddyfile` is located at `/etc/frankenphp/Caddyfile`.
The static binary will also look for the `Caddyfile` in the directory where the `frankenphp run` command is executed.
The most common format is the `Caddyfile`, which is a simple, human-readable text format.
By default, FrankenPHP will look for a `Caddyfile` in the current directory.
You can specify a custom path with the `-c` or `--config` option.
A minimal `Caddyfile` to serve a PHP application is shown below:
```caddyfile
# The hostname to respond to
localhost
# Optionaly, the directory to serve files from, otherwise defaults to the current directory
#root public/
php_server
```
A 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),
and with Docker images.
PHP itself can be configured [using a `php.ini` file](https://www.php.net/manual/en/configuration.file.php).
Depending on your installation method, the PHP interpreter will look for configuration files in locations described below.
Depending on your installation method, FrankenPHP and the PHP interpreter will look for configuration files in locations described below.
## Docker
FrankenPHP:
- `/etc/frankenphp/Caddyfile`: the main configuration file
- `/etc/frankenphp/Caddyfile.d/*.caddyfile`: additional configuration files that are loaded automatically
PHP:
- `php.ini`: `/usr/local/etc/php/php.ini` (no `php.ini` is provided by default)
- additional configuration files: `/usr/local/etc/php/conf.d/*.ini`
- PHP extensions: `/usr/local/lib/php/extensions/no-debug-zts-<YYYYMMDD>/`
@@ -29,12 +50,24 @@ RUN cp $PHP_INI_DIR/php.ini-development $PHP_INI_DIR/php.ini
## RPM and Debian packages
- `php.ini`: `/etc/frankenphp/php.ini` (a `php.ini` file with production presets is provided by default)
- additional configuration files: `/etc/frankenphp/php.d/*.ini`
- PHP extensions: `/usr/lib/frankenphp/modules/`
FrankenPHP:
- `/etc/frankenphp/Caddyfile`: the main configuration file
- `/etc/frankenphp/Caddyfile.d/*.caddyfile`: additional configuration files that are loaded automatically
PHP:
- `php.ini`: `/etc/php-zts/php.ini` (a `php.ini` file with production presets is provided by default)
- additional configuration files: `/etc/php-zts/conf.d/*.ini`
## Static binary
FrankenPHP:
- In the current working directory: `Caddyfile`
PHP:
- `php.ini`: The directory in which `frankenphp run` or `frankenphp php-server` is executed, then `/etc/frankenphp/php.ini`
- additional configuration files: `/etc/frankenphp/php.d/*.ini`
- PHP extensions: cannot be loaded, bundle them in the binary itself
@@ -229,34 +262,6 @@ and otherwise forward the request to the worker matching the path pattern.
}
```
### Full Duplex (HTTP/1)
When using HTTP/1.x, it may be desirable to enable full-duplex mode to allow writing a response before the entire body
has been read. (for example: WebSocket, Server-Sent Events, etc.)
This is an opt-in configuration that needs to be added to the global options in the `Caddyfile`:
```caddyfile
{
servers {
enable_full_duplex
}
}
```
> [!CAUTION]
>
> Enabling this option may cause old HTTP/1.x clients that don't support full-duplex to deadlock.
> This can also be configured using the `CADDY_GLOBAL_OPTIONS` environment config:
```sh
CADDY_GLOBAL_OPTIONS="servers {
enable_full_duplex
}"
```
You can find more information about this setting in the [Caddy documentation](https://caddyserver.com/docs/caddyfile/options#enable-full-duplex).
## Environment Variables
The following environment variables can be used to inject Caddy directives in the `Caddyfile` without modifying it:
@@ -293,6 +298,43 @@ You can also change the PHP configuration using the `php_ini` directive in the `
}
```
### Disabling HTTPS
By default, FrankenPHP will automatically enable HTTPS using for all the hostnames, including `localhost`.
If you want to disable HTTPS (for example in a development environment), you can set the `SERVER_NAME` environment variable to `http://` or `:80`:
Alternatively, you can use all other methods described in the [Caddy documentation](https://caddyserver.com/docs/automatic-https#activation).
If 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.
### Full Duplex (HTTP/1)
When using HTTP/1.x, it may be desirable to enable full-duplex mode to allow writing a response before the entire body
has been read. (for example: [Mercure](mercure.md), WebSocket, Server-Sent Events, etc.)
This is an opt-in configuration that needs to be added to the global options in the `Caddyfile`:
```caddyfile
{
servers {
enable_full_duplex
}
}
```
> [!CAUTION]
>
> Enabling this option may cause old HTTP/1.x clients that don't support full-duplex to deadlock.
> This can also be configured using the `CADDY_GLOBAL_OPTIONS` environment config:
```sh
CADDY_GLOBAL_OPTIONS="servers {
enable_full_duplex
}"
```
You can find more information about this setting in the [Caddy documentation](https://caddyserver.com/docs/caddyfile/options#enable-full-duplex).
## Enable the Debug Mode
When using the Docker image, set the `CADDY_GLOBAL_OPTIONS` environment variable to `debug` to enable the debug mode:

View File

@@ -1,8 +1,10 @@
# Building Custom Docker Image
[FrankenPHP Docker images](https://hub.docker.com/r/dunglas/frankenphp) are based on [official PHP images](https://hub.docker.com/_/php/). Debian and Alpine Linux variants are provided for popular architectures. Debian variants are recommended.
[FrankenPHP Docker images](https://hub.docker.com/r/dunglas/frankenphp) are based on [official PHP images](https://hub.docker.com/_/php/).
Debian and Alpine Linux variants are provided for popular architectures.
Debian variants are recommended.
Variants for PHP 8.2, 8.3 and 8.4 are provided.
Variants for PHP 8.2, 8.3, 8.4 and 8.5 are provided.
The tags follow this pattern: `dunglas/frankenphp:<frankenphp-version>-php<php-version>-<os>`
@@ -28,6 +30,11 @@ docker build -t my-php-app .
docker run -it --rm --name my-running-app my-php-app
```
## How to Tweak the Configuration
For convenience, [a default `Caddyfile`](https://github.com/php/frankenphp/blob/main/caddy/frankenphp/Caddyfile) containing
useful environment variables is provided in the image.
## How to Install More PHP Extensions
The [`docker-php-extension-installer`](https://github.com/mlocati/docker-php-extension-installer) script is provided in the base image.

View File

@@ -54,7 +54,8 @@ The easiest way to create a Linux binary is to use the Docker-based builder we p
1. Create a file named `static-build.Dockerfile` in the repository of your app:
```dockerfile
FROM --platform=linux/amd64 dunglas/frankenphp:static-builder
FROM --platform=linux/amd64 dunglas/frankenphp:static-builder-gnu
# If you intend to run the binary on musl-libc systems, use static-builder-musl instead
# Copy your app
WORKDIR /go/src/app/dist/app

View File

@@ -33,7 +33,7 @@ As covered in the manual implementation section below as well, you need to [get
The first step to writing a PHP extension in Go is to create a new Go module. You can use the following command for this:
```console
go mod init github.com/my-account/my-module
go mod init example.com/example
```
The 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:
@@ -47,10 +47,15 @@ tar xf php-*
Everything 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:
```go
package example
// #include <Zend/zend_types.h>
import "C"
import (
"C"
"github.com/dunglas/frankenphp"
"strings"
"unsafe"
"github.com/dunglas/frankenphp"
)
//export_php:function repeat_this(string $str, int $count, bool $reverse): string
@@ -72,30 +77,34 @@ func repeat_this(s *C.zend_string, count int64, reverse bool) unsafe.Pointer {
There are two important things to note here:
* 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;
* The function must return an `unsafe.Pointer`. FrankenPHP provides an API to help you with type juggling between C and Go.
- 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;
- The function must return an `unsafe.Pointer`. FrankenPHP provides an API to help you with type juggling between C and Go.
While 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.
### Type Juggling
While 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. This table summarizes what you need to know:
While 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.
This table summarizes what you need to know:
| PHP type | Go type | Direct conversion | C to Go helper | Go to C helper | Class Methods Support |
|--------------------|-------------------------------|-------------------|-----------------------------------|------------------------------------|-----------------------|
| `int` | `int64` | ✅ | - | - | ✅ |
| `?int` | `*int64` | ✅ | - | - | ✅ |
| `float` | `float64` | ✅ | - | - | ✅ |
| `?float` | `*float64` | ✅ | - | - | ✅ |
| `bool` | `bool` | ✅ | - | - | ✅ |
| `?bool` | `*bool` | ✅ | - | - | ✅ |
| `string`/`?string` | `*C.zend_string` | ❌ | `frankenphp.GoString()` | `frankenphp.PHPString()` | ✅ |
| `array` | `frankenphp.AssociativeArray` | ❌ | `frankenphp.GoAssociativeArray()` | `frankenphp.PHPAssociativeArray()` | ✅ |
| `array` | `map[string]any` | ❌ | `frankenphp.GoMap()` | `frankenphp.PHPMap()` | ✅ |
| `array` | `[]any` | ❌ | `frankenphp.GoPackedArray()` | `frankenphp.PHPPackedArray()` | ✅ |
| `object` | `struct` | ❌ | _Not yet implemented_ | _Not yet implemented_ | ❌ |
| `int` | `int64` | ✅ | - | - | ✅ |
| `?int` | `*int64` | ✅ | - | - | ✅ |
| `float` | `float64` | ✅ | - | - | ✅ |
| `?float` | `*float64` | ✅ | - | - | ✅ |
| `bool` | `bool` | ✅ | - | - | ✅ |
| `?bool` | `*bool` | ✅ | - | - | ✅ |
| `string`/`?string` | `*C.zend_string` | ❌ | `frankenphp.GoString()` | `frankenphp.PHPString()` | ✅ |
| `array` | `frankenphp.AssociativeArray` | ❌ | `frankenphp.GoAssociativeArray()` | `frankenphp.PHPAssociativeArray()` | ✅ |
| `array` | `map[string]any` | ❌ | `frankenphp.GoMap()` | `frankenphp.PHPMap()` | ✅ |
| `array` | `[]any` | ❌ | `frankenphp.GoPackedArray()` | `frankenphp.PHPPackedArray()` | ✅ |
| `mixed` | `any` | ❌ | `GoValue()` | `PHPValue()` | ❌ |
| `callable` | `*C.zval` | ❌ | - | frankenphp.CallPHPCallable() | ❌ |
| `object` | `struct` | ❌ | _Not yet implemented_ | _Not yet implemented_ | ❌ |
> [!NOTE]
>
> This table is not exhaustive yet and will be completed as the FrankenPHP types API gets more complete.
>
> For class methods specifically, primitive types and arrays are currently supported. Objects cannot be used as method parameters or return types yet.
@@ -113,10 +122,23 @@ If order or association are not needed, it's also possible to directly convert t
**Creating and manipulating arrays in Go:**
```go
package example
// #include <Zend/zend_types.h>
import "C"
import (
"unsafe"
"github.com/dunglas/frankenphp"
)
// export_php:function process_data_ordered(array $input): array
func process_data_ordered_map(arr *C.zval) unsafe.Pointer {
func process_data_ordered_map(arr *C.zend_array) unsafe.Pointer {
// Convert PHP associative array to Go while keeping the order
associativeArray := frankenphp.GoAssociativeArray(unsafe.Pointer(arr))
associativeArray, err := frankenphp.GoAssociativeArray[any](unsafe.Pointer(arr))
if err != nil {
// handle error
}
// loop over the entries in order
for _, key := range associativeArray.Order {
@@ -126,8 +148,8 @@ func process_data_ordered_map(arr *C.zval) unsafe.Pointer {
// return an ordered array
// if 'Order' is not empty, only the key-value pairs in 'Order' will be respected
return frankenphp.PHPAssociativeArray(AssociativeArray{
Map: map[string]any{
return frankenphp.PHPAssociativeArray[string](frankenphp.AssociativeArray[string]{
Map: map[string]string{
"key1": "value1",
"key2": "value2",
},
@@ -136,10 +158,13 @@ func process_data_ordered_map(arr *C.zval) unsafe.Pointer {
}
// export_php:function process_data_unordered(array $input): array
func process_data_unordered_map(arr *C.zval) unsafe.Pointer {
func process_data_unordered_map(arr *C.zend_array) unsafe.Pointer {
// Convert PHP associative array to a Go map without keeping the order
// ignoring the order will be more performant
goMap := frankenphp.GoMap(unsafe.Pointer(arr))
goMap, err := frankenphp.GoMap[any](unsafe.Pointer(arr))
if err != nil {
// handle error
}
// loop over the entries in no specific order
for key, value := range goMap {
@@ -147,16 +172,19 @@ func process_data_unordered_map(arr *C.zval) unsafe.Pointer {
}
// return an unordered array
return frankenphp.PHPMap(map[string]any{
return frankenphp.PHPMap(map[string]string {
"key1": "value1",
"key2": "value2",
})
}
// export_php:function process_data_packed(array $input): array
func process_data_packed(arr *C.zval) unsafe.Pointer {
func process_data_packed(arr *C.zend_array) unsafe.Pointer {
// Convert PHP packed array to Go
goSlice := frankenphp.GoPackedArray(unsafe.Pointer(arr), false)
goSlice, err := frankenphp.GoPackedArray(unsafe.Pointer(arr))
if err != nil {
// handle error
}
// loop over the slice in order
for index, value := range goSlice {
@@ -164,32 +192,71 @@ func process_data_packed(arr *C.zval) unsafe.Pointer {
}
// return a packed array
return frankenphp.PHPackedArray([]any{"value1", "value2", "value3"})
return frankenphp.PHPPackedArray([]string{"value1", "value2", "value3"})
}
```
**Key features of array conversion:**
* **Ordered key-value pairs** - Option to keep the order of the associative array
* **Optimized for multiple cases** - Option to ditch the order for better performance or convert straight to a slice
* **Automatic list detection** - When converting to PHP, automatically detects if array should be a packed list or hashmap
* **Nested Arrays** - Arrays can be nested and will convert all support types automatically (`int64`,`float64`,`string`,`bool`,`nil`,`AssociativeArray`,`map[string]any`,`[]any`)
* **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.
- **Ordered key-value pairs** - Option to keep the order of the associative array
- **Optimized for multiple cases** - Option to ditch the order for better performance or convert straight to a slice
- **Automatic list detection** - When converting to PHP, automatically detects if array should be a packed list or hashmap
- **Nested Arrays** - Arrays can be nested and will convert all support types automatically (`int64`,`float64`,`string`,`bool`,`nil`,`AssociativeArray`,`map[string]any`,`[]any`)
- **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.
##### Available methods: Packed and Associative
* `frankenphp.PHPAssociativeArray(arr frankenphp.AssociativeArray) unsafe.Pointer` - Convert to an ordered PHP array with key-value pairs
* `frankenphp.PHPMap(arr map[string]any) unsafe.Pointer` - Convert a map to an unordered PHP array with key-value pairs
* `frankenphp.PHPPackedArray(slice []any) unsafe.Pointer` - Convert a slice to a PHP packed array with indexed values only
* `frankenphp.GoAssociativeArray(arr unsafe.Pointer, ordered bool) frankenphp.AssociativeArray` - Convert a PHP array to an ordered Go `AssociativeArray` (map with order)
* `frankenphp.GoMap(arr unsafe.Pointer) map[string]any` - Convert a PHP array to an unordered Go map
* `frankenphp.GoPackedArray(arr unsafe.Pointer) []any` - Convert a PHP array to a Go slice
- `frankenphp.PHPAssociativeArray(arr frankenphp.AssociativeArray) unsafe.Pointer` - Convert to an ordered PHP array with key-value pairs
- `frankenphp.PHPMap(arr map[string]any) unsafe.Pointer` - Convert a map to an unordered PHP array with key-value pairs
- `frankenphp.PHPPackedArray(slice []any) unsafe.Pointer` - Convert a slice to a PHP packed array with indexed values only
- `frankenphp.GoAssociativeArray(arr unsafe.Pointer, ordered bool) frankenphp.AssociativeArray` - Convert a PHP array to an ordered Go `AssociativeArray` (map with order)
- `frankenphp.GoMap(arr unsafe.Pointer) map[string]any` - Convert a PHP array to an unordered Go map
- `frankenphp.GoPackedArray(arr unsafe.Pointer) []any` - Convert a PHP array to a Go slice
- `frankenphp.IsPacked(zval *C.zend_array) bool` - Check if a PHP array is packed (indexed only) or associative (key-value pairs)
### Working with Callables
FrankenPHP 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.
To 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:
```go
// export_php:function my_array_map(array $data, callable $callback): array
func my_array_map(arr *C.zend_array, callback *C.zval) unsafe.Pointer {
goSlice, err := frankenphp.GoPackedArray[any](unsafe.Pointer(arr))
if err != nil {
panic(err)
}
result := make([]any, len(goSlice))
for index, value := range goSlice {
result[index] = frankenphp.CallPHPCallable(unsafe.Pointer(callback), []interface{}{value})
}
return frankenphp.PHPPackedArray(result)
}
```
Notice 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:
```php
<?php
$result = my_array_map([1, 2, 3], function($x) { return $x * 2; });
// $result will be [2, 4, 6]
$result = my_array_map(['hello', 'world'], 'strtoupper');
// $result will be ['HELLO', 'WORLD']
```
### Declaring a Native PHP Class
The 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:
```go
package example
//export_php:class User
type UserStruct struct {
Name string
@@ -201,11 +268,11 @@ type UserStruct struct {
**Opaque classes** are classes where the internal structure (properties) is hidden from PHP code. This means:
* **No direct property access**: You cannot read or write properties directly from PHP (`$user->name` won't work)
* **Method-only interface** - All interactions must go through methods you define
* **Better encapsulation** - Internal data structure is completely controlled by Go code
* **Type safety** - No risk of PHP code corrupting internal state with wrong types
* **Cleaner API** - Forces to design a proper public interface
- **No direct property access**: You cannot read or write properties directly from PHP (`$user->name` won't work)
- **Method-only interface** - All interactions must go through methods you define
- **Better encapsulation** - Internal data structure is completely controlled by Go code
- **Type safety** - No risk of PHP code corrupting internal state with wrong types
- **Cleaner API** - Forces to design a proper public interface
This 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.
@@ -214,6 +281,16 @@ This approach provides better encapsulation and prevents PHP code from accidenta
Since properties are not directly accessible, you **must define methods** to interact with your opaque classes. Use the `//export_php:method` directive to define behavior:
```go
package example
// #include <Zend/zend_types.h>
import "C"
import (
"unsafe"
"github.com/dunglas/frankenphp"
)
//export_php:class User
type UserStruct struct {
Name string
@@ -246,18 +323,28 @@ func (us *UserStruct) SetNamePrefix(prefix *C.zend_string) {
The 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:
```go
package example
// #include <Zend/zend_types.h>
import "C"
import (
"unsafe"
"github.com/dunglas/frankenphp"
)
//export_php:method User::updateInfo(?string $name, ?int $age, ?bool $active): void
func (us *UserStruct) UpdateInfo(name *C.zend_string, age *int64, active *bool) {
// Check if name was provided (not null)
if name != nil {
us.Name = frankenphp.GoString(unsafe.Pointer(name))
}
// Check if age was provided (not null)
if age != nil {
us.Age = int(*age)
}
// Check if active was provided (not null)
if active != nil {
us.Active = *active
@@ -267,12 +354,13 @@ func (us *UserStruct) UpdateInfo(name *C.zend_string, age *int64, active *bool)
**Key points about nullable parameters:**
* **Nullable primitive types** (`?int`, `?float`, `?bool`) become pointers (`*int64`, `*float64`, `*bool`) in Go
* **Nullable strings** (`?string`) remain as `*C.zend_string` but can be `nil`
* **Check for `nil`** before dereferencing pointer values
* **PHP `null` becomes Go `nil`** - when PHP passes `null`, your Go function receives a `nil` pointer
- **Nullable primitive types** (`?int`, `?float`, `?bool`) become pointers (`*int64`, `*float64`, `*bool`) in Go
- **Nullable strings** (`?string`) remain as `*C.zend_string` but can be `nil`
- **Check for `nil`** before dereferencing pointer values
- **PHP `null` becomes Go `nil`** - when PHP passes `null`, your Go function receives a `nil` pointer
> [!WARNING]
>
> 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`).
After generating the extension, you will be allowed to use the class and its methods in PHP. Note that you **cannot access properties directly**:
@@ -302,13 +390,15 @@ This design ensures that your Go code has complete control over how the object's
### Declaring Constants
The generator supports exporting Go constants to PHP using two directives: `//export_php:const` for global constants and `//export_php:classconstant` for class constants. This allows you to share configuration values, status codes, and other constants between Go and PHP code.
The 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.
#### Global Constants
Use the `//export_php:const` directive to create global PHP constants:
```go
package example
//export_php:const
const MAX_CONNECTIONS = 100
@@ -324,25 +414,27 @@ const STATUS_ERROR = iota
#### Class Constants
Use the `//export_php:classconstant ClassName` directive to create constants that belong to a specific PHP class:
Use the `//export_php:classconst ClassName` directive to create constants that belong to a specific PHP class:
```go
//export_php:classconstant User
package example
//export_php:classconst User
const STATUS_ACTIVE = 1
//export_php:classconstant User
//export_php:classconst User
const STATUS_INACTIVE = 0
//export_php:classconstant User
//export_php:classconst User
const ROLE_ADMIN = "admin"
//export_php:classconstant Order
//export_php:classconst Order
const STATE_PENDING = iota
//export_php:classconstant Order
//export_php:classconst Order
const STATE_PROCESSING = iota
//export_php:classconstant Order
//export_php:classconst Order
const STATE_COMPLETED = iota
```
@@ -366,10 +458,15 @@ The directive supports various value types including strings, integers, booleans
You 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:
```go
package example
// #include <Zend/zend_types.h>
import "C"
import (
"C"
"github.com/dunglas/frankenphp"
"strings"
"strings"
"unsafe"
"github.com/dunglas/frankenphp"
)
//export_php:const
@@ -378,45 +475,45 @@ const STR_REVERSE = iota
//export_php:const
const STR_NORMAL = iota
//export_php:classconstant StringProcessor
//export_php:classconst StringProcessor
const MODE_LOWERCASE = 1
//export_php:classconstant StringProcessor
//export_php:classconst StringProcessor
const MODE_UPPERCASE = 2
//export_php:function repeat_this(string $str, int $count, int $mode): string
func repeat_this(s *C.zend_string, count int64, mode int) unsafe.Pointer {
str := frankenphp.GoString(unsafe.Pointer(s))
str := frankenphp.GoString(unsafe.Pointer(s))
result := strings.Repeat(str, int(count))
if mode == STR_REVERSE {
// reverse the string
}
result := strings.Repeat(str, int(count))
if mode == STR_REVERSE {
// reverse the string
}
if mode == STR_NORMAL {
// no-op, just to showcase the constant
}
if mode == STR_NORMAL {
// no-op, just to showcase the constant
}
return frankenphp.PHPString(result, false)
return frankenphp.PHPString(result, false)
}
//export_php:class StringProcessor
type StringProcessorStruct struct {
// internal fields
// internal fields
}
//export_php:method StringProcessor::process(string $input, int $mode): string
func (sp *StringProcessorStruct) Process(input *C.zend_string, mode int64) unsafe.Pointer {
str := frankenphp.GoString(unsafe.Pointer(input))
switch mode {
case MODE_LOWERCASE:
str = strings.ToLower(str)
case MODE_UPPERCASE:
str = strings.ToUpper(str)
}
return frankenphp.PHPString(str, false)
str := frankenphp.GoString(unsafe.Pointer(input))
switch mode {
case MODE_LOWERCASE:
str = strings.ToLower(str)
case MODE_UPPERCASE:
str = strings.ToUpper(str)
}
return frankenphp.PHPString(str, false)
}
```
@@ -430,9 +527,13 @@ Use the `//export_php:namespace` directive at the top of your Go file to place a
```go
//export_php:namespace My\Extension
package main
package example
import "C"
import (
"unsafe"
"github.com/dunglas/frankenphp"
)
//export_php:function hello(): string
func hello() string {
@@ -470,17 +571,17 @@ echo My\Extension\STATUS_ACTIVE; // 1
#### Important Notes
* Only **one** namespace directive is allowed per file. If multiple namespace directives are found, the generator will return an error.
* The namespace applies to **all** exported symbols in the file: functions, classes, methods, and constants.
* Namespace names follow PHP namespace conventions using backslashes (`\`) as separators.
* If no namespace is declared, symbols are exported to the global namespace as usual.
- Only **one** namespace directive is allowed per file. If multiple namespace directives are found, the generator will return an error.
- The namespace applies to **all** exported symbols in the file: functions, classes, methods, and constants.
- Namespace names follow PHP namespace conventions using backslashes (`\`) as separators.
- If no namespace is declared, symbols are exported to the global namespace as usual.
### Generating the Extension
This is where the magic happens, and your extension can now be generated. You can run the generator with the following command:
```console
GEN_STUB_SCRIPT=php-src/build/gen_stub.php frankenphp extension-init my_extension.go
GEN_STUB_SCRIPT=php-src/build/gen_stub.php frankenphp extension-init my_extension.go
```
> [!NOTE]
@@ -535,25 +636,26 @@ We'll see how to write a simple PHP extension in Go that defines a new native fu
In 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:
```go
package ext_go
package example
//#include "extension.h"
// #include "extension.h"
import "C"
import (
"unsafe"
"github.com/caddyserver/caddy/v2"
"github.com/dunglas/frankenphp"
"log/slog"
"unsafe"
"github.com/dunglas/frankenphp"
)
func init() {
frankenphp.RegisterExtension(unsafe.Pointer(&C.ext_module_entry))
frankenphp.RegisterExtension(unsafe.Pointer(&C.ext_module_entry))
}
//export go_print_something
func go_print_something() {
go func() {
caddy.Log().Info("Hello from a goroutine!")
}()
go func() {
slog.Info("Hello from a goroutine!")
}()
}
```
@@ -600,9 +702,9 @@ extern zend_module_entry ext_module_entry;
Next, create a file named `extension.c` that will perform the following steps:
* Include PHP headers;
* Declare our new native PHP function `go_print()`;
* Declare the extension metadata.
- Include PHP headers;
- Declare our new native PHP function `go_print()`;
- Declare the extension metadata.
Let's start by including the required headers:
@@ -729,21 +831,33 @@ There's only one thing left to do: implement the `go_upper` function in Go.
Our 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.
```go
import "strings"
package example
// #include <Zend/zend_types.h>
import "C"
import (
"unsafe"
"strings"
"github.com/dunglas/frankenphp"
)
//export go_upper
func go_upper(s *C.zend_string) *C.zend_string {
str := frankenphp.GoString(unsafe.Pointer(s))
upper := strings.ToUpper(str)
return (*C.zend_string)(frankenphp.PHPString(upper, false))
}
```
This approach is much cleaner and safer than manual memory management. FrankenPHP's helper functions handle the conversion between PHP's `zend_string` format and Go strings automatically. The `false` parameter in `PHPString()` indicates that we want to create a new non-persistent string (freed at the end of the request).
This approach is much cleaner and safer than manual memory management.
FrankenPHP's helper functions handle the conversion between PHP's `zend_string` format and Go strings automatically.
The `false` parameter in `PHPString()` indicates that we want to create a new non-persistent string (freed at the end of the request).
> [!TIP]
>
> 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.
### Integrating the Extension into FrankenPHP

View File

@@ -16,30 +16,63 @@ Découvrez plus de détails sur ce serveur dapplication dans le replay de cet
## Pour Commencer
### Binaire autonome
Sur Windows, utilisez [WSL](https://learn.microsoft.com/windows/wsl/) pour exécuter FrankenPHP.
Si vous préférez ne pas utiliser Docker, nous fournissons des binaires autonomes de FrankenPHP pour Linux et macOS
contenant [PHP 8.4](https://www.php.net/releases/8.4/fr.php) et la plupart des extensions PHP populaires.
### Script d'installation
Sous Windows, utilisez [WSL](https://learn.microsoft.com/windows/wsl/) pour exécuter FrankenPHP.
[Téléchargez FrankenPHP](https://github.com/php/frankenphp/releases) ou copiez cette ligne dans votre terminal pour installer automatiquement la version appropriée à votre plateforme :
Vous pouvez copier cette ligne dans votre terminal pour installer automatiquement
une version adaptée à votre plateforme :
```console
curl https://frankenphp.dev/install.sh | sh
mv frankenphp /usr/local/bin/
```
Pour servir le contenu du répertoire courant, exécutez :
### Binaire autonome
Nous fournissons des binaires statiques de FrankenPHP pour le développement, pour Linux et macOS,
contenant [PHP 8.4](https://www.php.net/releases/8.4/fr.php) et la plupart des extensions PHP populaires.
[Télécharger FrankenPHP](https://github.com/php/frankenphp/releases)
**Installation d'extensions :** Les extensions les plus courantes sont incluses. Il n'est pas possible d'en installer davantage.
### Paquets rpm
Nos mainteneurs proposent des paquets rpm pour tous les systèmes utilisant `dnf`. Pour installer, exécutez :
```console
frankenphp php-server
sudo dnf install https://rpm.henderkes.com/static-php-1-0.noarch.rpm
sudo dnf module enable php-zts:static-8.4 # 8.2-8.5 disponibles
sudo dnf install frankenphp
```
Vous pouvez également exécuter des scripts en ligne de commande avec :
**Installation d'extensions :** `sudo dnf install php-zts-<extension>`
Pour les extensions non disponibles par défaut, utilisez [PIE](https://github.com/php/pie) :
```console
frankenphp php-cli /path/to/your/script.php
sudo dnf install pie-zts
sudo pie-zts install asgrim/example-pie-extension
```
### Paquets deb
Nos mainteneurs proposent des paquets deb pour tous les systèmes utilisant `apt`. Pour installer, exécutez :
```console
sudo curl -fsSL https://key.henderkes.com/static-php.gpg -o /usr/share/keyrings/static-php.gpg && \
echo "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 && \
sudo apt update
sudo apt install frankenphp
```
**Installation d'extensions :** `sudo apt install php-zts-<extension>`
Pour les extensions non disponibles par défaut, utilisez [PIE](https://github.com/php/pie) :
```console
sudo apt install pie-zts
sudo pie-zts install asgrim/example-pie-extension
```
### Docker
@@ -69,12 +102,28 @@ Pour l'installer :
brew install dunglas/frankenphp/frankenphp
```
**Installation d'extensions :** Utilisez [PIE](https://github.com/php/pie).
### Utilisation
Pour servir le contenu du répertoire courant, exécutez :
```console
frankenphp php-server
```
Vous pouvez également exécuter des scripts en ligne de commande avec :
```console
frankenphp php-cli /path/to/your/script.php
```
Pour les paquets deb et rpm, vous pouvez aussi démarrer le service systemd :
```console
sudo systemctl start frankenphp
```
## Documentation
- [Le mode classique](classic.md)

View File

@@ -82,7 +82,7 @@ Certaines fonctionnalités de FrankenPHP nécessitent des dépendances optionnel
Ces fonctionnalités peuvent également être désactivées en passant des tags de compilation au compilateur Go.
| Fonctionnalité | Dépendance | Tag de compilation pour la désactiver |
|---------------------------------------------------------|-----------------------------------------------------------------------|---------------------------------------|
| ------------------------------------------------------- | --------------------------------------------------------------------- | ------------------------------------- |
| Compression Brotli | [Brotli](https://github.com/google/brotli) | nobrotli |
| Redémarrage des workers en cas de changement de fichier | [Watcher C](https://github.com/e-dant/watcher/tree/release/watcher-c) | nowatcher |

View File

@@ -1,4 +1,4 @@
# Configuration
# Configuration
FrankenPHP, Caddy ainsi que les modules Mercure et Vulcain peuvent être configurés en utilisant [les formats pris en charge par Caddy](https://caddyserver.com/docs/getting-started#your-first-config).

View File

@@ -2,7 +2,7 @@
Les 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.
Des variantes pour PHP 8.2, 8.3 et 8.4 sont disponibles. [Parcourir les tags](https://hub.docker.com/r/dunglas/frankenphp/tags).
Des 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).
Les tags suivent le pattern suivant: `dunglas/frankenphp:<frankenphp-version>-php<php-version>-<os>`

View File

@@ -56,13 +56,13 @@ La manière la plus simple de créer un binaire Linux est d'utiliser le builder
1. Créez un fichier nommé `static-build.Dockerfile` dans le répertoire de votre application préparée :
```dockerfile
FROM --platform=linux/amd64 dunglas/frankenphp:static-builder
FROM --platform=linux/amd64 dunglas/frankenphp:static-builder-gnu
# Si vous envisagez d'exécuter le binaire sur des systèmes musl-libc, utilisez plutôt static-builder-musl
# Copy your app
WORKDIR /go/src/app/dist/app
COPY . .
# Build the static binary, be sure to select only the PHP extensions you want
WORKDIR /go/src/app/
RUN EMBED=dist/app/ ./build-static.sh
```

View File

@@ -47,10 +47,15 @@ tar xf php-*
Tout 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 :
```go
package example
// #include <Zend/zend_types.h>
import "C"
import (
"C"
"github.com/dunglas/frankenphp"
"strings"
"unsafe"
"github.com/dunglas/frankenphp"
)
//export_php:function repeat_this(string $str, int $count, bool $reverse): string
@@ -72,26 +77,29 @@ func repeat_this(s *C.zend_string, count int64, reverse bool) unsafe.Pointer {
Il y a deux choses importantes à noter ici :
* 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 ;
* La fonction doit retourner un `unsafe.Pointer`. FrankenPHP fournit une API pour vous aider avec le jonglage de types entre C et Go.
- 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 ;
- La fonction doit retourner un `unsafe.Pointer`. FrankenPHP fournit une API pour vous aider avec le jonglage de types entre C et Go.
Alors que le premier point parle de lui-même, le second peut être plus difficile à appréhender. Plongeons plus profondément dans la jonglage de types dans la section suivante.
Alors 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.
### Jonglage de Types
Bien 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 :
| Type PHP | Type Go | Conversion directe | Assistant C vers Go | Assistant Go vers C | Support des Méthodes de Classe |
|--------------------|---------------------|--------------------|-------------------------|-------------------------|--------------------------------|
| `int` | `int64` | ✅ | - | - | ✅ |
| `?int` | `*int64` | ✅ | - | - | ✅ |
| `float` | `float64` | ✅ | - | - | ✅ |
| `?float` | `*float64` | ✅ | - | - | ✅ |
| `bool` | `bool` | ✅ | - | - | ✅ |
| `?bool` | `*bool` | ✅ | - | - | ✅ |
| `string`/`?string` | `*C.zend_string` | ❌ | frankenphp.GoString() | frankenphp.PHPString() | ✅ |
| `array` | `*frankenphp.Array` | ❌ | frankenphp.GoArray() | frankenphp.PHPArray() | ✅ |
| `object` | `struct` | ❌ | _Pas encore implémenté_ | _Pas encore implémenté_ | ❌ |
| Type PHP | Type Go | Conversion directe | Assistant C vers Go | Assistant Go vers C | Support des Méthodes de Classe |
| ------------------ | ----------------------------- | ------------------ | --------------------------------- | ---------------------------------- | ------------------------------ |
| `int` | `int64` | ✅ | - | - | ✅ |
| `?int` | `*int64` | ✅ | - | - | ✅ |
| `float` | `float64` | ✅ | - | - | ✅ |
| `?float` | `*float64` | ✅ | - | - | ✅ |
| `bool` | `bool` | ✅ | - | - | ✅ |
| `?bool` | `*bool` | ✅ | - | - | ✅ |
| `string`/`?string` | `*C.zend_string` | ❌ | frankenphp.GoString() | frankenphp.PHPString() | ✅ |
| `array` | `frankenphp.AssociativeArray` | ❌ | `frankenphp.GoAssociativeArray()` | `frankenphp.PHPAssociativeArray()` | ✅ |
| `array` | `map[string]any` | ❌ | `frankenphp.GoMap()` | `frankenphp.PHPMap()` | ✅ |
| `array` | `[]any` | ❌ | `frankenphp.GoPackedArray()` | `frankenphp.PHPPackedArray()` | ✅ |
| `mixed` | `any` | ❌ | `GoValue()` | `PHPValue()` | ❌ |
| `object` | `struct` | ❌ | _Pas encore implémenté_ | _Pas encore implémenté_ | ❌ |
> [!NOTE]
> 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.
@@ -102,61 +110,150 @@ Si vous vous référez à l'extrait de code de la section précédente, vous pou
#### Travailler avec les Tableaux
FrankenPHP fournit un support natif pour les tableaux PHP à travers le type `frankenphp.Array`. Ce type représente à la fois les tableaux indexés PHP (listes) et les tableaux associatifs (hashmaps) avec des paires clé-valeur ordonnées.
FrankenPHP fournit un support natif pour les tableaux PHP à travers `frankenphp.AssociativeArray` ou une conversion directe vers une map ou un slice.
`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).
Si 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`.
**Créer et manipuler des tableaux en Go :**
```go
//export_php:function process_data(array $input): array
func process_data(arr *C.zval) unsafe.Pointer {
// Convertir le tableau PHP vers Go
goArray := frankenphp.GoArray(unsafe.Pointer(arr))
result := &frankenphp.Array{}
result.SetInt(0, "first")
result.SetInt(1, "second")
result.Append("third") // Assigne automatiquement la prochaine clé entière
result.SetString("name", "John")
result.SetString("age", int64(30))
for i := uint32(0); i < goArray.Len(); i++ {
key, value := goArray.At(i)
if key.Type == frankenphp.PHPStringKey {
result.SetString("processed_"+key.Str, value)
} else {
result.SetInt(key.Int+100, value)
}
package example
// #include <Zend/zend_types.h>
import "C"
import (
"unsafe"
"github.com/dunglas/frankenphp"
)
// export_php:function process_data_ordered(array $input): array
func process_data_ordered_map(arr *C.zval) unsafe.Pointer {
// Convertir le tableau associatif PHP vers Go en conservant l'ordre
associativeArray, err := frankenphp.GoAssociativeArray[any](unsafe.Pointer(arr))
if err != nil {
// gérer l'erreur
}
// Reconvertir vers un tableau PHP
return frankenphp.PHPArray(result)
// parcourir les entrées dans l'ordre
for _, key := range associativeArray.Order {
value, _ = associativeArray.Map[key]
// faire quelque chose avec key et value
}
// retourner un tableau ordonné
// si 'Order' n'est pas vide, seules les paires clé-valeur dans 'Order' seront respectées
return frankenphp.PHPAssociativeArray[string](frankenphp.AssociativeArray[string]{
Map: map[string]string{
"key1": "value1",
"key2": "value2",
},
Order: []string{"key1", "key2"},
})
}
// export_php:function process_data_unordered(array $input): array
func process_data_unordered_map(arr *C.zval) unsafe.Pointer {
// Convertir le tableau associatif PHP vers une map Go sans conserver l'ordre
// ignorer l'ordre sera plus performant
goMap, err := frankenphp.GoMap[any](unsafe.Pointer(arr))
if err != nil {
// gérer l'erreur
}
// parcourir les entrées sans ordre spécifique
for key, value := range goMap {
// faire quelque chose avec key et value
}
// retourner un tableau non ordonné
return frankenphp.PHPMap(map[string]string {
"key1": "value1",
"key2": "value2",
})
}
// export_php:function process_data_packed(array $input): array
func process_data_packed(arr *C.zval) unsafe.Pointer {
// Convertir le tableau packed PHP vers Go
goSlice, err := frankenphp.GoPackedArray(unsafe.Pointer(arr))
if err != nil {
// gérer l'erreur
}
// parcourir le slice dans l'ordre
for index, value := range goSlice {
// faire quelque chose avec index et value
}
// retourner un tableau packed
return frankenphp.PHPPackedArray([]string{"value1", "value2", "value3"})
}
```
**Fonctionnalités clés de `frankenphp.Array` :**
**Fonctionnalités clés de la conversion de tableaux :**
* **Paires clé-valeur ordonnées** - Maintient l'ordre d'insertion comme les tableaux PHP
* **Types de clés mixtes** - Supporte les clés entières et chaînes dans le même tableau
* **Sécurité de type** - Le type `PHPKey` assure une gestion appropriée des clés
* **Détection automatique de liste** - Lors de la conversion vers PHP, détecte automatiquement si le tableau doit être une liste compacte ou un hashmap
* **Les objets ne sont pas supportés** - Actuellement, seuls les types scalaires et les tableaux sont supportés. Passer un objet en tant qu'élément du tableau résultera d'une valeur `null` dans le tableau PHP.
- **Paires clé-valeur ordonnées** - Option pour conserver l'ordre du tableau associatif
- **Optimisé pour plusieurs cas** - Option de ne pas conserver l'ordre pour de meilleures performances ou conversion directe vers un slice
- **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
- **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`)
- **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.
**Méthodes disponibles :**
##### Méthodes disponibles : Packed et Associatif
* `SetInt(key int64, value interface{})` - Définir une valeur avec une clé entière
* `SetString(key string, value interface{})` - Définir une valeur avec une clé chaîne
* `Append(value interface{})` - Ajouter une valeur avec la prochaine clé entière disponible
* `Len() uint32` - Obtenir le nombre d'éléments
* `At(index uint32) (PHPKey, interface{})` - Obtenir la paire clé-valeur à l'index
* `frankenphp.PHPArray(arr *frankenphp.Array) unsafe.Pointer` - Convertir vers un tableau PHP
- `frankenphp.PHPAssociativeArray(arr frankenphp.AssociativeArray) unsafe.Pointer` - Convertir vers un tableau PHP ordonné avec des paires clé-valeur
- `frankenphp.PHPMap(arr map[string]any) unsafe.Pointer` - Convertir une map vers un tableau PHP non ordonné avec des paires clé-valeur
- `frankenphp.PHPPackedArray(slice []any) unsafe.Pointer` - Convertir un slice vers un tableau PHP packed avec uniquement des valeurs indexées
- `frankenphp.GoAssociativeArray(arr unsafe.Pointer, ordered bool) frankenphp.AssociativeArray` - Convertir un tableau PHP vers un `AssociativeArray` Go ordonné (map avec ordre)
- `frankenphp.GoMap(arr unsafe.Pointer) map[string]any` - Convertir un tableau PHP vers une map Go non ordonnée
- `frankenphp.GoPackedArray(arr unsafe.Pointer) []any` - Convertir un tableau PHP vers un slice Go
- `frankenphp.IsPacked(zval *C.zend_array) bool` - Vérifie si le tableau PHP est une liste ou un tableau associatif
### Travailler avec des Callables
FrankenPHP 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.
Pour 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 :
```go
// export_php:function my_array_map(array $data, callable $callback): array
func my_array_map(arr *C.zend_array, callback *C.zval) unsafe.Pointer {
goSlice, err := frankenphp.GoPackedArray[any](unsafe.Pointer(arr))
if err != nil {
panic(err)
}
result := make([]any, len(goSlice))
for index, value := range goSlice {
result[index] = frankenphp.CallPHPCallable(unsafe.Pointer(callback), []interface{}{value})
}
return frankenphp.PHPPackedArray(result)
}
```
Remarquez 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_ :
```php
<?php
$result = my_array_map([1, 2, 3], function($x) { return $x * 2; });
// $result vaudra [2, 4, 6]
$result = my_array_map(['hello', 'world'], 'strtoupper');
// $result vaudra ['HELLO', 'WORLD']
```
### Déclarer une Classe PHP Native
Le 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 :
```go
package example
//export_php:class User
type UserStruct struct {
Name string
@@ -168,11 +265,11 @@ type UserStruct struct {
Les **classes opaques** sont des classes avec lesquelles la structure interne (comprendre : les propriétés) est cachée du code PHP. Cela signifie :
* **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)
* **Interface uniquement par méthodes** - Toutes les interactions doivent passer par les méthodes que vous définissez
* **Meilleure encapsulation** - La structure de données interne est complètement contrôlée par le code Go
* **Sécurité de type** - Aucun risque que le code PHP corrompe l'état interne avec de mauvais types
* **API plus propre** - Force à concevoir une interface publique appropriée
- **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)
- **Interface uniquement par méthodes** - Toutes les interactions doivent passer par les méthodes que vous définissez
- **Meilleure encapsulation** - La structure de données interne est complètement contrôlée par le code Go
- **Sécurité de type** - Aucun risque que le code PHP corrompe l'état interne avec de mauvais types
- **API plus propre** - Force à concevoir une interface publique appropriée
Cette 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.
@@ -181,6 +278,16 @@ Cette approche fournit une meilleure encapsulation et empêche le code PHP de co
Puisque 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 :
```go
package example
// #include <Zend/zend_types.h>
import "C"
import (
"unsafe"
"github.com/dunglas/frankenphp"
)
//export_php:class User
type UserStruct struct {
Name string
@@ -213,19 +320,29 @@ func (us *UserStruct) SetNamePrefix(prefix *C.zend_string) {
Le 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 :
```go
package example
// #include <Zend/zend_types.h>
import "C"
import (
"unsafe"
"github.com/dunglas/frankenphp"
)
//export_php:method User::updateInfo(?string $name, ?int $age, ?bool $active): void
func (us *UserStruct) UpdateInfo(name *C.zend_string, age *int64, active *bool) {
// $name est null?
// Vérifier si name a été fourni (pas null)
if name != nil {
us.Name = frankenphp.GoString(unsafe.Pointer(name))
}
// $age est null?
// Vérifier si age a été fourni (pas null)
if age != nil {
us.Age = int(*age)
}
// $active est null?
// Vérifier si active a été fourni (pas null)
if active != nil {
us.Active = *active
}
@@ -234,10 +351,10 @@ func (us *UserStruct) UpdateInfo(name *C.zend_string, age *int64, active *bool)
**Points clés sur les paramètres nullables :**
* **Types primitifs nullables** (`?int`, `?float`, `?bool`) deviennent des pointeurs (`*int64`, `*float64`, `*bool`) en Go
* **Chaînes nullables** (`?string`) restent comme `*C.zend_string` mais peuvent être `nil`
* **Vérifiez `nil`** avant de déréférencer les valeurs de pointeur
* **PHP `null` devient Go `nil`** - quand PHP passe `null`, votre fonction Go reçoit un pointeur `nil`
- **Types primitifs nullables** (`?int`, `?float`, `?bool`) deviennent des pointeurs (`*int64`, `*float64`, `*bool`) en Go
- **Chaînes nullables** (`?string`) restent comme `*C.zend_string` mais peuvent être `nil`
- **Vérifiez `nil`** avant de déréférencer les valeurs de pointeur
- **PHP `null` devient Go `nil`** - quand PHP passe `null`, votre fonction Go reçoit un pointeur `nil`
> [!WARNING]
> 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`).
@@ -269,13 +386,15 @@ Cette conception garantit que votre code Go a un contrôle complet sur la façon
### Déclarer des Constantes
Le 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:classconstant` 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.
Le 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.
#### Constantes Globales
Utilisez la directive `//export_php:const` pour créer des constantes PHP globales :
```go
package example
//export_php:const
const MAX_CONNECTIONS = 100
@@ -291,25 +410,27 @@ const STATUS_ERROR = iota
#### Constantes de Classe
Utilisez la directive `//export_php:classconstant ClassName` pour créer des constantes qui appartiennent à une classe PHP spécifique :
Utilisez la directive `//export_php:classconst ClassName` pour créer des constantes qui appartiennent à une classe PHP spécifique :
```go
//export_php:classconstant User
package example
//export_php:classconst User
const STATUS_ACTIVE = 1
//export_php:classconstant User
//export_php:classconst User
const STATUS_INACTIVE = 0
//export_php:classconstant User
//export_php:classconst User
const ROLE_ADMIN = "admin"
//export_php:classconstant Order
//export_php:classconst Order
const STATE_PENDING = iota
//export_php:classconstant Order
//export_php:classconst Order
const STATE_PROCESSING = iota
//export_php:classconstant Order
//export_php:classconst Order
const STATE_COMPLETED = iota
```
@@ -333,10 +454,15 @@ La directive prend en charge divers types de valeurs incluant les chaînes, enti
Vous 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 :
```go
package example
// #include <Zend/zend_types.h>
import "C"
import (
"C"
"github.com/dunglas/frankenphp"
"strings"
"unsafe"
"github.com/dunglas/frankenphp"
)
//export_php:const
@@ -345,10 +471,10 @@ const STR_REVERSE = iota
//export_php:const
const STR_NORMAL = iota
//export_php:classconstant StringProcessor
//export_php:classconst StringProcessor
const MODE_LOWERCASE = 1
//export_php:classconstant StringProcessor
//export_php:classconst StringProcessor
const MODE_UPPERCASE = 2
//export_php:function repeat_this(string $str, int $count, int $mode): string
@@ -356,7 +482,7 @@ func repeat_this(s *C.zend_string, count int64, mode int) unsafe.Pointer {
str := frankenphp.GoString(unsafe.Pointer(s))
result := strings.Repeat(str, int(count))
if mode == STR_REVERSE {
if mode == STR_REVERSE {
// inverser la chaîne
}
@@ -375,14 +501,14 @@ type StringProcessorStruct struct {
//export_php:method StringProcessor::process(string $input, int $mode): string
func (sp *StringProcessorStruct) Process(input *C.zend_string, mode int64) unsafe.Pointer {
str := frankenphp.GoString(unsafe.Pointer(input))
switch mode {
case MODE_LOWERCASE:
str = strings.ToLower(str)
case MODE_UPPERCASE:
str = strings.ToUpper(str)
}
return frankenphp.PHPString(str, false)
}
```
@@ -397,9 +523,13 @@ Utilisez la directive `//export_php:namespace` en haut de votre fichier Go pour
```go
//export_php:namespace My\Extension
package main
package example
import "C"
import (
"unsafe"
"github.com/dunglas/frankenphp"
)
//export_php:function hello(): string
func hello() string {
@@ -437,17 +567,17 @@ echo My\Extension\STATUS_ACTIVE; // 1
#### Notes Importantes
* 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.
* L'espace de noms s'applique à **tous** les symboles exportés dans le fichier : fonctions, classes, méthodes et constantes.
* Les noms d'espaces de noms suivent les conventions des espaces de noms PHP en utilisant les barres obliques inverses (`\`) comme séparateurs.
* Si aucun espace de noms n'est déclaré, les symboles sont exportés vers l'espace de noms global comme d'habitude.
- 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.
- L'espace de noms s'applique à **tous** les symboles exportés dans le fichier : fonctions, classes, méthodes et constantes.
- Les noms d'espaces de noms suivent les conventions des espaces de noms PHP en utilisant les barres obliques inverses (`\`) comme séparateurs.
- Si aucun espace de noms n'est déclaré, les symboles sont exportés vers l'espace de noms global comme d'habitude.
### Générer l'Extension
C'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 :
```console
GEN_STUB_SCRIPT=php-src/build/gen_stub.php frankenphp extension-init my_extension.go
GEN_STUB_SCRIPT=php-src/build/gen_stub.php frankenphp extension-init my_extension.go
```
> [!NOTE]
@@ -499,16 +629,17 @@ Nous allons voir comment écrire une extension PHP simple en Go qui définit une
#### Définir la Fonction Go
Dans votre module Go vide, 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 :
Dans 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 :
```go
package ext_go
package example
//#include "extension.h"
// #include "extension.h"
import "C"
import (
"log/slog"
"unsafe"
"github.com/caddyserver/caddy/v2"
"github.com/dunglas/frankenphp"
)
@@ -519,7 +650,7 @@ func init() {
//export go_print_something
func go_print_something() {
go func() {
caddy.Log().Info("Hello from a goroutine!")
slog.Info("Hello from a goroutine!")
}()
}
```
@@ -567,9 +698,9 @@ extern zend_module_entry ext_module_entry;
Ensuite, créez un fichier nommé `extension.c` qui effectuera les étapes suivantes :
* Inclure les en-têtes PHP ;
* Déclarer notre nouvelle fonction PHP native `go_print()` ;
* Déclarer les métadonnées de l'extension.
- Inclure les en-têtes PHP ;
- Déclarer notre nouvelle fonction PHP native `go_print()` ;
- Déclarer les métadonnées de l'extension.
Commençons par inclure les en-têtes requis :
@@ -696,14 +827,23 @@ Il ne reste qu'une chose à faire : implémenter la fonction `go_upper` en Go.
Notre 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.
```go
import "strings"
package example
// #include <Zend/zend_types.h>
import "C"
import (
"unsafe"
"strings"
"github.com/dunglas/frankenphp"
)
//export go_upper
func go_upper(s *C.zend_string) *C.zend_string {
str := frankenphp.GoString(unsafe.Pointer(s))
upper := strings.ToUpper(str)
return (*C.zend_string)(frankenphp.PHPString(upper, false))
}
```

View File

@@ -88,7 +88,8 @@ Suivez ces étapes pour empaqueter votre application Laravel en tant que binaire
1. Créez un fichier nommé `static-build.Dockerfile` dans le dépôt de votre application :
```dockerfile
FROM --platform=linux/amd64 dunglas/frankenphp:static-builder
FROM --platform=linux/amd64 dunglas/frankenphp:static-builder-gnu
# Si vous avez l'intention d'exécuter le binaire sur des systèmes musl-libc, utilisez plutôt static-builder-musl
# Copiez votre application
WORKDIR /go/src/app/dist/app

View File

@@ -37,6 +37,9 @@ frankenphp php-server --worker /path/to/your/worker/script.php --watch="/path/to
## Runtime Symfony
> [!TIP]
> La section suivante est nécessaire uniquement avant Symfony 7.4, où le support natif du mode worker de FrankenPHP a été introduit.
Le mode worker de FrankenPHP est pris en charge par le [Composant Runtime de Symfony](https://symfony.com/doc/current/components/runtime.html).
Pour démarrer une application Symfony dans un worker, installez le package FrankenPHP de [PHP Runtime](https://github.com/php-runtime/runtime) :

View File

@@ -5,7 +5,7 @@ mais parfois, il est nécessaire d'exécuter du code PHP avant de les envoyer :
contrôle d'accès, statistiques, en-têtes HTTP personnalisés...
Malheureusement, utiliser PHP pour servir de gros fichiers statiques est inefficace comparé à
à l'utilisation directe du serveur web (surcharge mémoire, diminution des performances...).
l'utilisation directe du serveur web (surcharge mémoire, diminution des performances...).
FrankenPHP permet de déléguer l'envoi des fichiers statiques au serveur web
**après** avoir exécuté du code PHP personnalisé.

View File

@@ -16,31 +16,62 @@ FrankenPHPはあらゆるPHPアプリと連携し、ワーカーモードの公
## はじめに
### スタンドアロンバイナリ
LinuxとmacOS向けに、[PHP 8.4](https://www.php.net/releases/8.4/en.php)と人気のPHP拡張モジュールを含む静的な
FrankenPHPバイナリを提供しています。
Windowsをお使いの場合は、[WSL](https://learn.microsoft.com/windows/wsl/)を使用してFrankenPHPを実行してください。
[FrankenPHPをダウンロード](https://github.com/php/frankenphp/releases)するか、以下のコマンドを
ターミナルにコピーして実行すると、環境に合ったバージョンが自動的にインストールされます:
### インストールスクリプト
以下のコマンドをターミナルに貼り付けると、環境に合ったバージョンが自動的にインストールされます:
```console
curl https://frankenphp.dev/install.sh | sh
mv frankenphp /usr/local/bin/
```
現在のディレクトリのコンテンツを配信するには、以下を実行してください:
### スタンドアロンバイナリ
LinuxとmacOS向けに、開発用途の静的FrankenPHPバイナリを提供しています。
[PHP 8.4](https://www.php.net/releases/8.4/en.php)と主要なPHP拡張が含まれます。
[FrankenPHPをダウンロード](https://github.com/php/frankenphp/releases)
**拡張のインストール:** よく使われる拡張は同梱されています。追加の拡張をインストールすることはできません。
### rpm パッケージ
メンテナーが `dnf` を使用するすべてのシステム向けに rpm パッケージを提供しています。インストール方法:
```console
frankenphp php-server
sudo dnf install https://rpm.henderkes.com/static-php-1-0.noarch.rpm
sudo dnf module enable php-zts:static-8.4 # 8.2-8.5 利用可能
sudo dnf install frankenphp
```
コマンドラインスクリプトも実行できます:
**拡張のインストール:** `sudo dnf install php-zts-<extension>`
デフォルトで提供されていない拡張については [PIE](https://github.com/php/pie) を使用してください:
```console
frankenphp php-cli /path/to/your/script.php
sudo dnf install pie-zts
sudo pie-zts install asgrim/example-pie-extension
```
### deb パッケージ
メンテナーが `apt` を使用するすべてのシステム向けに deb パッケージを提供しています。インストール方法:
```console
sudo curl -fsSL https://key.henderkes.com/static-php.gpg -o /usr/share/keyrings/static-php.gpg && \
echo "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 && \
sudo apt update
sudo apt install frankenphp
```
**拡張のインストール:** `sudo apt install php-zts-<extension>`
デフォルトで提供されていない拡張については [PIE](https://github.com/php/pie) を使用してください:
```console
sudo apt install pie-zts
sudo pie-zts install asgrim/example-pie-extension
```
### Docker
@@ -70,12 +101,28 @@ FrankenPHPはmacOSおよびLinux向けに[Homebrew](https://brew.sh)パッケー
brew install dunglas/frankenphp/frankenphp
```
**拡張のインストール:** [PIE](https://github.com/php/pie) を使用してください。
### 使い方
現在のディレクトリのコンテンツを配信するには、以下を実行してください:
```console
frankenphp php-server
```
コマンドラインスクリプトも実行できます:
```console
frankenphp php-cli /path/to/your/script.php
```
deb / rpm パッケージの場合は、systemd サービスを起動することもできます:
```console
sudo systemctl start frankenphp
```
## ドキュメント
- [クラシックモード](https://frankenphp.dev/docs/classic/)

View File

@@ -79,10 +79,10 @@ sudo make install
FrankenPHPの一部の機能は、システムにインストールされているオプションの依存パッケージに依存しています。
または、Goコンパイラにビルドタグを渡すことで、これらの機能を無効にできます。
| 機能 | 依存関係 | 無効にするためのビルドタグ |
|--------------------------------|-----------------------------------------------------------------------|-------------------------|
| Brotli圧縮 | [Brotli](https://github.com/google/brotli) | nobrotli |
| ファイル変更時のワーカー再起動 | [Watcher C](https://github.com/e-dant/watcher/tree/release/watcher-c) | nowatcher |
| 機能 | 依存関係 | 無効にするためのビルドタグ |
| ------------------------------ | --------------------------------------------------------------------- | -------------------------- |
| Brotli圧縮 | [Brotli](https://github.com/google/brotli) | nobrotli |
| ファイル変更時のワーカー再起動 | [Watcher C](https://github.com/e-dant/watcher/tree/release/watcher-c) | nowatcher |
## Goアプリのコンパイル

View File

@@ -2,7 +2,7 @@
[FrankenPHPのDockerイメージ](https://hub.docker.com/r/dunglas/frankenphp)は、[公式PHPイメージ](https://hub.docker.com/_/php/)をベースにしています。主要なアーキテクチャに対してDebianとAlpine Linuxのバリアントを提供しており、Debianバリアントの使用を推奨しています。
PHP 8.2、8.3、8.4向けのバリアントが提供されています。
PHP 8.2、8.3、8.4、8.5向けのバリアントが提供されています。
タグは次のパターンに従います:`dunglas/frankenphp:<frankenphp-version>-php<php-version>-<os>`

View File

@@ -54,7 +54,8 @@ Linux用バイナリを作成する最も簡単な方法は、提供されてい
1. アプリのリポジトリに`static-build.Dockerfile`というファイルを作成します:
```dockerfile
FROM --platform=linux/amd64 dunglas/frankenphp:static-builder
FROM --platform=linux/amd64 dunglas/frankenphp:static-builder-gnu
# バイナリをmusl-libcシステムで実行する場合は、static-builder-musl を使用してください
# アプリをコピー
WORKDIR /go/src/app/dist/app

View File

@@ -72,8 +72,8 @@ func repeat_this(s *C.zend_string, count int64, reverse bool) unsafe.Pointer {
ここで重要なポイントが2つあります
* ディレクティブコメント`//export_php:function`はPHPでの関数シグネチャを定義します。これにより、ジェネレーターは適切なパラメータと戻り値の型でPHP関数を生成する方法を知ることができます。
* 関数は`unsafe.Pointer`を返さなければなりません。FrankenPHPはCとGo間の型変換を支援するAPIを提供しています。
- ディレクティブコメント`//export_php:function`はPHPでの関数シグネチャを定義します。これにより、ジェネレーターは適切なパラメータと戻り値の型でPHP関数を生成する方法を知ることができます。
- 関数は`unsafe.Pointer`を返さなければなりません。FrankenPHPはCとGo間の型変換を支援するAPIを提供しています。
前者は理解しやすいですが、後者は少し複雑かもしれません。次のセクションで型変換について詳しく説明します。
@@ -81,17 +81,18 @@ func repeat_this(s *C.zend_string, count int64, reverse bool) unsafe.Pointer {
C/PHPとGoの間でメモリ表現が同じ変数型もありますが、直接使用するにはより多くのロジックが必要な型もあります。これは拡張モジュールを書く際の最も挑戦的な部分かもしれません。Zendエンジンの内部仕組みや、変数がPHP内でどのように格納されているかを理解する必要があるためです。以下の表は、知っておくべき重要な情報をまとめています
| PHP型 | Go型 | 直接変換 | CからGoヘルパー | GoからCヘルパー | クラスメソッドサポート |
|--------------------|------------------|-------------------|-----------------------|------------------------|-----------------------|
| `int` | `int64` | ✅ | - | - | ✅ |
| `?int` | `*int64` | ✅ | - | - | ✅ |
| `float` | `float64` | ✅ | - | - | ✅ |
| `?float` | `*float64` | ✅ | - | - | ✅ |
| `bool` | `bool` | ✅ | - | - | ✅ |
| `?bool` | `*bool` | ✅ | - | - | ✅ |
| `string`/`?string` | `*C.zend_string` | ❌ | frankenphp.GoString() | frankenphp.PHPString() | ✅ |
| `array` | `slice`/`map` | ❌ | _未実装_ | _未実装_ | ❌ |
| `object` | `struct` | ❌ | _未実装_ | _未実装_ | ❌ |
| PHP型 | Go型 | 直接変換 | CからGoヘルパー | GoからCヘルパー | クラスメソッドサポート |
| ------------------ | ---------------- | -------- | --------------------- | ---------------------- | ---------------------- |
| `int` | `int64` | ✅ | - | - | ✅ |
| `?int` | `*int64` | ✅ | - | - | ✅ |
| `float` | `float64` | ✅ | - | - | ✅ |
| `?float` | `*float64` | ✅ | - | - | ✅ |
| `bool` | `bool` | ✅ | - | - | ✅ |
| `?bool` | `*bool` | ✅ | - | - | ✅ |
| `string`/`?string` | `*C.zend_string` | ❌ | frankenphp.GoString() | frankenphp.PHPString() | ✅ |
| `array` | `slice`/`map` | ❌ | _未実装_ | _未実装_ | ❌ |
| `mixed` | `any` | ❌ | `GoValue()` | `PHPValue()` | ❌ |
| `object` | `struct` | ❌ | _未実装_ | _未実装_ | ❌ |
> [!NOTE]
> この表はまだ完全ではなく、FrankenPHPの型APIがより完全になるにつれて完成されます。
@@ -116,11 +117,11 @@ type UserStruct struct {
**不透明クラスopaque classes**は、内部構造プロパティがPHPコードから隠されているクラスです。これは以下を意味します
* **プロパティへの直接アクセス不可** PHPから直接プロパティを読み書きできません`$user->name`は機能しません)
* **メソッド経由のみで操作** - すべてのやりとりはGoで定義したメソッドを通じて行う必要があります
* **より良いカプセル化** - 内部データ構造は完全にGoコードによって制御されます
* **型安全性** - PHP側から誤った型で内部状態が破壊されるリスクがありません
* **よりクリーンなAPI** - 適切な公開インターフェースを設計することを強制します
- **プロパティへの直接アクセス不可** PHPから直接プロパティを読み書きできません`$user->name`は機能しません)
- **メソッド経由のみで操作** - すべてのやりとりはGoで定義したメソッドを通じて行う必要があります
- **より良いカプセル化** - 内部データ構造は完全にGoコードによって制御されます
- **型安全性** - PHP側から誤った型で内部状態が破壊されるリスクがありません
- **よりクリーンなAPI** - 適切な公開インターフェースを設計することを強制します
このアプローチは優れたカプセル化を実現し、PHPコードがGoオブジェクトの内部状態を意図せずに破壊してしまうことを防ぎます。オブジェクトとのすべてのやりとりは、明示的に定義したメソッドを通じて行う必要があります。
@@ -167,12 +168,12 @@ func (us *UserStruct) UpdateInfo(name *C.zend_string, age *int64, active *bool)
if name != nil {
us.Name = frankenphp.GoString(unsafe.Pointer(name))
}
// ageが渡されたnullではないかチェック
if age != nil {
us.Age = int(*age)
}
// activeが渡されたnullではないかチェック
if active != nil {
us.Active = *active
@@ -182,10 +183,10 @@ func (us *UserStruct) UpdateInfo(name *C.zend_string, age *int64, active *bool)
**Nullableパラメータの重要なポイント**
* **プリミティブ型のnullable** (`?int`, `?float`, `?bool`) はGoではそれぞれポインタ (`*int64`, `*float64`, `*bool`) になります
* **nullable文字列** (`?string`) は `*C.zend_string` のままですが、`nil` になることがあります
* ポインタ値を逆参照する前に **`nil`をチェック** してください
* **PHPの`null`はGoの`nil`になります** - PHPが`null`を渡すと、Go関数は`nil`ポインタを受け取ります
- **プリミティブ型のnullable** (`?int`, `?float`, `?bool`) はGoではそれぞれポインタ (`*int64`, `*float64`, `*bool`) になります
- **nullable文字列** (`?string`) は `*C.zend_string` のままですが、`nil` になることがあります
- ポインタ値を逆参照する前に **`nil`をチェック** してください
- **PHPの`null`はGoの`nil`になります** - PHPが`null`を渡すと、Go関数は`nil`ポインタを受け取ります
> [!WARNING]
> 現在、クラスメソッドには次の制限があります。**配列とオブジェクトはパラメータ型や戻り値の型としてサポートされていません**。サポートされるのは`string`、`int`、`float`、`bool`、`void`(戻り値の型)といったスカラー型のみです。**nullableなスカラー型はすべてサポートされています** `?string`、`?int`、`?float`、`?bool`)。
@@ -217,7 +218,7 @@ $user->updateInfo(null, 25, null); // Nameとactiveがnull
### 定数の宣言
ジェネレーターは、2つのディレクティブを使用してGo定数をPHPにエクスポートすることをサポートしていますグローバル定数用の`//export_php:const`とクラス定数用の`//export_php:classconstant`です。これにより、GoとPHPコード間で設定値、ステータスコード、その他の定数を共有できます。
ジェネレーターは、2つのディレクティブを使用してGo定数をPHPにエクスポートすることをサポートしていますグローバル定数用の`//export_php:const`とクラス定数用の`//export_php:classconst`です。これにより、GoとPHPコード間で設定値、ステータスコード、その他の定数を共有できます。
#### グローバル定数
@@ -239,25 +240,25 @@ const STATUS_ERROR = iota
#### クラス定数
`//export_php:classconstant ClassName`ディレクティブを使用して、特定のPHPクラスに属する定数を作成できます
`//export_php:classconst ClassName`ディレクティブを使用して、特定のPHPクラスに属する定数を作成できます
```go
//export_php:classconstant User
//export_php:classconst User
const STATUS_ACTIVE = 1
//export_php:classconstant User
//export_php:classconst User
const STATUS_INACTIVE = 0
//export_php:classconstant User
//export_php:classconst User
const ROLE_ADMIN = "admin"
//export_php:classconstant Order
//export_php:classconst Order
const STATE_PENDING = iota
//export_php:classconstant Order
//export_php:classconst Order
const STATE_PROCESSING = iota
//export_php:classconstant Order
//export_php:classconst Order
const STATE_COMPLETED = iota
```
@@ -293,10 +294,10 @@ const STR_REVERSE = iota
//export_php:const
const STR_NORMAL = iota
//export_php:classconstant StringProcessor
//export_php:classconst StringProcessor
const MODE_LOWERCASE = 1
//export_php:classconstant StringProcessor
//export_php:classconst StringProcessor
const MODE_UPPERCASE = 2
//export_php:function repeat_this(string $str, int $count, int $mode): string
@@ -304,7 +305,7 @@ func repeat_this(s *C.zend_string, count int64, mode int) unsafe.Pointer {
str := frankenphp.GoString(unsafe.Pointer(s))
result := strings.Repeat(str, int(count))
if mode == STR_REVERSE {
if mode == STR_REVERSE {
// 文字列を逆転
}
@@ -323,14 +324,14 @@ type StringProcessorStruct struct {
//export_php:method StringProcessor::process(string $input, int $mode): string
func (sp *StringProcessorStruct) Process(input *C.zend_string, mode int64) unsafe.Pointer {
str := frankenphp.GoString(unsafe.Pointer(input))
switch mode {
case MODE_LOWERCASE:
str = strings.ToLower(str)
case MODE_UPPERCASE:
str = strings.ToUpper(str)
}
return frankenphp.PHPString(str, false)
}
```
@@ -340,11 +341,10 @@ func (sp *StringProcessorStruct) Process(input *C.zend_string, mode int64) unsaf
ここでいよいよ、拡張モジュールを生成できるようになります。以下のコマンドでジェネレーターを実行できます:
```console
GEN_STUB_SCRIPT=php-src/build/gen_stub.php frankenphp extension-init my_extension.go
GEN_STUB_SCRIPT=php-src/build/gen_stub.php frankenphp extension-init my_extension.go
```
> [!NOTE]
> `GEN_STUB_SCRIPT`環境変数に、先ほどダウンロードしたPHPソースの`gen_stub.php`ファイルのパスを設定するのを忘れないでください。これは手動実装セクションで言及されているのと同じ`gen_stub.php`スクリプトです。
> [!NOTE] > `GEN_STUB_SCRIPT`環境変数に、先ほどダウンロードしたPHPソースの`gen_stub.php`ファイルのパスを設定するのを忘れないでください。これは手動実装セクションで言及されているのと同じ`gen_stub.php`スクリプトです。
すべてがうまくいけば、`build`という名前の新しいディレクトリが作成されているはずです。このディレクトリには、生成されたPHP関数スタブを含む`my_extension.go`ファイルなど、拡張用の生成されたファイルが含まれています。
@@ -460,9 +460,9 @@ extern zend_module_entry ext_module_entry;
次に、以下のステップを実行する`extension.c`という名前のファイルを作成します:
* PHPヘッダーをインクルードする
* 新しいネイティブPHP関数`go_print()`を宣言する
* 拡張モジュールのメタデータを宣言する
- PHPヘッダーをインクルードする
- 新しいネイティブPHP関数`go_print()`を宣言する
- 拡張モジュールのメタデータを宣言する
まずは必要なヘッダーのインクルードから始めましょう:
@@ -594,9 +594,9 @@ import "strings"
//export go_upper
func go_upper(s *C.zend_string) *C.zend_string {
str := frankenphp.GoString(unsafe.Pointer(s))
upper := strings.ToUpper(str)
return (*C.zend_string)(frankenphp.PHPString(upper, false))
}
```

View File

@@ -4,8 +4,8 @@
以下の拡張モジュールはFrankenPHPと互換性がないことが確認されています
| 名前 | 理由 | 代替手段 |
| ----------------------------------------------------------------------------------------------------------- | --------------- | -------------------------------------------------------------------------------------------------------------------- |
| 名前 | 理由 | 代替手段 |
| ----------------------------------------------------------------------------------------------------------- | -------------------- | -------------------------------------------------------------------------------------------------------------------- |
| [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) |
| [newrelic](https://docs.newrelic.com/docs/apm/agents/php-agent/getting-started/introduction-new-relic-php/) | スレッドセーフでない | - |
@@ -13,8 +13,8 @@
以下の拡張モジュールはFrankenPHPとの組み合わせで既知のバグや予期しない動作が確認されています
| 名前 | 問題 |
| ------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 名前 | 問題 |
| ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| [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)です。 |
## get_browser

View File

@@ -88,7 +88,8 @@ LaravelアプリをLinux用のスタンドアロンバイナリとしてパッ
1. アプリのリポジトリに`static-build.Dockerfile`という名前のファイルを作成します:
```dockerfile
FROM --platform=linux/amd64 dunglas/frankenphp:static-builder
FROM --platform=linux/amd64 dunglas/frankenphp:static-builder-gnu
# バイナリをmusl-libcシステムで実行する場合は、static-builder-musl を使用してください
# アプリをコピー
WORKDIR /go/src/app/dist/app

View File

@@ -13,9 +13,9 @@ The following extensions are known not to be compatible with FrankenPHP:
The following extensions have known bugs and unexpected behaviors when used with FrankenPHP:
| Name | Problem |
| ------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [ext-openssl](https://www.php.net/manual/en/book.openssl.php) | When using a static build of FrankenPHP (built with the musl libc), the OpenSSL extension may crash under heavy loads. A workaround is to use a dynamically linked build (like the one used in Docker images). This bug is [being tracked by PHP](https://github.com/php/php-src/issues/13648). |
| Name | Problem |
| ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [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). |
## get_browser
@@ -23,7 +23,11 @@ The [get_browser()](https://www.php.net/manual/en/function.get-browser.php) func
## Standalone Binary and Alpine-based Docker Images
The standalone 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. This may lead to some compatibility issues. In particular, the glob flag `GLOB_BRACE` is [not available](https://www.php.net/manual/en/function.glob.php)
The 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.
This may lead to some compatibility issues.
In particular, the glob flag `GLOB_BRACE` is [not available](https://www.php.net/manual/en/function.glob.php)
Prefer using the GNU variant of the static binary and Debian-based Docker images if you encounter issues.
## Using `https://127.0.0.1` with Docker

View File

@@ -76,6 +76,8 @@ The `octane:frankenphp` command can take the following options:
> [!TIP]
> To get structured JSON logs (useful when using log analytics solutions), explicitly the pass `--log-level` option.
See also [how to use Mercure with Octane](#mercure-support).
Learn more about [Laravel Octane in its official documentation](https://laravel.com/docs/octane).
## Laravel Apps As Standalone Binaries
@@ -88,7 +90,8 @@ Follow these steps to package your Laravel app as a standalone binary for Linux:
1. Create a file named `static-build.Dockerfile` in the repository of your app:
```dockerfile
FROM --platform=linux/amd64 dunglas/frankenphp:static-builder
FROM --platform=linux/amd64 dunglas/frankenphp:static-builder-gnu
# If you intend to run the binary on musl-libc systems, use static-builder-musl instead
# Copy your app
WORKDIR /go/src/app/dist/app
@@ -166,6 +169,34 @@ This is not suitable for embedded applications, as each new version will be extr
Set 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.
### Mercure Support
[Mercure](https://mercure.rocks) is a great way to add real-time capabilities to your Laravel apps.
FrankenPHP includes [Mercure support out of the box](mercure.md).
If you are not using [Octane](#laravel-octane), see [the Mercure documentation entry](mercure.md).
If you are using Octane, you can use enable Mercure support by adding the following lines to your `config/octane.php` file:
```php
// ...
return [
// ...
'mercure' => [
'anonymous' => true,
'publisher_jwt' => '!ChangeThisMercureHubJWTSecretKey!',
'subscriber_jwt' => '!ChangeThisMercureHubJWTSecretKey!',
],
];
```
You can use [all directives supported by Mercure](https://mercure.rocks/docs/hub/config#directives) in this array.
To publish and subscribe to updates, we recommend using the [Laravel Mercure Broadcaster](https://github.com/mvanduijker/laravel-mercure-broadcaster) library.
Alternatively, see [the Mercure documentation](mercure.md) to do it in pure PHP and JavaScript.
### Running Octane With Standalone Binaries
It's even possible to package Laravel Octane apps as standalone binaries!

View File

@@ -3,13 +3,147 @@
FrankenPHP comes with a built-in [Mercure](https://mercure.rocks) hub!
Mercure allows you to push real-time events to all the connected devices: they will receive a JavaScript event instantly.
No JS library or SDK is required!
It's a convenient alternative to WebSockets that is simple to use and is natively supported by all modern web browsers!
![Mercure](mercure-hub.png)
To enable the Mercure hub, update the `Caddyfile` as described [on Mercure's site](https://mercure.rocks/docs/hub/config).
## Enabling Mercure
The path of the Mercure hub is `/.well-known/mercure`.
When running FrankenPHP inside Docker, the full send URL would look like `http://php/.well-known/mercure` (with `php` being the container's name running FrankenPHP).
Mercure support is disabled by default.
Here is a minimal example of a `Caddyfile` enabling both FrankenPHP and the Mercure hub:
To push Mercure updates from your code, we recommend the [Symfony Mercure Component](https://symfony.com/components/Mercure) (you don't need the Symfony full-stack framework to use it).
```caddyfile
# The hostname to respond to
localhost
mercure {
# The secret key used to sign the JWT tokens for publishers
publisher_jwt !ChangeThisMercureHubJWTSecretKey!
# Allows anonymous subscribers (without JWT)
anonymous
}
root public/
php_server
```
> [!TIP]
>
> The [sample `Caddyfile`](https://github.com/php/frankenphp/blob/main/caddy/frankenphp/Caddyfile)
> provided by [the Docker images](docker.md) already includes a commented Mercure configuration
> with convenient environment variables to configure it.
>
> Uncomment the Mercure section in `/etc/frankenphp/Caddyfile` to enable it.
## Subscribing to Updates
By default, the Mercure hub is available on the `/.well-known/mercure` path of your FrankenPHP server.
To subscribe to updates, use the native [`EventSource`](https://developer.mozilla.org/docs/Web/API/EventSource) JavaScript class:
```html
<!-- public/index.html -->
<!doctype html>
<title>Mercure Example</title>
<script>
const eventSource = new EventSource("/.well-known/mercure?topic=my-topic");
eventSource.onmessage = function (event) {
console.log("New message:", event.data);
};
</script>
```
## Publishing Updates
### Using `mercure_publish()`
FrankenPHP provides a convenient `mercure_publish()` function to publish updates to the built-in Mercure hub:
```php
<?php
// public/publish.php
$updateID = mercure_publish('my-topic', json_encode(['key' => 'value']));
// Write to FrankenPHP's logs
error_log("update $updateID published", 4);
```
The full function signature is:
```php
/**
* @param string|string[] $topics
*/
function mercure_publish(string|array $topics, string $data = '', bool $private = false, ?string $id = null, ?string $type = null, ?int $retry = null): string {}
```
### Using `file_get_contents()`
To dispatch an update to connected subscribers, send an authenticated POST request to the Mercure hub with the `topic` and `data` parameters:
```php
<?php
// public/publish.php
const JWT = 'eyJhbGciOiJIUzI1NiJ9.eyJtZXJjdXJlIjp7InB1Ymxpc2giOlsiKiJdfX0.PXwpfIGng6KObfZlcOXvcnWCJOWTFLtswGI5DZuWSK4';
$updateID = file_get_contents('https://localhost/.well-known/mercure', context: stream_context_create(['http' => [
'method' => 'POST',
'header' => "Content-type: application/x-www-form-urlencoded\r\nAuthorization: Bearer " . JWT,
'content' => http_build_query([
'topic' => 'my-topic',
'data' => json_encode(['key' => 'value']),
]),
]]));
// Write to FrankenPHP's logs
error_log("update $updateID published", 4);
```
The 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.
The JWT must include a `mercure` claim with a `publish` permission for the topics you want to publish to.
See [the Mercure documentation](https://mercure.rocks/spec#publishers) about authorization.
To generate your own tokens, you can use [this jwt.io link](https://www.jwt.io/#token=eyJhbGciOiJIUzI1NiJ9.eyJtZXJjdXJlIjp7InB1Ymxpc2giOlsiKiJdfX0.PXwpfIGng6KObfZlcOXvcnWCJOWTFLtswGI5DZuWSK4),
but 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).
### Using Symfony Mercure
Alternatively, you can use the [Symfony Mercure Component](https://symfony.com/components/Mercure), a standalone PHP library.
This library handled the JWT generation, update publishing as well as cookie-based authorization for subscribers.
First, install the library using Composer:
```console
composer require symfony/mercure lcobucci/jwt
```
Then, you can use it like this:
```php
<?php
// public/publish.php
require __DIR__ . '/../vendor/autoload.php';
const JWT_SECRET = '!ChangeThisMercureHubJWTSecretKey!'; // Must be the same as mercure.publisher_jwt in Caddyfile
// Set up the JWT token provider
$jwFactory = new \Symfony\Component\Mercure\Jwt\LcobucciFactory(JWT_SECRET);
$provider = new \Symfony\Component\Mercure\Jwt\FactoryTokenProvider($jwFactory, publish: ['*']);
$hub = new \Symfony\Component\Mercure\Hub('https://localhost/.well-known/mercure', $provider);
// Serialize the update, and dispatch it to the hub, that will broadcast it to the clients
$updateID = $hub->publish(new \Symfony\Component\Mercure\Update('my-topic', json_encode(['key' => 'value'])));
// Write to FrankenPHP's logs
error_log("update $updateID published", 4);
```
Mercure is also natively supported by:
- [Laravel](laravel.md#mercure-support)
- [Symfony](https://symfony.com/doc/current/mercure.html)
- [API Platform](https://api-platform.com/docs/core/mercure/)

View File

@@ -41,11 +41,9 @@ especially when compiled in ZTS mode (thread-safe), which is required for Franke
Also, [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).
In production environments, we recommend using FrankenPHP linked against glibc.
In production environments, we recommend using FrankenPHP linked against glibc, compiled with an appropriate optimization level.
This can be achieved by using the Debian Docker images (the default), downloading the -gnu suffix binary from our [Releases](https://github.com/php/frankenphp/releases), or by [compiling FrankenPHP from sources](compile.md).
Alternatively, we provide static musl binaries compiled with [the mimalloc allocator](https://github.com/microsoft/mimalloc), which alleviates the problems in threaded scenarios.
This can be achieved by using the Debian Docker images, using our maintainers [.deb](https://debs.henderkes.com) or [.rpm](https://rpms.henderkes.com) packages, or by [compiling FrankenPHP from sources](compile.md).
## Go Runtime Configuration

View File

@@ -24,22 +24,64 @@ conjunto de slides:
## Começando
### Binário independente
Fornecemos binários estáticos do FrankenPHP para Linux e macOS contendo o
[PHP 8.4](https://www.php.net/releases/8.4/pt_BR.php) e as extensões PHP mais
populares.
No Windows, use [WSL](https://learn.microsoft.com/pt-br/windows/wsl/) para
executar o FrankenPHP.
[Baixe o FrankenPHP](https://github.com/php/frankenphp/releases) ou copie esta
linha no seu terminal para instalar automaticamente a versão apropriada para sua
plataforma:
### Script de instalação
Você pode copiar esta linha no seu terminal para instalar automaticamente a
versão apropriada para sua plataforma:
```console
curl https://frankenphp.dev/install.sh | sh
mv frankenphp /usr/local/bin/
```
### Binário independente
Fornecemos binários estáticos do FrankenPHP para desenvolvimento em Linux e macOS contendo o
[PHP 8.4](https://www.php.net/releases/8.4/pt_BR.php) e as extensões PHP mais populares.
[Baixe o FrankenPHP](https://github.com/php/frankenphp/releases)
**Instalação de extensões:** As extensões mais comuns já estão incluídas. Não é possível instalar mais extensões.
### Pacotes rpm
Nossos mantenedores oferecem pacotes rpm para todos os sistemas que usam `dnf`. Para instalar, execute:
```console
sudo dnf install https://rpm.henderkes.com/static-php-1-0.noarch.rpm
sudo dnf module enable php-zts:static-8.4 # 8.2-8.5 disponíveis
sudo dnf install frankenphp
```
**Instalação de extensões:** `sudo dnf install php-zts-<extension>`
Para extensões não disponíveis por padrão, use o [PIE](https://github.com/php/pie):
```console
sudo dnf install pie-zts
sudo pie-zts install asgrim/example-pie-extension
```
### Pacotes deb
Nossos mantenedores oferecem pacotes deb para todos os sistemas que usam `apt`. Para instalar, execute:
```console
sudo curl -fsSL https://key.henderkes.com/static-php.gpg -o /usr/share/keyrings/static-php.gpg && \
echo "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 && \
sudo apt update
sudo apt install frankenphp
```
**Instalação de extensões:** `sudo apt install php-zts-<extension>`
Para extensões não disponíveis por padrão, use o [PIE](https://github.com/php/pie):
```console
sudo apt install pie-zts
sudo pie-zts install asgrim/example-pie-extension
```
Para servir o conteúdo do diretório atual, execute:
@@ -85,12 +127,28 @@ Para instalá-lo:
brew install dunglas/frankenphp/frankenphp
```
**Instalação de extensões:** Use o [PIE](https://github.com/php/pie).
### Uso
Para servir o conteúdo do diretório atual, execute:
```console
frankenphp php-server
```
Você também pode executar scripts de linha de comando com:
```console
frankenphp php-cli /caminho/para/seu/script.php
```
Para os pacotes deb e rpm, você também pode iniciar o serviço systemd:
```console
sudo systemctl start frankenphp
```
## Documentação
- [Modo clássico](classic.md)

View File

@@ -92,7 +92,7 @@ Alternativamente, esses recursos podem ser desabilitados passando as tags de
compilação para o compilador Go.
| Recurso | Dependência | Tag de compilação para desabilitá-lo |
|---------------------------------------|-----------------------------------------------------------------------|--------------------------------------|
| ------------------------------------- | --------------------------------------------------------------------- | ------------------------------------ |
| Compressão Brotli | [Brotli](https://github.com/google/brotli) | `nobrotli` |
| Reiniciar workers ao alterar arquivos | [Watcher C](https://github.com/e-dant/watcher/tree/release/watcher-c) | `nowatcher` |
@@ -126,8 +126,8 @@ xcaddy build \
> Se você estiver usando a `libc` `musl` (o padrão no Alpine Linux) e Symfony,
> pode ser necessário aumentar o tamanho da pilha padrão.
> Caso contrário, você poderá receber erros como `PHP Fatal error: Maximum call
> stack size of 83360 bytes reached during compilation.
> Try splitting expression`.
stack size of 83360 bytes reached during compilation.
Try splitting expression`.
>
> Para fazer isso, altere a variável de ambiente `XCADDY_GO_BUILD_FLAGS` para
> algo como

View File

@@ -6,7 +6,7 @@ Variantes do Debian e do Alpine Linux são fornecidas para arquiteturas
populares.
Variantes do Debian são recomendadas.
Variantes para PHP 8.2, 8.3 e 8.4 são fornecidas.
Variantes para PHP 8.2, 8.3, 8.4 e 8.5 são fornecidas.
As tags seguem este padrão:
`dunglas/frankenphp:<versao-do-frankenphp>-php<versao-do-php>-<so>`.

View File

@@ -66,7 +66,8 @@ Docker que fornecemos.
aplicação:
```dockerfile
FROM --platform=linux/amd64 dunglas/frankenphp:static-builder
FROM --platform=linux/amd64 dunglas/frankenphp:static-builder-gnu
# Se você pretende executar o binário em sistemas musl-libc, use o static-builder-musl
# Copia sua aplicação
WORKDIR /go/src/app/dist/app

View File

@@ -132,19 +132,20 @@ variáveis são armazenadas internamente no PHP.
Esta tabela resume o que você precisa saber:
| Tipo PHP | Tipo Go | Conversão direta | Auxiliar de C para Go | Auxiliar de Go para C | Suporte a métodos de classe |
|--------------------|-------------------------------|------------------|-----------------------------------|------------------------------------|-----------------------------|
| `int` | `int64` | ✅ | - | - | ✅ |
| `?int` | `*int64` | ✅ | - | - | ✅ |
| `float` | `float64` | ✅ | - | - | ✅ |
| `?float` | `*float64` | ✅ | - | - | ✅ |
| `bool` | `bool` | ✅ | - | - | ✅ |
| `?bool` | `*bool` | ✅ | - | - | ✅ |
| `?bool` | `*bool` | ✅ | - | - | ✅ |
| `string`/`?string` | `*C.zend_string` | ❌ | `frankenphp.GoString()` | `frankenphp.PHPString()` | ✅ |
| `array` | `frankenphp.AssociativeArray` | ❌ | `frankenphp.GoAssociativeArray()` | `frankenphp.PHPAssociativeArray()` | ✅ |
| `array` | `map[string]any` | ❌ | `frankenphp.GoMap()` | `frankenphp.PHPMap()` | ✅ |
| `array` | `[]any` | ❌ | `frankenphp.GoPackedArray()` | `frankenphp.PHPPackedArray()` | ✅ |
| `object` | `struct` | ❌ | _Ainda não implementado_ | _Ainda não implementado_ | ❌ |
| ------------------ | ----------------------------- | ---------------- | --------------------------------- | ---------------------------------- | --------------------------- |
| `int` | `int64` | ✅ | - | - | ✅ |
| `?int` | `*int64` | ✅ | - | - | ✅ |
| `float` | `float64` | ✅ | - | - | ✅ |
| `?float` | `*float64` | ✅ | - | - | ✅ |
| `bool` | `bool` | ✅ | - | - | ✅ |
| `?bool` | `*bool` | ✅ | - | - | ✅ |
| `?bool` | `*bool` | ✅ | - | - | ✅ |
| `string`/`?string` | `*C.zend_string` | ❌ | `frankenphp.GoString()` | `frankenphp.PHPString()` | ✅ |
| `array` | `frankenphp.AssociativeArray` | ❌ | `frankenphp.GoAssociativeArray()` | `frankenphp.PHPAssociativeArray()` | ✅ |
| `array` | `map[string]any` | ❌ | `frankenphp.GoMap()` | `frankenphp.PHPMap()` | ✅ |
| `array` | `[]any` | ❌ | `frankenphp.GoPackedArray()` | `frankenphp.PHPPackedArray()` | ✅ |
| `mixed` | `any` | ❌ | `GoValue()` | `PHPValue()` | ❌ |
| `object` | `struct` | ❌ | _Ainda não implementado_ | _Ainda não implementado_ | ❌ |
> [!NOTE]
> Esta tabela ainda não é exaustiva e será completada à medida que a API de
@@ -411,7 +412,7 @@ segurança de tipos.
### Declarando constantes
O gerador suporta a exportação de constantes Go para PHP usando duas diretivas:
`//export_php:const` para constantes globais e `//export_php:classconstant` para
`//export_php:const` para constantes globais e `//export_php:classconst` para
constantes de classe.
Isso permite que você compartilhe valores de configuração, códigos de status e
outras constantes entre código Go e PHP.
@@ -436,26 +437,26 @@ const STATUS_ERROR = iota
#### Constantes de classe
Use a diretiva `//export_php:classconstant ClassName` para criar constantes que
Use a diretiva `//export_php:classconst ClassName` para criar constantes que
pertencem a uma classe PHP específica:
```go
//export_php:classconstant User
//export_php:classconst User
const STATUS_ACTIVE = 1
//export_php:classconstant User
//export_php:classconst User
const STATUS_INACTIVE = 0
//export_php:classconstant User
//export_php:classconst User
const ROLE_ADMIN = "admin"
//export_php:classconstant Order
//export_php:classconst Order
const STATE_PENDING = iota
//export_php:classconstant Order
//export_php:classconst Order
const STATE_PROCESSING = iota
//export_php:classconstant Order
//export_php:classconst Order
const STATE_COMPLETED = iota
```
@@ -501,10 +502,10 @@ const STR_REVERSE = iota
//export_php:const
const STR_NORMAL = iota
//export_php:classconstant StringProcessor
//export_php:classconst StringProcessor
const MODE_LOWERCASE = 1
//export_php:classconstant StringProcessor
//export_php:classconst StringProcessor
const MODE_UPPERCASE = 2
//export_php:function repeat_this(string $str, int $count, int $mode): string

View File

@@ -6,7 +6,7 @@ As seguintes extensões são conhecidas por não serem compatíveis com o
FrankenPHP:
| Nome | Motivo | Alternativas |
|-------------------------------------------------------------------------------------------------------------|-------------------|----------------------------------------------------------------------------------------------------------------------|
| ----------------------------------------------------------------------------------------------------------- | ----------------- | -------------------------------------------------------------------------------------------------------------------- |
| [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) |
| [newrelic](https://docs.newrelic.com/docs/apm/agents/php-agent/getting-started/introduction-new-relic-php/) | Não é thread-safe | - |
@@ -16,7 +16,7 @@ As seguintes extensões apresentam falhas conhecidas e comportamentos inesperado
quando usadas com o FrankenPHP:
| Nome | Problema |
|------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| ---------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [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) |
## `get_browser`

View File

@@ -108,7 +108,8 @@ independente para Linux:
aplicação:
```dockerfile
FROM --platform=linux/amd64 dunglas/frankenphp:static-builder
FROM --platform=linux/amd64 dunglas/frankenphp:static-builder-gnu
# Se você pretende executar o binário em sistemas musl-libc, use o static-builder-musl
# Copia sua aplicação
WORKDIR /go/src/app/dist/app

View File

@@ -16,6 +16,63 @@ FrankenPHP также может использоваться как автон
## Начало работы
В Windows используйте [WSL](https://learn.microsoft.com/windows/wsl/) для запуска FrankenPHP.
### Скрипт установки
Скопируйте и выполните эту команду в терминале, чтобы автоматически установить подходящую версию для вашей платформы:
```console
curl https://frankenphp.dev/install.sh | sh
```
### Автономный бинарный файл
Если вы предпочитаете не использовать Docker, мы предоставляем автономные статические бинарные файлы FrankenPHP для Linux и macOS, включающие [PHP 8.4](https://www.php.net/releases/8.4/en.php) и большинство популярных PHPрасширений.
[Скачать FrankenPHP](https://github.com/php/frankenphp/releases)
**Установка расширений:** Наиболее распространенные расширения уже включены. Устанавливать дополнительные расширения невозможно.
### Пакеты rpm
Наши мейнтейнеры предлагают rpmпакеты для всех систем с `dnf`. Для установки выполните:
```console
sudo dnf install https://rpm.henderkes.com/static-php-1-0.noarch.rpm
sudo dnf module enable php-zts:static-8.4 # доступны 8.28.5
sudo dnf install frankenphp
```
**Установка расширений:** `sudo dnf install php-zts-<extension>`
Для расширений, недоступных по умолчанию, используйте [PIE](https://github.com/php/pie):
```console
sudo dnf install pie-zts
sudo pie-zts install asgrim/example-pie-extension
```
### Пакеты deb
Наши мейнтейнеры предлагают debпакеты для всех систем с `apt`. Для установки выполните:
```console
sudo curl -fsSL https://key.henderkes.com/static-php.gpg -o /usr/share/keyrings/static-php.gpg && \
echo "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 && \
sudo apt update
sudo apt install frankenphp
```
**Установка расширений:** `sudo apt install php-zts-<extension>`
Для расширений, недоступных по умолчанию, используйте [PIE](https://github.com/php/pie):
```console
sudo apt install pie-zts
sudo pie-zts install asgrim/example-pie-extension
```
### Docker
```console
@@ -31,31 +88,36 @@ docker run -v .:/app/public \
> Не используйте `https://127.0.0.1`. Используйте `https://localhost` и настройте самоподписанный сертификат.
> Чтобы изменить используемый домен, настройте переменную окружения [`SERVER_NAME`](config.md#переменные-окружения).
### Автономный бинарный файл
### Homebrew
Если вы предпочитаете не использовать Docker, мы предоставляем автономный бинарный файл FrankenPHP для Linux и macOS, включающий [PHP 8.4](https://www.php.net/releases/8.4/en.php) и большинство популярных PHP-расширений.
Для Windows используйте [WSL](https://learn.microsoft.com/windows/wsl/) для запуска FrankenPHP.
[Скачать FrankenPHP](https://github.com/php/frankenphp/releases) или выполните следующую команду для автоматической установки подходящей версии:
FrankenPHP также доступен как пакет [Homebrew](https://brew.sh) для macOS и Linux.
```console
curl https://frankenphp.dev/install.sh | sh
mv frankenphp /usr/local/bin/
brew install dunglas/frankenphp/frankenphp
```
Для запуска содержимого текущей директории выполните:
**Установка расширений:** Используйте [PIE](https://github.com/php/pie).
### Использование
Для запуска содержимого текущего каталога выполните:
```console
frankenphp php-server
```
Вы также можете запускать CLI-скрипты:
Также можно запускать CLIскрипты:
```console
frankenphp php-cli /path/to/your/script.php
```
Для пакетов deb и rpm можно запустить сервис systemd:
```console
sudo systemctl start frankenphp
```
## Документация
- [Worker режим](https://frankenphp.dev/docs/worker/)

View File

@@ -2,7 +2,7 @@
[Docker-образы FrankenPHP](https://hub.docker.com/r/dunglas/frankenphp) основаны на [официальных PHP-образах](https://hub.docker.com/_/php/). Доступны варианты для Debian и Alpine Linux для популярных архитектур. Рекомендуется использовать Debian-варианты.
Доступны версии для PHP 8.2, 8.3 и 8.4.
Доступны версии для PHP 8.2, 8.3, 8.4 и 8.5.
Теги следуют следующему шаблону: `dunglas/frankenphp:<frankenphp-version>-php<php-version>-<os>`.

View File

@@ -52,7 +52,8 @@ composer dump-env prod
1. Создайте файл `static-build.Dockerfile` в репозитории вашего приложения:
```dockerfile
FROM --platform=linux/amd64 dunglas/frankenphp:static-builder
FROM --platform=linux/amd64 dunglas/frankenphp:static-builder-gnu
# Если вы планируете запускать бинарный файл на системах с musl-libc, используйте static-builder-musl
# Скопировать приложение
WORKDIR /go/src/app/dist/app

View File

@@ -85,7 +85,8 @@ php artisan octane:frankenphp
1. Создайте файл с именем `static-build.Dockerfile` в репозитории вашего приложения:
```dockerfile
FROM --platform=linux/amd64 dunglas/frankenphp:static-builder
FROM --platform=linux/amd64 dunglas/frankenphp:static-builder-gnu
# Если вы планируете запускать бинарный файл на системах с musl-libc, используйте static-builder-musl
# Скопируйте ваше приложение
WORKDIR /go/src/app/dist/app

View File

@@ -16,6 +16,64 @@ FrankenPHP, PHP'yi `net/http` kullanarak herhangi bir uygulamaya yerleştirmek i
## Başlarken
Windows üzerinde FrankenPHP çalıştırmak için [WSL](https://learn.microsoft.com/windows/wsl/) kullanın.
### Kurulum Betiği
Platformunuza uygun sürümü otomatik olarak kurmak için bu satırı terminalinize kopyalayabilirsiniz:
```console
curl https://frankenphp.dev/install.sh | sh
```
### Binary Çıktısı
Docker kullanmayı tercih etmiyorsanız, Linux ve macOS için geliştirme amaçlı bağımsız (statik) FrankenPHP binary dosyaları sağlıyoruz;
[PHP 8.4](https://www.php.net/releases/8.4/en.php) ve en popüler PHP eklentilerinin çoğu dahildir.
[FrankenPHP'yi indirin](https://github.com/php/frankenphp/releases)
**Eklenti kurulumu:** Yaygın eklentiler paketle birlikte gelir. Daha fazla eklenti yüklemek mümkün değildir.
### rpm Paketleri
Bakımcılarımız `dnf` kullanan tüm sistemler için rpm paketleri sunuyor. Kurulum için:
```console
sudo dnf install https://rpm.henderkes.com/static-php-1-0.noarch.rpm
sudo dnf module enable php-zts:static-8.4 # 8.2-8.5 mevcut
sudo dnf install frankenphp
```
**Eklenti kurulumu:** `sudo dnf install php-zts-<extension>`
Varsayılan olarak mevcut olmayan eklentiler için [PIE](https://github.com/php/pie) kullanın:
```console
sudo dnf install pie-zts
sudo pie-zts install asgrim/example-pie-extension
```
### deb Paketleri
Bakımcılarımız `apt` kullanan tüm sistemler için deb paketleri sunuyor. Kurulum için:
```console
sudo curl -fsSL https://key.henderkes.com/static-php.gpg -o /usr/share/keyrings/static-php.gpg && \
echo "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 && \
sudo apt update
sudo apt install frankenphp
```
**Eklenti kurulumu:** `sudo apt install php-zts-<extension>`
Varsayılan olarak mevcut olmayan eklentiler için [PIE](https://github.com/php/pie) kullanın:
```console
sudo apt install pie-zts
sudo pie-zts install asgrim/example-pie-extension
```
### Docker
```console
@@ -31,23 +89,36 @@ docker run -v $PWD:/app/public \
> `https://127.0.0.1` kullanmaya çalışmayın. `https://localhost` kullanın ve kendinden imzalı sertifikayı kabul edin.
> 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.
### Binary Çıktısı
### Homebrew
Docker kullanmayı tercih etmiyorsanız, Linux ve macOS için bağımsız FrankenPHP binary dosyası sağlıyoruz
[PHP 8.4](https://www.php.net/releases/8.4/en.php) ve en popüler PHP eklentilerini de içermekte: [FrankenPHP](https://github.com/php/frankenphp/releases) indirin
FrankenPHP, macOS ve Linux için [Homebrew](https://brew.sh) paketi olarak da mevcuttur.
Geçerli dizinin içeriğini başlatmak için çalıştırın:
```console
brew install dunglas/frankenphp/frankenphp
```
**Eklenti kurulumu:** [PIE](https://github.com/php/pie) kullanın.
### Kullanım
Geçerli dizinin içeriğini sunmak için çalıştırın:
```console
frankenphp php-server
```
Ayrıca aşağıdaki tek komut satırı ile de çalıştırabilirsiniz:
Komut satırı betiklerini şu şekilde çalıştırabilirsiniz:
```console
frankenphp php-cli /path/to/your/script.php
```
deb ve rpm paketleri için systemd servisini de başlatabilirsiniz:
```console
sudo systemctl start frankenphp
```
## Docs
- [Worker modu](worker.md)

View File

@@ -2,7 +2,7 @@
[Resmi PHP imajları](https://hub.docker.com/_/php/) temel alınarak [FrankenPHP Docker imajları](https://hub.docker.com/r/dunglas/frankenphp) hazırlanmıştır. Popüler mimariler için Debian ve Alpine Linux varyantları sağlanmıştır. Debian dağıtımı tavsiye edilir.
PHP 8.2, 8.3 ve 8.4 için varyantlar sağlanmıştır. [Etiketlere göz atın](https://hub.docker.com/r/dunglas/frankenphp/tags).
PHP 8.2, 8.3, 8.4 ve 8.5 için varyantlar sağlanmıştır. [Etiketlere göz atın](https://hub.docker.com/r/dunglas/frankenphp/tags).
## İmajlar Nasıl Kullanılır

View File

@@ -46,7 +46,8 @@ Bir Linux binary çıktısı almanın en kolay yolu, sağladığımız Docker ta
1. Hazırladığınız uygulamanın deposunda `static-build.Dockerfile` adlı bir dosya oluşturun:
```dockerfile
FROM --platform=linux/amd64 dunglas/frankenphp:static-builder
FROM --platform=linux/amd64 dunglas/frankenphp:static-builder-gnu
# İkili dosyayı musl-libc sistemlerinde çalıştırmayı düşünüyorsanız static-builder-musl kullanın
# Uygulamanızı kopyalayın
WORKDIR /go/src/app/dist/app
@@ -55,7 +56,6 @@ Bir Linux binary çıktısı almanın en kolay yolu, sağladığımız Docker ta
# Statik binary dosyasını oluşturun, yalnızca istediğiniz PHP eklentilerini seçtiğinizden emin olun
WORKDIR /go/src/app/
RUN EMBED=dist/app/ \
PHP_EXTENSIONS=ctype,iconv,pdo_sqlite \
./build-static.sh
```
@@ -86,7 +86,6 @@ Docker kullanmak istemiyorsanız veya bir macOS binary dosyası oluşturmak isti
git clone https://github.com/php/frankenphp
cd frankenphp
EMBED=/path/to/your/app \
PHP_EXTENSIONS=ctype,iconv,pdo_sqlite \
./build-static.sh
```

View File

@@ -10,13 +10,13 @@ Bu komutu Laravel uygulamanızın ana dizininden çalıştırın:
docker run -p 80:80 -p 443:443 -p 443:443/udp -v $PWD:/app dunglas/frankenphp
```
And tadını çıkarın!
Ve tadını çıkarın!
## Yerel Kurulum
Alternatif olarak, Laravel projelerinizi FrankenPHP ile yerel makinenizden çalıştırabilirsiniz:
1. [Sisteminize karşılık gelen binary dosyayı indirin](https://github.com/php/frankenphp/releases)
1. [Sisteminize karşılık gelen ikili dosyayı indirin](../#standalone-binary)
2. Aşağıdaki yapılandırmayı Laravel projenizin kök dizinindeki `Caddyfile` adlı bir dosyaya ekleyin:
```caddyfile
@@ -30,8 +30,10 @@ Alternatif olarak, Laravel projelerinizi FrankenPHP ile yerel makinenizden çal
root public/
# Sıkıştırmayı etkinleştir (isteğe bağlı)
encode zstd br gzip
# PHP dosyalarını public/ dizininden çalıştırın ve varlıkları sunun
php_server
# public/ dizininden PHP dosyalarını çalıştırın ve statik dosyaları servis edin
php_server {
try_files {path} index.php
}
}
```
@@ -64,11 +66,118 @@ php artisan octane:frankenphp
- `--admin-port`: Yönetici sunucusunun erişilebilir olması gereken port (varsayılan: `2019`)
- `--workers`: İstekleri işlemek için hazır olması gereken worker sayısı (varsayılan: `auto`)
- `--max-requests`: Sunucu yeniden yüklenmeden önce işlenecek istek sayısı (varsayılan: `500`)
- `--caddyfile`: FrankenPHP `Caddyfile` dosyasının yolu
- `--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))
- `--https`: HTTPS, HTTP/2 ve HTTP/3'ü etkinleştirin ve sertifikaları otomatik olarak oluşturup yenileyin
- `--http-redirect`: HTTP'den HTTPS'ye yeniden yönlendirmeyi etkinleştir (yalnızca --https geçilirse etkinleştirilir)
- `--watch`: Uygulamada kod değişikliği olduğunda sunucuyu otomatik olarak yeniden yükle
- `--http-redirect`: HTTP'den HTTPS'ye yeniden yönlendirmeyi etkinleştir (yalnızca --https ile birlikte geçilirse etkinleşir)
- `--watch`: Uygulama değiştirildiğinde sunucuyu otomatik olarak yeniden yükle
- `--poll`: Dosyaları bir ağ üzerinden izlemek için izleme sırasında dosya sistemi yoklamasını kullanın
- `--log-level`: Belirtilen günlük seviyesinde veya üzerinde günlük mesajları
- `--log-level`: Yerel Caddy günlüğünü kullanarak belirtilen günlük seviyesinde veya üzerinde mesajları kaydedin
Laravel Octane hakkında daha fazla bilgi edinmek için [Laravel Octane resmi belgelerine](https://laravel.com/docs/octane) göz atın.
> [!TIP]
> 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.
[Laravel Octane hakkında daha fazla bilgiyi resmi belgelerde bulabilirsiniz](https://laravel.com/docs/octane).
## Laravel Uygulamalarını Bağımsız Çalıştırılabilir Dosyalar Olarak Dağıtma
[FrankenPHP'nin uygulama gömme özelliğini](embed.md) kullanarak, Laravel
uygulamalarını bağımsız çalıştırılabilir dosyalar olarak dağıtmak mümkündür.
Linux için Laravel uygulamanızı bağımsız bir çalıştırılabilir olarak paketlemek için şu adımları izleyin:
1. Uygulamanızın deposunda `static-build.Dockerfile` adında bir dosya oluşturun:
```dockerfile
FROM --platform=linux/amd64 dunglas/frankenphp:static-builder-gnu
# İkiliyi musl-libc sistemlerinde çalıştırmayı düşünüyorsanız, bunun yerine static-builder-musl kullanın
# Uygulamanızı kopyalayın
WORKDIR /go/src/app/dist/app
COPY . .
# Yer kaplamamak için testleri ve diğer gereksiz dosyaları kaldırın
# Alternatif olarak, bu dosyaları bir .dockerignore dosyasına ekleyin
RUN rm -Rf tests/
# .env dosyasını kopyalayın
RUN cp .env.example .env
# APP_ENV ve APP_DEBUG değerlerini production için uygun hale getirin
RUN sed -i'' -e 's/^APP_ENV=.*/APP_ENV=production/' -e 's/^APP_DEBUG=.*/APP_DEBUG=false/' .env
# Gerekirse .env dosyanıza diğer değişiklikleri yapın
# Bağımlılıkları yükleyin
RUN composer install --ignore-platform-reqs --no-dev -a
# Statik ikiliyi derleyin
WORKDIR /go/src/app/
RUN EMBED=dist/app/ ./build-static.sh
```
> [!CAUTION]
> Bazı `.dockerignore` dosyaları
> `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.
2. İmajı oluşturun:
```console
docker build -t static-laravel-app -f static-build.Dockerfile .
```
3. İkili dosyayı dışa aktarın:
```console
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
```
4. Önbellekleri doldurun:
```console
frankenphp php-cli artisan optimize
```
5. Veritabanı migration'larını çalıştırın (varsa):
```console
frankenphp php-cli artisan migrate
```
6. Uygulamanın gizli anahtarını oluşturun:
```console
frankenphp php-cli artisan key:generate
```
7. Sunucuyu başlatın:
```console
frankenphp php-server
```
Uygulamanız artık hazır!
Mevcut 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)
belgelerinde öğrenin.
### Depolama Yolunu Değiştirme
Varsayılan olarak, Laravel yüklenen dosyaları, önbellekleri, logları vb. uygulamanın `storage/` dizininde saklar.
Gömülü uygulamalar için bu uygun değildir, çünkü her yeni sürüm farklı bir geçici dizine çıkarılacaktır.
Geç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.
### Bağımsız Çalıştırılabilir Dosyalarla Octane'i Çalıştırma
Laravel Octane uygulamalarını bağımsız çalıştırılabilir dosyalar olarak paketlemek bile mümkündür!
Bunu 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.
Ardından, Octane üzerinden FrankenPHP'yi worker modunda başlatmak için şunu çalıştırın:
```console
PATH="$PWD:$PATH" frankenphp php-cli artisan octane:frankenphp
```
> [!CAUTION]
> Komutun çalışması için, bağımsız ikili dosya mutlaka `frankenphp` olarak adlandırılmış olmalıdır,
> çünkü Octane, yol üzerinde `frankenphp` adlı bir programın mevcut olmasını bekler.

View File

@@ -37,6 +37,9 @@ frankenphp php-server --worker /path/to/your/worker/script.php --watch="/path/to
## Symfony Runtime
> [!TIP]
> The following section is only necessary prior to Symfony 7.4, where native support for FrankenPHP worker mode was introduced.
The worker mode of FrankenPHP is supported by the [Symfony Runtime Component](https://symfony.com/doc/current/components/runtime.html).
To start any Symfony application in a worker, install the FrankenPHP package of [PHP Runtime](https://github.com/php-runtime/runtime):
@@ -78,9 +81,15 @@ $myApp->boot();
// Handler outside the loop for better performance (doing less work)
$handler = static function () use ($myApp) {
// Called when a request is received,
// superglobals, php://input and the like are reset
echo $myApp->handle($_GET, $_POST, $_COOKIE, $_FILES, $_SERVER);
try {
// Called when a request is received,
// superglobals, php://input and the like are reset
echo $myApp->handle($_GET, $_POST, $_COOKIE, $_FILES, $_SERVER);
} catch (\Throwable $exception) {
// `set_exception_handler` is called only when the worker script ends,
// which may not be what you expect, so catch and handle exceptions here
(new \MyCustomExceptionHandler)->handleException($exception);
}
};
$maxRequests = (int)($_SERVER['MAX_REQUESTS'] ?? 0);

5
env.go
View File

@@ -1,10 +1,9 @@
package frankenphp
// #cgo nocallback frankenphp_init_persistent_string
// #cgo nocallback frankenphp_add_assoc_str_ex
// #cgo noescape frankenphp_init_persistent_string
// #cgo noescape frankenphp_add_assoc_str_ex
// #include "frankenphp.h"
// #include <Zend/zend_API.h>
import "C"
import (
"os"
@@ -98,7 +97,7 @@ func go_getfullenv(threadIndex C.uintptr_t, trackVarsArray *C.zval) {
env := getSandboxedEnv(thread)
for key, val := range env {
C.frankenphp_add_assoc_str_ex(trackVarsArray, toUnsafeChar(key), C.size_t(len(key)), val)
C.add_assoc_str_ex(trackVarsArray, toUnsafeChar(key), C.size_t(len(key)), val)
}
}

View File

@@ -51,7 +51,6 @@ frankenphp_version frankenphp_get_version() {
frankenphp_config frankenphp_get_config() {
return (frankenphp_config){
frankenphp_get_version(),
#ifdef ZTS
true,
#else
@@ -75,6 +74,10 @@ __thread uintptr_t thread_index;
__thread bool is_worker_thread = false;
__thread zval *os_environment = NULL;
void frankenphp_update_local_thread_context(bool is_worker) {
is_worker_thread = is_worker;
}
static void frankenphp_update_request_context() {
/* the server context is stored on the go side, still SG(server_context) needs
* to not be NULL */
@@ -82,7 +85,7 @@ static void frankenphp_update_request_context() {
/* status It is not reset by zend engine, set it to 200. */
SG(sapi_headers).http_response_code = 200;
is_worker_thread = go_update_request_info(thread_index, &SG(request_info));
go_update_request_info(thread_index, &SG(request_info));
}
static void frankenphp_free_request_context() {
@@ -206,11 +209,6 @@ PHPAPI void get_full_env(zval *track_vars_array) {
go_getfullenv(thread_index, track_vars_array);
}
void frankenphp_add_assoc_str_ex(zval *track_vars_array, char *key,
size_t keylen, zend_string *val) {
add_assoc_str_ex(track_vars_array, key, keylen, val);
}
/* Adapted from php_request_startup() */
static int frankenphp_worker_request_startup() {
int retval = SUCCESS;
@@ -432,10 +430,11 @@ PHP_FUNCTION(frankenphp_handle_request) {
zend_unset_timeout();
#endif
bool has_request = go_frankenphp_worker_handle_request_start(thread_index);
struct go_frankenphp_worker_handle_request_start_return result =
go_frankenphp_worker_handle_request_start(thread_index);
if (frankenphp_worker_request_startup() == FAILURE
/* Shutting down */
|| !has_request) {
|| !result.r0) {
RETURN_FALSE;
}
@@ -450,24 +449,39 @@ PHP_FUNCTION(frankenphp_handle_request) {
/* Call the PHP func passed to frankenphp_handle_request() */
zval retval = {0};
zval *callback_ret = NULL;
fci.size = sizeof fci;
fci.retval = &retval;
if (zend_call_function(&fci, &fcc) == SUCCESS) {
zval_ptr_dtor(&retval);
fci.params = result.r1;
fci.param_count = result.r1 == NULL ? 0 : 1;
if (zend_call_function(&fci, &fcc) == SUCCESS && Z_TYPE(retval) != IS_UNDEF) {
callback_ret = &retval;
}
/*
* If an exception occurred, print the message to the client before
* closing the connection and bailout.
* closing the connection.
*/
if (EG(exception) && !zend_is_unwind_exit(EG(exception)) &&
!zend_is_graceful_exit(EG(exception))) {
zend_exception_error(EG(exception), E_ERROR);
zend_bailout();
if (EG(exception)) {
if (!zend_is_unwind_exit(EG(exception)) &&
!zend_is_graceful_exit(EG(exception))) {
zend_exception_error(EG(exception), E_ERROR);
} else {
/* exit() will jump directly to after php_execute_script */
zend_bailout();
}
}
frankenphp_worker_request_shutdown();
go_frankenphp_finish_worker_request(thread_index);
go_frankenphp_finish_worker_request(thread_index, callback_ret);
if (result.r1 != NULL) {
zval_ptr_dtor(result.r1);
}
if (callback_ret != NULL) {
zval_ptr_dtor(&retval);
}
RETURN_TRUE;
}
@@ -493,7 +507,72 @@ PHP_FUNCTION(headers_send) {
RETURN_LONG(sapi_send_headers());
}
PHP_FUNCTION(mercure_publish) {
zval *topics;
zend_string *data = NULL, *id = NULL, *type = NULL;
zend_bool private = 0;
zend_long retry = 0;
bool retry_is_null = 1;
ZEND_PARSE_PARAMETERS_START(1, 6)
Z_PARAM_ZVAL(topics)
Z_PARAM_OPTIONAL
Z_PARAM_STR_OR_NULL(data)
Z_PARAM_BOOL(private)
Z_PARAM_STR_OR_NULL(id)
Z_PARAM_STR_OR_NULL(type)
Z_PARAM_LONG_OR_NULL(retry, retry_is_null)
ZEND_PARSE_PARAMETERS_END();
if (Z_TYPE_P(topics) != IS_ARRAY && Z_TYPE_P(topics) != IS_STRING) {
zend_argument_type_error(1, "must be of type array|string");
RETURN_THROWS();
}
struct go_mercure_publish_return result =
go_mercure_publish(thread_index, topics, data, private, id, type, retry);
switch (result.r1) {
case 0:
RETURN_STR(result.r0);
case 1:
zend_throw_exception(spl_ce_RuntimeException, "No Mercure hub configured",
0);
RETURN_THROWS();
case 2:
zend_throw_exception(spl_ce_RuntimeException, "Publish failed", 0);
RETURN_THROWS();
}
zend_throw_exception(spl_ce_RuntimeException,
"FrankenPHP not built with Mercure support", 0);
RETURN_THROWS();
}
PHP_FUNCTION(frankenphp_log) {
zend_string *message = NULL;
zend_long level = 0;
zval *context = NULL;
ZEND_PARSE_PARAMETERS_START(1, 3)
Z_PARAM_STR(message)
Z_PARAM_OPTIONAL
Z_PARAM_LONG(level)
Z_PARAM_ARRAY(context)
ZEND_PARSE_PARAMETERS_END();
char *ret = NULL;
ret = go_log_attrs(thread_index, message, level, context);
if (ret != NULL) {
zend_throw_exception(spl_ce_RuntimeException, ret, 0);
free(ret);
RETURN_THROWS();
}
}
PHP_MINIT_FUNCTION(frankenphp) {
register_frankenphp_symbols(module_number);
zend_function *func;
// Override putenv
@@ -594,8 +673,9 @@ static char *frankenphp_read_cookies(void) {
}
/* all variables with well defined keys can safely be registered like this */
void frankenphp_register_trusted_var(zend_string *z_key, char *value,
size_t val_len, HashTable *ht) {
static inline void frankenphp_register_trusted_var(zend_string *z_key,
char *value, size_t val_len,
HashTable *ht) {
if (value == NULL) {
zval empty;
ZVAL_EMPTY_STRING(&empty);
@@ -786,7 +866,7 @@ static void frankenphp_register_variables(zval *track_vars_array) {
}
static void frankenphp_log_message(const char *message, int syslog_type_int) {
go_log((char *)message, syslog_type_int);
go_log(thread_index, (char *)message, syslog_type_int);
}
static char *frankenphp_getenv(const char *name, size_t name_len) {
@@ -1029,8 +1109,7 @@ static char **cli_argv;
* <johannes@php.net> Parts based on CGI SAPI Module by Rasmus Lerdorf, Stig
* Bakken and Zeev Suraski
*/
static void cli_register_file_handles(bool no_close) /* {{{ */
{
static void cli_register_file_handles(void) {
php_stream *s_in, *s_out, *s_err;
php_stream_context *sc_in = NULL, *sc_out = NULL, *sc_err = NULL;
zend_constant ic, oc, ec;
@@ -1039,6 +1118,17 @@ static void cli_register_file_handles(bool no_close) /* {{{ */
s_out = php_stream_open_wrapper_ex("php://stdout", "wb", 0, NULL, sc_out);
s_err = php_stream_open_wrapper_ex("php://stderr", "wb", 0, NULL, sc_err);
/* Release stream resources, but don't free the underlying handles. Othewrise,
* extensions which write to stderr or company during mshutdown/gshutdown
* won't have the expected functionality.
*/
if (s_in)
s_in->flags |= PHP_STREAM_FLAG_NO_RSCR_DTOR_CLOSE;
if (s_out)
s_out->flags |= PHP_STREAM_FLAG_NO_RSCR_DTOR_CLOSE;
if (s_err)
s_err->flags |= PHP_STREAM_FLAG_NO_RSCR_DTOR_CLOSE;
if (s_in == NULL || s_out == NULL || s_err == NULL) {
if (s_in)
php_stream_close(s_in);
@@ -1049,12 +1139,6 @@ static void cli_register_file_handles(bool no_close) /* {{{ */
return;
}
if (no_close) {
s_in->flags |= PHP_STREAM_FLAG_NO_CLOSE;
s_out->flags |= PHP_STREAM_FLAG_NO_CLOSE;
s_err->flags |= PHP_STREAM_FLAG_NO_CLOSE;
}
/*s_in_process = s_in;*/
php_stream_to_zval(s_in, &ic.value);
@@ -1073,7 +1157,6 @@ static void cli_register_file_handles(bool no_close) /* {{{ */
ec.name = zend_string_init_interned("STDERR", sizeof("STDERR") - 1, 0);
zend_register_constant(&ec);
}
/* }}} */
static void sapi_cli_register_variables(zval *track_vars_array) /* {{{ */
{
@@ -1118,7 +1201,7 @@ static void *execute_script_cli(void *arg) {
php_embed_init(cli_argc, cli_argv);
cli_register_file_handles(false);
cli_register_file_handles();
zend_first_try {
if (eval) {
/* evaluate the cli_script as literal PHP code (php-cli -r "...") */

View File

@@ -41,55 +41,71 @@ import (
type contextKeyStruct struct{}
var contextKey = contextKeyStruct{}
var (
ErrInvalidRequest = errors.New("not a FrankenPHP request")
ErrAlreadyStarted = errors.New("FrankenPHP is already started")
ErrInvalidPHPVersion = errors.New("FrankenPHP is only compatible with PHP 8.2+")
ErrMainThreadCreation = errors.New("error creating the main thread")
ErrRequestContextCreation = errors.New("error during request context creation")
ErrScriptExecution = errors.New("error during PHP script execution")
ErrNotRunning = errors.New("FrankenPHP is not running. For proper configuration visit: https://frankenphp.dev/docs/config/#caddyfile-config")
ErrInvalidRequest = errors.New("not a FrankenPHP request")
ErrAlreadyStarted = errors.New("FrankenPHP is already started")
ErrInvalidPHPVersion = errors.New("FrankenPHP is only compatible with PHP 8.2+")
ErrMainThreadCreation = errors.New("error creating the main thread")
ErrScriptExecution = errors.New("error during PHP script execution")
ErrNotRunning = errors.New("FrankenPHP is not running. For proper configuration visit: https://frankenphp.dev/docs/config/#caddyfile-config")
isRunning bool
ErrInvalidRequestPath = ErrRejected{"invalid request path", http.StatusBadRequest}
ErrInvalidContentLengthHeader = ErrRejected{"invalid Content-Length header", http.StatusBadRequest}
ErrMaxWaitTimeExceeded = ErrRejected{"maximum request handling time exceeded", http.StatusServiceUnavailable}
loggerMu sync.RWMutex
logger *slog.Logger
contextKey = contextKeyStruct{}
serverHeader = []string{"FrankenPHP"}
isRunning bool
onServerShutdown []func()
// Set default values to make Shutdown() idempotent
globalMu sync.Mutex
globalCtx = context.Background()
globalLogger = slog.Default()
metrics Metrics = nullMetrics{}
maxWaitTime time.Duration
)
type ErrRejected struct {
message string
status int
}
func (e ErrRejected) Error() string {
return e.message
}
type syslogLevel int
const (
emerg syslogLevel = iota // system is unusable
alert // action must be taken immediately
crit // critical conditions
err // error conditions
warning // warning conditions
notice // normal but significant condition
info // informational
debug // debug-level messages
syslogLevelEmerg syslogLevel = iota // system is unusable
syslogLevelAlert // action must be taken immediately
syslogLevelCrit // critical conditions
syslogLevelErr // error conditions
syslogLevelWarn // warning conditions
syslogLevelNotice // normal but significant condition
syslogLevelInfo // informational
syslogLevelDebug // debug-level messages
)
func (l syslogLevel) String() string {
switch l {
case emerg:
case syslogLevelEmerg:
return "emerg"
case alert:
case syslogLevelAlert:
return "alert"
case crit:
case syslogLevelCrit:
return "crit"
case err:
case syslogLevelErr:
return "err"
case warning:
case syslogLevelWarn:
return "warning"
case notice:
case syslogLevelNotice:
return "notice"
case debug:
case syslogLevelDebug:
return "debug"
default:
return "info"
@@ -137,10 +153,10 @@ func Config() PHPConfig {
}
}
func calculateMaxThreads(opt *opt) (int, int, int, error) {
func calculateMaxThreads(opt *opt) (numWorkers int, _ error) {
maxProcs := runtime.GOMAXPROCS(0) * 2
maxThreadsFromWorkers := 0
var numWorkers int
for i, w := range opt.workers {
if w.num <= 0 {
// https://github.com/php/frankenphp/issues/126
@@ -149,33 +165,53 @@ func calculateMaxThreads(opt *opt) (int, int, int, error) {
metrics.TotalWorkers(w.name, w.num)
numWorkers += opt.workers[i].num
if w.maxThreads > 0 {
if w.maxThreads < w.num {
return 0, fmt.Errorf("worker max_threads (%d) must be greater or equal to worker num (%d) (%q)", w.maxThreads, w.num, w.fileName)
}
if w.maxThreads > opt.maxThreads && opt.maxThreads > 0 {
return 0, fmt.Errorf("worker max_threads (%d) cannot be greater than total max_threads (%d) (%q)", w.maxThreads, opt.maxThreads, w.fileName)
}
maxThreadsFromWorkers += w.maxThreads - w.num
}
}
numThreadsIsSet := opt.numThreads > 0
maxThreadsIsSet := opt.maxThreads != 0
maxThreadsIsAuto := opt.maxThreads < 0 // maxthreads < 0 signifies auto mode (see phpmaintread.go)
// if max_threads is only defined in workers, scale up to the sum of all worker max_threads
if !maxThreadsIsSet && maxThreadsFromWorkers > 0 {
maxThreadsIsSet = true
if numThreadsIsSet {
opt.maxThreads = opt.numThreads + maxThreadsFromWorkers
} else {
opt.maxThreads = numWorkers + 1 + maxThreadsFromWorkers
}
}
if numThreadsIsSet && !maxThreadsIsSet {
opt.maxThreads = opt.numThreads
if opt.numThreads <= numWorkers {
err := fmt.Errorf("num_threads (%d) must be greater than the number of worker threads (%d)", opt.numThreads, numWorkers)
return 0, 0, 0, err
return 0, fmt.Errorf("num_threads (%d) must be greater than the number of worker threads (%d)", opt.numThreads, numWorkers)
}
return opt.numThreads, numWorkers, opt.maxThreads, nil
return numWorkers, nil
}
if maxThreadsIsSet && !numThreadsIsSet {
opt.numThreads = numWorkers + 1
if !maxThreadsIsAuto && opt.numThreads > opt.maxThreads {
err := fmt.Errorf("max_threads (%d) must be greater than the number of worker threads (%d)", opt.maxThreads, numWorkers)
return 0, 0, 0, err
return 0, fmt.Errorf("max_threads (%d) must be greater than the number of worker threads (%d)", opt.maxThreads, numWorkers)
}
return opt.numThreads, numWorkers, opt.maxThreads, nil
return numWorkers, nil
}
if !numThreadsIsSet {
if !maxThreadsIsSet && !numThreadsIsSet {
if numWorkers >= maxProcs {
// Start at least as many threads as workers, and keep a free thread to handle requests in non-worker mode
opt.numThreads = numWorkers + 1
@@ -184,21 +220,19 @@ func calculateMaxThreads(opt *opt) (int, int, int, error) {
}
opt.maxThreads = opt.numThreads
return opt.numThreads, numWorkers, opt.maxThreads, nil
return numWorkers, nil
}
// both num_threads and max_threads are set
if opt.numThreads <= numWorkers {
err := fmt.Errorf("num_threads (%d) must be greater than the number of worker threads (%d)", opt.numThreads, numWorkers)
return 0, 0, 0, err
return 0, fmt.Errorf("num_threads (%d) must be greater than the number of worker threads (%d)", opt.numThreads, numWorkers)
}
if !maxThreadsIsAuto && opt.maxThreads < opt.numThreads {
err := fmt.Errorf("max_threads (%d) must be greater than or equal to num_threads (%d)", opt.maxThreads, opt.numThreads)
return 0, 0, 0, err
return 0, fmt.Errorf("max_threads (%d) must be greater than or equal to num_threads (%d)", opt.maxThreads, opt.numThreads)
}
return opt.numThreads, numWorkers, opt.maxThreads, nil
return numWorkers, nil
}
// Init starts the PHP runtime and the configured workers.
@@ -217,73 +251,102 @@ func Init(options ...Option) error {
opt := &opt{}
for _, o := range options {
if err := o(opt); err != nil {
Shutdown()
return err
}
}
if opt.logger == nil {
// set a default logger
// to disable logging, set the logger to slog.New(slog.NewTextHandler(io.Discard, nil))
l := slog.New(slog.NewTextHandler(os.Stdout, nil))
globalMu.Lock()
loggerMu.Lock()
logger = l
loggerMu.Unlock()
} else {
loggerMu.Lock()
logger = opt.logger
loggerMu.Unlock()
if opt.ctx != nil {
globalCtx = opt.ctx
opt.ctx = nil
}
if opt.logger != nil {
globalLogger = opt.logger
opt.logger = nil
}
globalMu.Unlock()
if opt.metrics != nil {
metrics = opt.metrics
}
maxWaitTime = opt.maxWaitTime
totalThreadCount, workerThreadCount, maxThreadCount, err := calculateMaxThreads(opt)
workerThreadCount, err := calculateMaxThreads(opt)
if err != nil {
Shutdown()
return err
}
metrics.TotalThreads(totalThreadCount)
metrics.TotalThreads(opt.numThreads)
config := Config()
if config.Version.MajorVersion < 8 || (config.Version.MajorVersion == 8 && config.Version.MinorVersion < 2) {
Shutdown()
return ErrInvalidPHPVersion
}
if config.ZTS {
if !config.ZendMaxExecutionTimers && runtime.GOOS == "linux" {
logger.Warn(`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`)
if globalLogger.Enabled(globalCtx, slog.LevelWarn) {
globalLogger.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`)
}
}
} else {
totalThreadCount = 1
logger.Warn(`ZTS is not enabled, only 1 thread will be available, recompile PHP using the "--enable-zts" configuration option or performance will be degraded`)
opt.numThreads = 1
if globalLogger.Enabled(globalCtx, slog.LevelWarn) {
globalLogger.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`)
}
}
mainThread, err := initPHPThreads(totalThreadCount, maxThreadCount, opt.phpIni)
mainThread, err := initPHPThreads(opt.numThreads, opt.maxThreads, opt.phpIni)
if err != nil {
Shutdown()
return err
}
regularRequestChan = make(chan *frankenPHPContext, totalThreadCount-workerThreadCount)
regularThreads = make([]*phpThread, 0, totalThreadCount-workerThreadCount)
for i := 0; i < totalThreadCount-workerThreadCount; i++ {
regularRequestChan = make(chan contextHolder)
regularThreads = make([]*phpThread, 0, opt.numThreads-workerThreadCount)
for i := 0; i < opt.numThreads-workerThreadCount; i++ {
convertToRegularThread(getInactivePHPThread())
}
if err := initWorkers(opt.workers); err != nil {
Shutdown()
return err
}
if err := initWatchers(opt); err != nil {
Shutdown()
return err
}
initAutoScaling(mainThread)
ctx := context.Background()
logger.LogAttrs(ctx, slog.LevelInfo, "FrankenPHP started 🐘", slog.String("php_version", Version().Version), slog.Int("num_threads", mainThread.numThreads), slog.Int("max_threads", mainThread.maxThreads))
if EmbeddedAppPath != "" {
logger.LogAttrs(ctx, slog.LevelInfo, "embedded PHP app 📦", slog.String("path", EmbeddedAppPath))
if globalLogger.Enabled(globalCtx, slog.LevelInfo) {
globalLogger.LogAttrs(globalCtx, slog.LevelInfo, "FrankenPHP started 🐘", slog.String("php_version", Version().Version), slog.Int("num_threads", mainThread.numThreads), slog.Int("max_threads", mainThread.maxThreads))
if EmbeddedAppPath != "" {
globalLogger.LogAttrs(globalCtx, slog.LevelInfo, "embedded PHP app 📦", slog.String("path", EmbeddedAppPath))
}
}
// register the startup/shutdown hooks (mainly useful for extensions)
onServerShutdown = nil
for _, w := range opt.workers {
if w.onServerStartup != nil {
w.onServerStartup()
}
if w.onServerShutdown != nil {
onServerShutdown = append(onServerShutdown, w.onServerShutdown)
}
}
return nil
@@ -295,7 +358,12 @@ func Shutdown() {
return
}
drainWatcher()
// call the shutdown hooks (mainly useful for extensions)
for _, fn := range onServerShutdown {
fn()
}
drainWatchers()
drainAutoScaling()
drainPHPThreads()
@@ -307,41 +375,52 @@ func Shutdown() {
}
isRunning = false
logger.Debug("FrankenPHP shut down")
if globalLogger.Enabled(globalCtx, slog.LevelDebug) {
globalLogger.LogAttrs(globalCtx, slog.LevelDebug, "FrankenPHP shut down")
}
resetGlobals()
}
// ServeHTTP executes a PHP script according to the given context.
func ServeHTTP(responseWriter http.ResponseWriter, request *http.Request) error {
h := responseWriter.Header()
if h["Server"] == nil {
h["Server"] = serverHeader
}
if !isRunning {
return ErrNotRunning
}
fc, ok := fromContext(request.Context())
ctx := request.Context()
fc, ok := fromContext(ctx)
ch := contextHolder{ctx, fc}
if !ok {
return ErrInvalidRequest
}
fc.responseWriter = responseWriter
if !fc.validate() {
return nil
if err := fc.validate(); err != nil {
return err
}
// Detect if a worker is available to handle this request
if fc.worker != nil {
fc.worker.handleRequest(fc)
return nil
return fc.worker.handleRequest(ch)
}
// If no worker was available, send the request to non-worker threads
handleRequestWithRegularPHPThreads(fc)
return nil
return handleRequestWithRegularPHPThreads(ch)
}
//export go_ub_write
func go_ub_write(threadIndex C.uintptr_t, cBuf *C.char, length C.int) (C.size_t, C.bool) {
fc := phpThreads[threadIndex].getRequestContext()
thread := phpThreads[threadIndex]
fc := thread.frankenPHPContext()
if fc.isDone {
return 0, C.bool(true)
@@ -356,14 +435,27 @@ func go_ub_write(threadIndex C.uintptr_t, cBuf *C.char, length C.int) (C.size_t,
writer = fc.responseWriter
}
var ctx context.Context
i, e := writer.Write(unsafe.Slice((*byte)(unsafe.Pointer(cBuf)), length))
if e != nil {
fc.logger.LogAttrs(context.Background(), slog.LevelWarn, "write error", slog.Any("error", e))
ctx = thread.context()
if fc.logger.Enabled(ctx, slog.LevelWarn) {
fc.logger.LogAttrs(ctx, slog.LevelWarn, "write error", slog.Any("error", e))
}
}
if fc.responseWriter == nil {
// probably starting a worker script, log the output
fc.logger.Info(writer.(*bytes.Buffer).String())
if ctx == nil {
ctx = thread.context()
}
if fc.logger.Enabled(ctx, slog.LevelInfo) {
fc.logger.LogAttrs(ctx, slog.LevelInfo, writer.(*bytes.Buffer).String())
}
}
return C.size_t(i), C.bool(fc.clientHasClosed())
@@ -372,12 +464,15 @@ func go_ub_write(threadIndex C.uintptr_t, cBuf *C.char, length C.int) (C.size_t,
//export go_apache_request_headers
func go_apache_request_headers(threadIndex C.uintptr_t) (*C.go_string, C.size_t) {
thread := phpThreads[threadIndex]
fc := thread.getRequestContext()
ctx := thread.context()
fc := thread.frankenPHPContext()
if fc.responseWriter == nil {
// worker mode, not handling a request
logger.LogAttrs(context.Background(), slog.LevelDebug, "apache_request_headers() called in non-HTTP context", slog.String("worker", fc.scriptFilename))
if globalLogger.Enabled(ctx, slog.LevelDebug) {
globalLogger.LogAttrs(ctx, slog.LevelDebug, "apache_request_headers() called in non-HTTP context", slog.String("worker", fc.worker.name))
}
return nil, 0
}
@@ -405,10 +500,13 @@ func go_apache_request_headers(threadIndex C.uintptr_t) (*C.go_string, C.size_t)
return sd, C.size_t(len(fc.request.Header))
}
func addHeader(fc *frankenPHPContext, cString *C.char, length C.int) {
func addHeader(ctx context.Context, fc *frankenPHPContext, cString *C.char, length C.int) {
key, val := splitRawHeader(cString, int(length))
if key == "" {
fc.logger.LogAttrs(context.Background(), slog.LevelDebug, "invalid header", slog.String("header", C.GoStringN(cString, length)))
if fc.logger.Enabled(ctx, slog.LevelDebug) {
fc.logger.LogAttrs(ctx, slog.LevelDebug, "invalid header", slog.String("header", C.GoStringN(cString, length)))
}
return
}
fc.responseWriter.Header().Add(key, val)
@@ -447,8 +545,8 @@ func splitRawHeader(rawHeader *C.char, length int) (string, string) {
//export go_write_headers
func go_write_headers(threadIndex C.uintptr_t, status C.int, headers *C.zend_llist) C.bool {
fc := phpThreads[threadIndex].getRequestContext()
thread := phpThreads[threadIndex]
fc := thread.frankenPHPContext()
if fc == nil {
return C.bool(false)
}
@@ -466,13 +564,27 @@ func go_write_headers(threadIndex C.uintptr_t, status C.int, headers *C.zend_lli
for current != nil {
h := (*C.sapi_header_struct)(unsafe.Pointer(&(current.data)))
addHeader(fc, h.header, C.int(h.header_len))
addHeader(thread.context(), fc, h.header, C.int(h.header_len))
current = current.next
}
fc.responseWriter.WriteHeader(int(status))
goStatus := int(status)
if status >= 100 && status < 200 {
// go panics on invalid status code
// https://github.com/golang/go/blob/9b8742f2e79438b9442afa4c0a0139d3937ea33f/src/net/http/server.go#L1162
if goStatus < 100 || goStatus > 999 {
ctx := thread.context()
if globalLogger.Enabled(ctx, slog.LevelWarn) {
globalLogger.LogAttrs(ctx, slog.LevelWarn, "Invalid response status code", slog.Int("status_code", goStatus))
}
goStatus = 500
}
fc.responseWriter.WriteHeader(goStatus)
if goStatus < 200 {
// Clear headers, it's not automatically done by ResponseWriter.WriteHeader() for 1xx responses
h := fc.responseWriter.Header()
for k := range h {
@@ -485,8 +597,13 @@ func go_write_headers(threadIndex C.uintptr_t, status C.int, headers *C.zend_lli
//export go_sapi_flush
func go_sapi_flush(threadIndex C.uintptr_t) bool {
fc := phpThreads[threadIndex].getRequestContext()
if fc == nil || fc.responseWriter == nil {
thread := phpThreads[threadIndex]
fc := thread.frankenPHPContext()
if fc == nil {
return false
}
if fc.responseWriter == nil {
return false
}
@@ -495,7 +612,11 @@ func go_sapi_flush(threadIndex C.uintptr_t) bool {
}
if err := http.NewResponseController(fc.responseWriter).Flush(); err != nil {
logger.LogAttrs(context.Background(), 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))
ctx := thread.context()
if globalLogger.Enabled(ctx, slog.LevelWarn) {
globalLogger.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))
}
}
return false
@@ -503,7 +624,7 @@ func go_sapi_flush(threadIndex C.uintptr_t) bool {
//export go_read_post
func go_read_post(threadIndex C.uintptr_t, cBuf *C.char, countBytes C.size_t) (readBytes C.size_t) {
fc := phpThreads[threadIndex].getRequestContext()
fc := phpThreads[threadIndex].frankenPHPContext()
if fc.responseWriter == nil {
return 0
@@ -522,8 +643,12 @@ func go_read_post(threadIndex C.uintptr_t, cBuf *C.char, countBytes C.size_t) (r
//export go_read_cookies
func go_read_cookies(threadIndex C.uintptr_t) *C.char {
cookies := phpThreads[threadIndex].getRequestContext().request.Header.Values("Cookie")
cookie := strings.Join(cookies, "; ")
request := phpThreads[threadIndex].frankenPHPContext().request
if request == nil {
return nil
}
cookie := strings.Join(request.Header.Values("Cookie"), "; ")
if cookie == "" {
return nil
}
@@ -535,34 +660,97 @@ func go_read_cookies(threadIndex C.uintptr_t) *C.char {
return C.CString(cookie)
}
//export go_log
func go_log(message *C.char, level C.int) {
m := C.GoString(message)
func getLogger(threadIndex C.uintptr_t) (*slog.Logger, context.Context) {
ctxHolder := phpThreads[threadIndex]
if ctxHolder == nil {
return globalLogger, globalCtx
}
var le syslogLevel
if level < C.int(emerg) || level > C.int(debug) {
le = info
} else {
ctx := ctxHolder.context()
if ctxHolder.handler == nil {
return globalLogger, ctx
}
fCtx := ctxHolder.frankenPHPContext()
if fCtx == nil || fCtx.logger == nil {
return globalLogger, ctx
}
return fCtx.logger, ctx
}
//export go_log
func go_log(threadIndex C.uintptr_t, message *C.char, level C.int) {
logger, ctx := getLogger(threadIndex)
m := C.GoString(message)
le := syslogLevelInfo
if level >= C.int(syslogLevelEmerg) && level <= C.int(syslogLevelDebug) {
le = syslogLevel(level)
}
switch le {
case emerg, alert, crit, err:
logger.LogAttrs(context.Background(), slog.LevelError, m, slog.String("syslog_level", syslogLevel(level).String()))
case syslogLevelEmerg, syslogLevelAlert, syslogLevelCrit, syslogLevelErr:
if logger.Enabled(ctx, slog.LevelError) {
logger.LogAttrs(ctx, slog.LevelError, m, slog.String("syslog_level", le.String()))
}
case warning:
logger.LogAttrs(context.Background(), slog.LevelWarn, m, slog.String("syslog_level", syslogLevel(level).String()))
case debug:
logger.LogAttrs(context.Background(), slog.LevelDebug, m, slog.String("syslog_level", syslogLevel(level).String()))
case syslogLevelWarn:
if logger.Enabled(ctx, slog.LevelWarn) {
logger.LogAttrs(ctx, slog.LevelWarn, m, slog.String("syslog_level", le.String()))
}
case syslogLevelDebug:
if logger.Enabled(ctx, slog.LevelDebug) {
logger.LogAttrs(ctx, slog.LevelDebug, m, slog.String("syslog_level", le.String()))
}
default:
logger.LogAttrs(context.Background(), slog.LevelInfo, m, slog.String("syslog_level", syslogLevel(level).String()))
if logger.Enabled(ctx, slog.LevelInfo) {
logger.LogAttrs(ctx, slog.LevelInfo, m, slog.String("syslog_level", le.String()))
}
}
}
//export go_log_attrs
func go_log_attrs(threadIndex C.uintptr_t, message *C.zend_string, cLevel C.zend_long, cAttrs *C.zval) *C.char {
logger, ctx := getLogger(threadIndex)
level := slog.Level(cLevel)
if !logger.Enabled(ctx, level) {
return nil
}
var attrs map[string]any
if cAttrs != nil {
var err error
if attrs, err = GoMap[any](unsafe.Pointer(*(**C.zend_array)(unsafe.Pointer(&cAttrs.value[0])))); err != nil {
// PHP exception message.
return C.CString("Failed to log message: converting attrs: " + err.Error())
}
}
logger.LogAttrs(ctx, level, GoString(unsafe.Pointer(message)), mapToAttr(attrs)...)
return nil
}
func mapToAttr(input map[string]any) []slog.Attr {
out := make([]slog.Attr, 0, len(input))
for key, val := range input {
out = append(out, slog.Any(key, val))
}
return out
}
//export go_is_context_done
func go_is_context_done(threadIndex C.uintptr_t) C.bool {
return C.bool(phpThreads[threadIndex].getRequestContext().isDone)
return C.bool(phpThreads[threadIndex].frankenPHPContext().isDone)
}
// ExecuteScriptCLI executes the PHP script passed as parameter.
@@ -611,3 +799,12 @@ func timeoutChan(timeout time.Duration) <-chan time.Time {
return time.After(timeout)
}
func resetGlobals() {
globalMu.Lock()
globalCtx = context.Background()
globalLogger = slog.Default()
workers = nil
watcherIsEnabled = false
globalMu.Unlock()
}

View File

@@ -23,12 +23,6 @@ typedef struct ht_key_value_pair {
size_t val_len;
} ht_key_value_pair;
typedef struct php_variable {
const char *var;
size_t data_len;
char *data;
} php_variable;
typedef struct frankenphp_version {
unsigned char major_version;
unsigned char minor_version;
@@ -40,7 +34,6 @@ typedef struct frankenphp_version {
frankenphp_version frankenphp_get_version();
typedef struct frankenphp_config {
frankenphp_version version;
bool zts;
bool zend_signals;
bool zend_max_execution_timers;
@@ -52,6 +45,7 @@ bool frankenphp_new_php_thread(uintptr_t thread_index);
bool frankenphp_shutdown_dummy_request(void);
int frankenphp_execute_script(char *file_name);
void frankenphp_update_local_thread_context(bool is_worker);
int frankenphp_execute_script_cli(char *script, int argc, char **argv,
bool eval);
@@ -65,8 +59,6 @@ void frankenphp_register_variable_safe(char *key, char *var, size_t val_len,
zend_string *frankenphp_init_persistent_string(const char *string, size_t len);
int frankenphp_reset_opcache(void);
int frankenphp_get_current_memory_limit();
void frankenphp_add_assoc_str_ex(zval *track_vars_array, char *key,
size_t keylen, zend_string *val);
void frankenphp_register_single(zend_string *z_key, char *value, size_t val_len,
zval *track_vars_array);

View File

@@ -2,6 +2,18 @@
/** @generate-class-entries */
/** @var int */
const FRANKENPHP_LOG_LEVEL_DEBUG = -4;
/** @var int */
const FRANKENPHP_LOG_LEVEL_INFO = 0;
/** @var int */
const FRANKENPHP_LOG_LEVEL_WARN = 4;
/** @var int */
const FRANKENPHP_LOG_LEVEL_ERROR = 8;
function frankenphp_handle_request(callable $callback): bool {}
function headers_send(int $status = 200): int {}
@@ -32,3 +44,13 @@ function frankenphp_response_headers(): array|bool {}
*/
function apache_response_headers(): array|bool {}
/**
* @param string|string[] $topics
*/
function mercure_publish(string|array $topics, string $data = '', bool $private = false, ?string $id = null, ?string $type = null, ?int $retry = null): string {}
/**
* @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
* 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
*/
function frankenphp_log(string $message, int $level = 0, array $context = []): void {}

View File

@@ -1,52 +1,75 @@
/* This is a generated file, edit the .stub.php file instead.
* Stub hash: 05ebde17137c559e891362fba6524fad1e0a2dfe */
* Stub hash: 60f0d27c04f94d7b24c052e91ef294595a2bc421 */
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_frankenphp_handle_request, 0, 1,
_IS_BOOL, 0)
ZEND_ARG_TYPE_INFO(0, callback, IS_CALLABLE, 0)
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_frankenphp_handle_request, 0, 1, _IS_BOOL, 0)
ZEND_ARG_TYPE_INFO(0, callback, IS_CALLABLE, 0)
ZEND_END_ARG_INFO()
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_headers_send, 0, 0, IS_LONG, 0)
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, status, IS_LONG, 0, "200")
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, status, IS_LONG, 0, "200")
ZEND_END_ARG_INFO()
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_frankenphp_finish_request, 0, 0,
_IS_BOOL, 0)
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_frankenphp_finish_request, 0, 0, _IS_BOOL, 0)
ZEND_END_ARG_INFO()
#define arginfo_fastcgi_finish_request arginfo_frankenphp_finish_request
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_frankenphp_request_headers, 0,
0, IS_ARRAY, 0)
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_frankenphp_request_headers, 0, 0, IS_ARRAY, 0)
ZEND_END_ARG_INFO()
#define arginfo_apache_request_headers arginfo_frankenphp_request_headers
#define arginfo_getallheaders arginfo_frankenphp_request_headers
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX(arginfo_frankenphp_response_headers, 0,
0, MAY_BE_ARRAY | MAY_BE_BOOL)
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX(arginfo_frankenphp_response_headers, 0, 0, MAY_BE_ARRAY|MAY_BE_BOOL)
ZEND_END_ARG_INFO()
#define arginfo_apache_response_headers arginfo_frankenphp_response_headers
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_mercure_publish, 0, 1, IS_STRING, 0)
ZEND_ARG_TYPE_MASK(0, topics, MAY_BE_STRING|MAY_BE_ARRAY, NULL)
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, data, IS_STRING, 0, "\'\'")
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, private, _IS_BOOL, 0, "false")
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, id, IS_STRING, 1, "null")
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, type, IS_STRING, 1, "null")
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, retry, IS_LONG, 1, "null")
ZEND_END_ARG_INFO()
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_frankenphp_log, 0, 1, IS_VOID, 0)
ZEND_ARG_TYPE_INFO(0, message, IS_STRING, 0)
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, level, IS_LONG, 0, "0")
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, context, IS_ARRAY, 0, "[]")
ZEND_END_ARG_INFO()
ZEND_FUNCTION(frankenphp_handle_request);
ZEND_FUNCTION(headers_send);
ZEND_FUNCTION(frankenphp_finish_request);
ZEND_FUNCTION(frankenphp_request_headers);
ZEND_FUNCTION(frankenphp_response_headers);
ZEND_FUNCTION(mercure_publish);
ZEND_FUNCTION(frankenphp_log);
// clang-format off
static const zend_function_entry ext_functions[] = {
ZEND_FE(frankenphp_handle_request, arginfo_frankenphp_handle_request)
ZEND_FE(headers_send, arginfo_headers_send)
ZEND_FE(frankenphp_finish_request, arginfo_frankenphp_finish_request)
ZEND_FALIAS(fastcgi_finish_request, frankenphp_finish_request, arginfo_fastcgi_finish_request)
ZEND_FE(frankenphp_request_headers, arginfo_frankenphp_request_headers)
ZEND_FALIAS(apache_request_headers, frankenphp_request_headers, arginfo_apache_request_headers)
ZEND_FALIAS(getallheaders, frankenphp_request_headers, arginfo_getallheaders)
ZEND_FE(frankenphp_response_headers, arginfo_frankenphp_response_headers)
ZEND_FALIAS(apache_response_headers, frankenphp_response_headers, arginfo_apache_response_headers)
ZEND_FE_END
ZEND_FE(frankenphp_handle_request, arginfo_frankenphp_handle_request)
ZEND_FE(headers_send, arginfo_headers_send)
ZEND_FE(frankenphp_finish_request, arginfo_frankenphp_finish_request)
ZEND_FALIAS(fastcgi_finish_request, frankenphp_finish_request, arginfo_fastcgi_finish_request)
ZEND_FE(frankenphp_request_headers, arginfo_frankenphp_request_headers)
ZEND_FALIAS(apache_request_headers, frankenphp_request_headers, arginfo_apache_request_headers)
ZEND_FALIAS(getallheaders, frankenphp_request_headers, arginfo_getallheaders)
ZEND_FE(frankenphp_response_headers, arginfo_frankenphp_response_headers)
ZEND_FALIAS(apache_response_headers, frankenphp_response_headers, arginfo_apache_response_headers)
ZEND_FE(mercure_publish, arginfo_mercure_publish)
ZEND_FE(frankenphp_log, arginfo_frankenphp_log)
ZEND_FE_END
};
// clang-format on
static void register_frankenphp_symbols(int module_number)
{
REGISTER_LONG_CONSTANT("FRANKENPHP_LOG_LEVEL_DEBUG", -4, CONST_PERSISTENT);
REGISTER_LONG_CONSTANT("FRANKENPHP_LOG_LEVEL_INFO", 0, CONST_PERSISTENT);
REGISTER_LONG_CONSTANT("FRANKENPHP_LOG_LEVEL_WARN", 4, CONST_PERSISTENT);
REGISTER_LONG_CONSTANT("FRANKENPHP_LOG_LEVEL_ERROR", 8, CONST_PERSISTENT);
}

View File

@@ -8,6 +8,7 @@ import (
"bytes"
"context"
"errors"
"flag"
"fmt"
"io"
"log"
@@ -31,10 +32,6 @@ import (
"github.com/dunglas/frankenphp/internal/fastabs"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/zap/exp/zapslog"
"go.uber.org/zap/zapcore"
"go.uber.org/zap/zaptest"
"go.uber.org/zap/zaptest/observer"
)
type testOptions struct {
@@ -46,6 +43,7 @@ type testOptions struct {
realServer bool
logger *slog.Logger
initOpts []frankenphp.Option
requestOpts []frankenphp.RequestOption
phpIni map[string]string
}
@@ -60,10 +58,6 @@ func runTest(t *testing.T, test func(func(http.ResponseWriter, *http.Request), *
cwd, _ := os.Getwd()
testDataDir := cwd + "/testdata/"
if opts.logger == nil {
opts.logger = slog.New(zapslog.NewHandler(zaptest.NewLogger(t).Core()))
}
initOpts := []frankenphp.Option{frankenphp.WithLogger(opts.logger)}
if opts.workerScript != "" {
workerOpts := []frankenphp.WorkerOption{
@@ -78,15 +72,19 @@ func runTest(t *testing.T, test func(func(http.ResponseWriter, *http.Request), *
}
err := frankenphp.Init(initOpts...)
require.Nil(t, err)
require.NoError(t, err)
defer frankenphp.Shutdown()
opts.requestOpts = append(opts.requestOpts, frankenphp.WithRequestDocumentRoot(testDataDir, false))
handler := func(w http.ResponseWriter, r *http.Request) {
req, err := frankenphp.NewRequestWithContext(r, frankenphp.WithRequestDocumentRoot(testDataDir, false))
req, err := frankenphp.NewRequestWithContext(r, opts.requestOpts...)
assert.NoError(t, err)
err = frankenphp.ServeHTTP(w, req)
assert.NoError(t, err)
if err != nil && !errors.As(err, &frankenphp.ErrRejected{}) {
assert.Fail(t, fmt.Sprintf("Received unexpected error:\n%+v", err))
}
}
var ts *httptest.Server
@@ -109,6 +107,7 @@ func runTest(t *testing.T, test func(func(http.ResponseWriter, *http.Request), *
func testRequest(req *http.Request, handler func(http.ResponseWriter, *http.Request), t *testing.T) (string, *http.Response) {
t.Helper()
w := httptest.NewRecorder()
handler(w, req)
resp := w.Result()
@@ -133,6 +132,16 @@ func testPost(url string, body string, handler func(http.ResponseWriter, *http.R
return testRequest(req, handler, t)
}
func TestMain(m *testing.M) {
flag.Parse()
if !testing.Verbose() {
slog.SetDefault(slog.New(slog.DiscardHandler))
}
os.Exit(m.Run())
}
func TestHelloWorld_module(t *testing.T) { testHelloWorld(t, nil) }
func TestHelloWorld_worker(t *testing.T) {
testHelloWorld(t, &testOptions{workerScript: "index.php"})
@@ -406,36 +415,61 @@ my_autoloader`, i), body)
}, opts)
}
func TestLog_module(t *testing.T) { testLog(t, &testOptions{}) }
func TestLog_worker(t *testing.T) {
testLog(t, &testOptions{workerScript: "log.php"})
func TestLog_error_log_module(t *testing.T) { testLog_error_log(t, &testOptions{}) }
func TestLog_error_log_worker(t *testing.T) {
testLog_error_log(t, &testOptions{workerScript: "log-error_log.php"})
}
func testLog(t *testing.T, opts *testOptions) {
logger, logs := observer.New(zapcore.InfoLevel)
opts.logger = slog.New(zapslog.NewHandler(logger))
func testLog_error_log(t *testing.T, opts *testOptions) {
var buf fmt.Stringer
opts.logger, buf = newTestLogger(t)
runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/log.php?i=%d", i), nil)
req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/log-error_log.php?i=%d", i), nil)
w := httptest.NewRecorder()
handler(w, req)
for logs.FilterMessage(fmt.Sprintf("request %d", i)).Len() <= 0 {
assert.Contains(t, buf.String(), fmt.Sprintf("request %d", i))
}, opts)
}
func TestLog_frankenphp_log_module(t *testing.T) { testLog_frankenphp_log(t, &testOptions{}) }
func TestLog_frankenphp_log_worker(t *testing.T) {
testLog_frankenphp_log(t, &testOptions{workerScript: "log-frankenphp_log.php"})
}
func testLog_frankenphp_log(t *testing.T, opts *testOptions) {
var buf fmt.Stringer
opts.logger, buf = newTestLogger(t)
runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/log-frankenphp_log.php?i=%d", i), nil)
w := httptest.NewRecorder()
handler(w, req)
logs := buf.String()
for _, message := range []string{
`level=INFO msg="default level message"`,
fmt.Sprintf(`level=DEBUG msg="some debug message %d" "key int"=1`, i),
fmt.Sprintf(`level=INFO msg="some info message %d" "key string"=string`, i),
fmt.Sprintf(`level=WARN msg="some warn message %d"`, i),
fmt.Sprintf(`level=ERROR msg="some error message %d" err="[a v]"`, i),
} {
assert.Contains(t, logs, message)
}
}, opts)
}
func TestConnectionAbort_module(t *testing.T) { testConnectionAbort(t, &testOptions{}) }
func TestConnectionAbort_worker(t *testing.T) {
testConnectionAbort(t, &testOptions{workerScript: "connectionStatusLog.php"})
testConnectionAbort(t, &testOptions{workerScript: "connection_status.php"})
}
func testConnectionAbort(t *testing.T, opts *testOptions) {
testFinish := func(finish string) {
t.Run(fmt.Sprintf("finish=%s", finish), func(t *testing.T) {
logger, logs := observer.New(zapcore.InfoLevel)
opts.logger = slog.New(zapslog.NewHandler(logger))
var buf syncBuffer
opts.logger = slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelInfo}))
runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/connectionStatusLog.php?i=%d&finish=%s", i, finish), nil)
req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/connection_status.php?i=%d&finish=%s", i, finish), nil)
w := httptest.NewRecorder()
ctx, cancel := context.WithCancel(req.Context())
@@ -443,7 +477,7 @@ func testConnectionAbort(t *testing.T, opts *testOptions) {
cancel()
handler(w, req)
for logs.FilterMessage(fmt.Sprintf("request %d: 1", i)).Len() <= 0 {
for !strings.Contains(buf.String(), fmt.Sprintf("request %d: 1", i)) {
}
}, opts)
})
@@ -601,10 +635,13 @@ func testRequestHeaders(t *testing.T, opts *testOptions) {
}
func TestFailingWorker(t *testing.T) {
runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
body, _ := testGet("http://example.com/failing-worker.php", handler, t)
assert.Contains(t, body, "ok")
}, &testOptions{workerScript: "failing-worker.php"})
t.Cleanup(frankenphp.Shutdown)
err := frankenphp.Init(
frankenphp.WithWorkers("failing worker", "testdata/failing-worker.php", 4, frankenphp.WithWorkerMaxFailures(1)),
frankenphp.WithNumThreads(5),
)
assert.Error(t, err, "should return an immediate error if workers fail on startup")
}
func TestEnv(t *testing.T) {
@@ -759,22 +796,18 @@ func ExampleExecuteScriptCLI() {
}
func BenchmarkHelloWorld(b *testing.B) {
if err := frankenphp.Init(frankenphp.WithLogger(slog.New(slog.NewTextHandler(io.Discard, nil)))); err != nil {
panic(err)
}
defer frankenphp.Shutdown()
require.NoError(b, frankenphp.Init())
b.Cleanup(frankenphp.Shutdown)
cwd, _ := os.Getwd()
testDataDir := cwd + "/testdata/"
opt := frankenphp.WithRequestDocumentRoot(testDataDir, false)
handler := func(w http.ResponseWriter, r *http.Request) {
req, err := frankenphp.NewRequestWithContext(r, frankenphp.WithRequestDocumentRoot(testDataDir, false))
if err != nil {
panic(err)
}
req, err := frankenphp.NewRequestWithContext(r, opt)
require.NoError(b, err)
if err := frankenphp.ServeHTTP(w, req); err != nil {
panic(err)
}
require.NoError(b, frankenphp.ServeHTTP(w, req))
}
req := httptest.NewRequest("GET", "http://example.com/index.php", nil)
@@ -786,21 +819,18 @@ func BenchmarkHelloWorld(b *testing.B) {
}
func BenchmarkEcho(b *testing.B) {
if err := frankenphp.Init(frankenphp.WithLogger(slog.New(slog.NewTextHandler(io.Discard, nil)))); err != nil {
panic(err)
}
defer frankenphp.Shutdown()
require.NoError(b, frankenphp.Init())
b.Cleanup(frankenphp.Shutdown)
cwd, _ := os.Getwd()
testDataDir := cwd + "/testdata/"
opt := frankenphp.WithRequestDocumentRoot(testDataDir, false)
handler := func(w http.ResponseWriter, r *http.Request) {
req, err := frankenphp.NewRequestWithContext(r, frankenphp.WithRequestDocumentRoot(testDataDir, false))
if err != nil {
panic(err)
}
if err := frankenphp.ServeHTTP(w, req); err != nil {
panic(err)
}
req, err := frankenphp.NewRequestWithContext(r, opt)
require.NoError(b, err)
require.NoError(b, frankenphp.ServeHTTP(w, req))
}
const body = `{
@@ -852,10 +882,9 @@ func BenchmarkEcho(b *testing.B) {
}
func BenchmarkServerSuperGlobal(b *testing.B) {
if err := frankenphp.Init(frankenphp.WithLogger(slog.New(slog.NewTextHandler(io.Discard, nil)))); err != nil {
panic(err)
}
defer frankenphp.Shutdown()
require.NoError(b, frankenphp.Init())
b.Cleanup(frankenphp.Shutdown)
cwd, _ := os.Getwd()
testDataDir := cwd + "/testdata/"
@@ -900,16 +929,62 @@ func BenchmarkServerSuperGlobal(b *testing.B) {
preparedEnv := frankenphp.PrepareEnv(env)
opts := []frankenphp.RequestOption{frankenphp.WithRequestDocumentRoot(testDataDir, false), frankenphp.WithRequestPreparedEnv(preparedEnv)}
handler := func(w http.ResponseWriter, r *http.Request) {
req, err := frankenphp.NewRequestWithContext(r, frankenphp.WithRequestDocumentRoot(testDataDir, false), frankenphp.WithRequestPreparedEnv(preparedEnv))
if err != nil {
panic(err)
}
req, err := frankenphp.NewRequestWithContext(r, opts...)
require.NoError(b, err)
r.Header = headers
if err := frankenphp.ServeHTTP(w, req); err != nil {
panic(err)
}
require.NoError(b, frankenphp.ServeHTTP(w, req))
}
req := httptest.NewRequest("GET", "http://example.com/server-variable.php", nil)
w := httptest.NewRecorder()
for b.Loop() {
handler(w, req)
}
}
func BenchmarkUncommonHeaders(b *testing.B) {
require.NoError(b, frankenphp.Init())
b.Cleanup(frankenphp.Shutdown)
cwd, _ := os.Getwd()
testDataDir := cwd + "/testdata/"
// Mimics headers of a request sent by Firefox to GitHub
headers := http.Header{}
headers.Add(strings.Clone("Accept"), strings.Clone("text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8"))
headers.Add(strings.Clone("Accept-Encoding"), strings.Clone("gzip, deflate, br"))
headers.Add(strings.Clone("Accept-Language"), strings.Clone("fr,fr-FR;q=0.8,en-US;q=0.5,en;q=0.3"))
headers.Add(strings.Clone("Cache-Control"), strings.Clone("no-cache"))
headers.Add(strings.Clone("Connection"), strings.Clone("keep-alive"))
headers.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"))
headers.Add(strings.Clone("DNT"), strings.Clone("1"))
headers.Add(strings.Clone("Host"), strings.Clone("example.com"))
headers.Add(strings.Clone("Pragma"), strings.Clone("no-cache"))
headers.Add(strings.Clone("Sec-Fetch-Dest"), strings.Clone("document"))
headers.Add(strings.Clone("Sec-Fetch-Mode"), strings.Clone("navigate"))
headers.Add(strings.Clone("Sec-Fetch-Site"), strings.Clone("cross-site"))
headers.Add(strings.Clone("Sec-GPC"), strings.Clone("1"))
headers.Add(strings.Clone("Upgrade-Insecure-Requests"), strings.Clone("1"))
headers.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"))
// Some uncommon headers
headers.Add(strings.Clone("X-Super-Custom"), strings.Clone("Foo"))
headers.Add(strings.Clone("Super-Super-Custom"), strings.Clone("Foo"))
headers.Add(strings.Clone("Super-Super-Custom"), strings.Clone("Bar"))
headers.Add(strings.Clone("Very-Custom"), strings.Clone("1"))
opt := frankenphp.WithRequestDocumentRoot(testDataDir, false)
handler := func(w http.ResponseWriter, r *http.Request) {
req, err := frankenphp.NewRequestWithContext(r, opt)
require.NoError(b, err)
r.Header = headers
require.NoError(b, frankenphp.ServeHTTP(w, req))
}
req := httptest.NewRequest("GET", "http://example.com/server-variable.php", nil)
@@ -942,7 +1017,7 @@ func testRejectInvalidHeaders(t *testing.T, opts *testOptions) {
}
func TestFlushEmptyResponse_module(t *testing.T) { testFlushEmptyResponse(t, &testOptions{}) }
func TestFlushEmptyRespnse_worker(t *testing.T) {
func TestFlushEmptyResponse_worker(t *testing.T) {
testFlushEmptyResponse(t, &testOptions{workerScript: "only-headers.php"})
}
@@ -988,7 +1063,8 @@ func FuzzRequest(f *testing.F) {
// The response status must be 400 if the request path contains null bytes
if strings.Contains(req.URL.Path, "\x00") {
assert.Equal(t, 400, resp.StatusCode)
assert.Contains(t, body, "Invalid request path")
assert.Contains(t, body, "invalid request path")
return
}
@@ -999,7 +1075,6 @@ func FuzzRequest(f *testing.F) {
// Headers should always be present even if empty
assert.Contains(t, body, fmt.Sprintf("[CONTENT_TYPE] => %s", fuzzedString))
assert.Contains(t, body, fmt.Sprintf("[HTTP_FUZZED] => %s", fuzzedString))
}, &testOptions{workerScript: "request-headers.php"})
})
}

53
go.mod
View File

@@ -1,45 +1,66 @@
module github.com/dunglas/frankenphp
go 1.25.0
go 1.25.4
retract v1.0.0-rc.1 // Human error
require (
github.com/Masterminds/sprig/v3 v3.3.0
github.com/maypok86/otter v1.2.4
github.com/prometheus/client_golang v1.23.0
github.com/dunglas/mercure v0.21.4
github.com/e-dant/watcher/watcher-go v0.0.0-20251208164151-f88ec3b7e146
github.com/maypok86/otter/v2 v2.2.1
github.com/prometheus/client_golang v1.23.2
github.com/stretchr/testify v1.11.1
go.uber.org/zap v1.27.0
go.uber.org/zap/exp v0.3.0
golang.org/x/net v0.43.0
golang.org/x/net v0.48.0
)
require (
dario.cat/mergo v1.0.2 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.4.0 // indirect
github.com/MauriceGit/skiplist v0.0.0-20211105230623-77f5c8d3e145 // indirect
github.com/RoaringBitmap/roaring/v2 v2.14.4 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bits-and-blooms/bitset v1.24.4 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dolthub/maphash v0.1.0 // indirect
github.com/gammazero/deque v1.1.0 // indirect
github.com/dunglas/skipfilter v1.0.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/gofrs/uuid/v5 v5.4.0 // indirect
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/handlers v1.5.2 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/huandu/xstrings v1.5.0 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/mschoch/smat v0.2.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.65.0 // indirect
github.com/prometheus/procfs v0.17.0 // indirect
github.com/prometheus/common v0.67.4 // indirect
github.com/prometheus/procfs v0.19.2 // indirect
github.com/rogpeppe/go-internal v1.13.1 // indirect
github.com/rs/cors v1.11.1 // indirect
github.com/sagikazarmark/locafero v0.12.0 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/spf13/cast v1.9.2 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.41.0 // indirect
golang.org/x/sys v0.35.0 // indirect
golang.org/x/text v0.28.0 // indirect
google.golang.org/protobuf v1.36.8 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/spf13/viper v1.21.0 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/unrolled/secure v1.17.0 // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
go.etcd.io/bbolt v1.4.3 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.46.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.32.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

106
go.sum
View File

@@ -6,74 +6,120 @@ github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=
github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
github.com/MauriceGit/skiplist v0.0.0-20211105230623-77f5c8d3e145 h1:1yw6O62BReQ+uA1oyk9XaQTvLhcoHWmoQAgXmDFXpIY=
github.com/MauriceGit/skiplist v0.0.0-20211105230623-77f5c8d3e145/go.mod h1:877WBceefKn14QwVVn4xRFUsHsZb9clICgdeTj4XsUg=
github.com/RoaringBitmap/roaring/v2 v2.14.4 h1:4aKySrrg9G/5oRtJ3TrZLObVqxgQ9f1znCRBwEwjuVw=
github.com/RoaringBitmap/roaring/v2 v2.14.4/go.mod h1:oMvV6omPWr+2ifRdeZvVJyaz+aoEUopyv5iH0u/+wbY=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bits-and-blooms/bitset v1.24.4 h1:95H15Og1clikBrKr/DuzMXkQzECs1M6hhoGXLwLQOZE=
github.com/bits-and-blooms/bitset v1.24.4/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dolthub/maphash v0.1.0 h1:bsQ7JsF4FkkWyrP3oCnFJgrCUAFbFf3kOl4L/QxPDyQ=
github.com/dolthub/maphash v0.1.0/go.mod h1:gkg4Ch4CdCDu5h6PMriVLawB7koZ+5ijb9puGMV50a4=
github.com/dunglas/mercure v0.21.4 h1:mXPXHfB+4cYfFFCRRDY198mfef5+MQcdCpUnAHBUW2M=
github.com/dunglas/mercure v0.21.4/go.mod h1:l/dglCjp/OQx8/quRyceRPx2hqZQ3CNviwoLMRQiJ/k=
github.com/dunglas/skipfilter v1.0.0 h1:JG9SgGg4n6BlFwuTYzb9RIqjH7PfwszvWehanrYWPF4=
github.com/dunglas/skipfilter v1.0.0/go.mod h1:ryhr8j7CAHSjzeN7wI6YEuwoArQ3OQmRqWWVCEAfb9w=
github.com/e-dant/watcher/watcher-go v0.0.0-20251208164151-f88ec3b7e146 h1:h3vVM6X45PK0mAk8NqiYNQGXTyhvXy1HQ5GhuQN4eeA=
github.com/e-dant/watcher/watcher-go v0.0.0-20251208164151-f88ec3b7e146/go.mod h1:sVUOkwtftoj71nnJRG2S0oWNfXFdKpz/M9vK0z06nmM=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/gammazero/deque v1.1.0 h1:OyiyReBbnEG2PP0Bnv1AASLIYvyKqIFN5xfl1t8oGLo=
github.com/gammazero/deque v1.1.0/go.mod h1:JVrR+Bj1NMQbPnYclvDlvSX0nVGReLrQZ0aUMuWLctg=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/gofrs/uuid/v5 v5.4.0 h1:EfbpCTjqMuGyq5ZJwxqzn3Cbr2d0rUZU7v5ycAk/e/0=
github.com/gofrs/uuid/v5 v5.4.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=
github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/maypok86/otter v1.2.4 h1:HhW1Pq6VdJkmWwcZZq19BlEQkHtI8xgsQzBVXJU0nfc=
github.com/maypok86/otter v1.2.4/go.mod h1:mKLfoI7v1HOmQMwFgX4QkRk23mX6ge3RDvjdHOWG4R4=
github.com/maypok86/otter/v2 v2.2.1 h1:hnGssisMFkdisYcvQ8L019zpYQcdtPse+g0ps2i7cfI=
github.com/maypok86/otter/v2 v2.2.1/go.mod h1:1NKY9bY+kB5jwCXBJfE59u+zAwOt6C7ni1FTlFFMqVs=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM=
github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc=
github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc=
github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI=
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4=
github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/spf13/cast v1.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE=
github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/unrolled/secure v1.17.0 h1:Io7ifFgo99Bnh0J7+Q+qcMzWM6kaDPCA5FroFZEdbWU=
github.com/unrolled/secure v1.17.0/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40=
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U=
go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ=
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=

44
hotreload.go Normal file
View File

@@ -0,0 +1,44 @@
//go:build !nomercure && !nowatcher
package frankenphp
import (
"encoding/json"
"log/slog"
"github.com/dunglas/frankenphp/internal/watcher"
"github.com/dunglas/mercure"
watcherGo "github.com/e-dant/watcher/watcher-go"
)
// WithHotReload sets files to watch for file changes to trigger a hot reload update.
func WithHotReload(topic string, hub *mercure.Hub, patterns []string) Option {
return func(o *opt) error {
o.hotReload = append(o.hotReload, &watcher.PatternGroup{
Patterns: patterns,
Callback: func(events []*watcherGo.Event) {
// Wait for workers to restart before sending the update
go func() {
data, err := json.Marshal(events)
if err != nil {
if globalLogger.Enabled(globalCtx, slog.LevelError) {
globalLogger.LogAttrs(globalCtx, slog.LevelError, "error marshaling watcher events", slog.Any("error", err))
}
return
}
if err := hub.Publish(globalCtx, &mercure.Update{
Topics: []string{topic},
Event: mercure.Event{Data: string(data)},
Debug: globalLogger.Enabled(globalCtx, slog.LevelDebug),
}); err != nil && globalLogger.Enabled(globalCtx, slog.LevelError) {
globalLogger.LogAttrs(globalCtx, slog.LevelError, "error publishing hot reloading Mercure update", slog.Any("error", err))
}
}()
},
})
return nil
}
}

Some files were not shown because too many files have changed in this diff Show More