Compare commits

...

684 Commits

Author SHA1 Message Date
Alliballibaba
f80034ee4f benches 2025-12-19 23:06:41 +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
Alexander Stecher
960dd209f7 feat: multiple workers with same file (#1856)
* Allow multiple workers with the same file.

* Fix formatting of duplicate filename check

* Adds docs.

* suggestions by @alexandre-daubois.

* Update performance.md

---------

Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
2025-09-09 14:27:00 +02:00
Youenn Le Gouedec
984f0a0211 docs: replace GEN_STUB_FILE by GEN_STUB_SCRIPT (#1849) 2025-09-08 14:13:16 +02:00
dependabot[bot]
86a2d27c01 ci: bump actions/setup-go from 5 to 6 in the github-actions group
Bumps the github-actions group with 1 update: [actions/setup-go](https://github.com/actions/setup-go).


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

---
updated-dependencies:
- dependency-name: actions/setup-go
  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-09-08 13:58:05 +02:00
dependabot[bot]
0f942c8601 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.9.1 to 1.10.0
- [Release notes](https://github.com/spf13/cobra/releases)
- [Commits](https://github.com/spf13/cobra/compare/v1.9.1...v1.10.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-01 23:36:48 +02:00
dependabot[bot]
8c501a8d98 ci: bump actions/attest-build-provenance in the github-actions group (#1852) 2025-09-01 23:10:01 +02:00
Adiel Cristo
c564c3ffb9 fix: minor docs fixes 2025-08-29 12:46:40 +02:00
Adiel Cristo
6ab8350561 feat: translate file docs/extensions.md (#1843) 2025-08-29 09:17:49 +02:00
Kévin Dunglas
ad86bf49c2 chore: prepare release 1.9.1 2025-08-28 19:18:41 +02:00
Kévin Dunglas
1030e4ceb4 ci: fix issues and lint with zizmor 2025-08-28 19:16:32 +02:00
Kévin Dunglas
460d63e436 chore: bump deps and check go.mod files are clean in CI 2025-08-28 19:16:01 +02:00
Alexander Stecher
78bc5c87d8 fix: free request context if php_request_startup() errors (#1842) 2025-08-28 17:29:10 +02:00
Alexandre Daubois
99bb87167e chore: bump deps 2025-08-27 15:56:40 +02:00
Alexandre Daubois
c14d771fdf feat(docker): add support for Debian Trixie (#1777)
* ci: add support for Debian Trixie

* nit

* add Trixie to bug_report.yaml

---------

Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
2025-08-27 08:32:05 +02:00
Alexander Stecher
952754db27 fix: don't flush env between requests (#1814) 2025-08-27 08:30:40 +02:00
Marc
9b851bf53e fix: add WorkingDirectory to ReadHat service file to make mercure.db files work without absolute paths (#1835)
* add WorkingDirectory to make mercure.db files work without absolut paths

* make linter happy
2025-08-26 16:39:02 +02:00
Alexander Stecher
d540727369 feat:(extgen) make Go arrays more consistent with PHP arrays (#1800)
* Makes go arrays more consistent with PHP arrays.

* NewAssociativeArray.

* linting

* go linting

* Exposes all primitive types.

* Removes pointer alias

* linting

* Optimizes hash update.

* Fixes extgen tests.

* Moves file to tests.

* Fixes suggested by @dunglas.

* Replaces 'interface{}' with 'any'.

* Panics on wrong zval.

* interface improvements as suggested by @dunglas.

* Adjusts docs.

* Adjusts docs.

* Removes PackedArray alias and adjusts docs.

* Updates docs.
2025-08-25 16:24:15 +02:00
Alexander Stecher
c10e85b905 refactor: cleanup context (#1816)
* Removes NewRequestWithContext.

* Moves cgi logic to cgi.go

* Calls 'update_request_info' from the C side.

* Calls 'update_request_info' from the C side.

* clang-format

* Removes unnecessary export.

* Adds TODO.

* Adds TODO.

* Removes 'is_worker_thread'

* Shortens return statement.

* Removes the context refactor.

* adjusts comment.

* Skips parsing cgi path variables on explicitly assigned worker.

* suggesions by @dunglas.

* Re-introduces 'is_worker_thread'.

* More formatting.
2025-08-25 16:18:20 +02:00
Alexandre Daubois
4dd6b5ea16 fix: support filename other than ext.go and keep local vars on generation 2025-08-25 16:17:01 +02:00
Alexander Stecher
2b78ffe15c tests: make caddy_tests faster (#1823) 2025-08-25 16:16:32 +02:00
demouth
53e6d1897d docs: add Japanese translation for documentation (#1740)
* docs(ja): add Japanese documentation translation

* docs(ja): fix invalid link fragments and improve section heading
2025-08-25 16:15:10 +02:00
Adiel Cristo
1eb16f1434 feat: add Brazilian Portuguese translation (#1645)
* feat: add Brazilian Portuguese

* Translate file README.md

* Update file README.md

* Translate file docs/classic.md

* Translate file docs/worker.md

* Translate file docs/early-hints.md

* Translate file docs/mercure.md

* Translate file docs/x-sendfile.md

* Translate file docs/config.md

* Translate file docs/docker.md

* Minor fixes

* Translate file docs/production.md

* Translate file CONTRIBUTING.md

* Minor fixes

* Translate file docs/performance.md

* Minor fixes

* Translate file docs/embed.md

* Minor fixes

* Minor fixes

* Translate file docs/static.md

* Translate file docs/compile.md

* Minor fixes

* Translate file docs/metrics.md

* Translate file docs/laravel.md

* Translate file docs/known-issues.md

* Minor fixes

* Translate file docs/github-actions.md

* Fix build

* Fix build

* fix: remove text already translated

* feat: update translation

* fix: format comments based on other translations
2025-08-25 16:13:04 +02:00
Marc
34005da9f8 feat(static): add memcache and memcached (#1825) 2025-08-25 10:35:07 +02:00
Alexandre Daubois
b3fb8dfaba chore: try running tests against PHP 8.5 as an experimental job (#1700) 2025-08-22 13:43:12 +02:00
Alexandre Daubois
d7aebedd2d chore: bump golang.org/x/net to v0.43.0 2025-08-18 11:27:19 +02:00
Kévin Dunglas
5f153e06d6 chore: upgrade to Go 1.25 (#1811)
* chore: upgrade to Go 1.25

* free all interned strings (test)

* Revert "free all interned strings (test)"

This reverts commit 34823baadb.

* Another test.

* Another test

* ASAN_OPTIONS: detect_leaks=0

* Update sanitizers.yaml

* Update sanitizers.yaml

* Update sanitizers.yaml

---------

Co-authored-by: Alliballibaba <alliballibaba@gmail.com>
2025-08-18 10:46:03 +02:00
Alexandre Daubois
555c613669 chore: bump super-linter/super-linter/slim to 8.0.0 2025-08-18 10:45:38 +02:00
Alliballibaba
e34b82b425 refactor: removes 'Max Threads' 2025-08-17 21:01:45 +02:00
Alexandre Daubois
af057a93a9 chore: bump actions/download-artifact (#1812) 2025-08-15 19:30:07 +02:00
Kévin Dunglas
a1ae2692e1 chore: modernize Go code 2025-08-15 00:22:44 +02:00
Alexandre Daubois
6ad34b1cb3 chore: bump deps 2025-08-14 15:27:29 +02:00
Pierre Clavequin
f2e9217bfc docs: update Chinese documentation to latest version 2025-08-13 20:52:42 +02:00
Bram
1f6d6bde92 Simplify chown command (#1802)
* Simplify chown command

* Fix whitespace and also modify other chown command
2025-08-12 09:36:46 +02:00
Rob Landers
c7bc5a3778 handle extensions in cli mode (#1798)
Signed-off-by: Robert Landers <landers.robert@gmail.com>
2025-08-11 11:00:13 +02:00
Alexander Stecher
9e4a6b789b refactor: remove some duplications in tests (#1783)
* Removes test duplications.

* Adds t.Helper().

* Fixes tests.

* UUpdate frankenphp_test.go

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

---------

Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
2025-08-09 22:45:56 +02:00
Marc
8d148a16e2 feat(static): add iconv extension (#1793) 2025-08-07 12:50:47 +02:00
Alexandre Daubois
1d0169d321 fix(types): better zval handling to avoid leaks with arrays (#1780) 2025-08-04 19:00:13 +02:00
WeidiDeng
365eae1a99 fix(caddy): check if http app fails to provision due to not configured or invalid configuration (#1774) 2025-07-30 11:43:48 +02:00
Alexander Stecher
2a41fc183a fix: dev Docker image build (#1769)
* Fixes build command.

* Rests ti go.sh
2025-07-27 15:31:23 +02:00
Alexandre Daubois
8175ae7e8c chore: miscellaneous fix in C code (#1766) 2025-07-24 10:24:38 +02:00
Cthulhux
cd16da248a Update feature_request.yaml (#1765)
docs: fix typo in issue template
2025-07-23 16:23:56 +02:00
Kévin Dunglas
f224ffaece docs: add extensions to the TOC 2025-07-18 18:22:52 +02:00
Kévin Dunglas
50b438f978 chore: prepare release 1.9.0 2025-07-18 12:13:24 +02:00
Kévin Dunglas
f7ea33d328 chore: upgrade Mercure to v0.20 2025-07-18 12:11:51 +02:00
Emmanuel Barat
ce9620b5be feat: add pdo_sqlsrv extension to static binary 2025-07-18 01:33:15 +02:00
Kévin Dunglas
6e120283e9 chore: add support for GITHUB_TOKEN in static-builder-gnu 2025-07-17 16:52:54 +02:00
Kévin Dunglas
1da2ba1f28 fix(ci): Docker builds 2025-07-17 10:14:18 +02:00
Kévin Dunglas
0c25b2488c chore: bump deps 2025-07-16 13:29:04 +02:00
Marc
3e542576f6 chore: remove system include locations from frankenphp.go (#1734)
* add "nosys" tag to not pull in system include locations

* rename to "nosysinc"

* Revert "rename to "nosysinc""

This reverts commit a7ff2a0fd9.

* remove paths all together

* bring back rpath for macos

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

---------

Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
2025-07-16 13:27:24 +02:00
Alexandre Daubois
34fbfd467b chore(extgen): remove useless constructors 2025-07-16 12:06:23 +02:00
Alexandre Daubois
8df41236d9 feat(extgen): add support for arrays as parameters and return types (#1724)
* feat(extgen): add support for arrays as parameters and return types

* cs

---------

Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
2025-07-16 12:05:29 +02:00
Alexandre Daubois
1804e36b93 feat(extgen): add support for //export_php:namespace (#1721) 2025-07-16 12:01:39 +02:00
Marc
63c742648d feat(static): add password-argon2 to default extensions (#1732) 2025-07-16 11:59:52 +02:00
Alexander Stecher
a19fcdb38d fix: forwards php_server root to try_files (#1729)
* Adds 'root' to try_files.

* Formatting.

* Fixes test with wrong assumption.

* Adds more test cases.

* Prevents conflicts with other tests.
2025-07-16 11:58:36 +02:00
Alexander Stecher
a161af26ae fix: allow headers without whitespace after colon (#1741)
* Allows headers without whitespace after colon.

* Makes headers faster.

* Optimizes header splitting.

* Formatting.
2025-07-16 08:57:37 +02:00
Alexander Stecher
23073b6626 ci: remove the prefix from the latest tag (#1745) 2025-07-15 04:08:37 +02:00
Alexander Stecher
d5544bbca4 ci: compare Docker images with latest release tag (#1736)
* Compares docker images with latest release version.

* Fixes variable.

* Makes linter happy
2025-07-09 23:41:28 +02:00
Alexandre Daubois
6ce99f251a chore(extgen): unexport more symbols (#1719) 2025-07-07 05:55:09 +02:00
Marc
1ba19ae09e docs: bring back note for php_server -> root (#1726) 2025-07-05 21:44:02 +02:00
Alexandre Daubois
b80cb6cdea chore: cleanup duplication in sapi_cli_register_variables() (#1716) 2025-07-05 19:05:38 +02:00
Alexandre Daubois
23c493dfcf chore(ci): only trigger time consuming steps when relevant (#1714) 2025-07-05 18:37:59 +02:00
Kévin Dunglas
6be261169a chore: prepare release 1.8.0 2025-07-01 15:43:12 +02:00
Kévin Dunglas
292e98cd3d chore: better errors for GNU builds (#1712) 2025-07-01 15:17:52 +02:00
Marc
e9d8923c6a fix: g++ not found error in GNU static builds (#1713)
* add brotli and xz extensions

* temporary fix for g++ not found
2025-07-01 14:16:48 +02:00
Luffy
ac900e0df4 docs: update repository links and sync cn readme (#1711) 2025-07-01 10:29:55 +02:00
Kévin Dunglas
40ee7929a1 chore: bump deps (#1708) 2025-07-01 10:29:21 +02:00
Alexander Stecher
fb10b1e8f0 feat: worker matching (#1646)
* Adds 'match' configuration

* test

* Adds Caddy's matcher.

* Adds no-fileserver test.

* Prevents duplicate path calculations and optimizes worker access.

* trigger

* Changes worker->match to match->worker

* Adjusts tests.

* formatting

* Resets implementation to worker->match

* Provisions match path rules.

* Allows matching multiple paths

* Fixes var

* Formatting.

* refactoring.

* Adds 'match' configuration

* test

* Adds Caddy's matcher.

* Adds no-fileserver test.

* Prevents duplicate path calculations and optimizes worker access.

* trigger

* Changes worker->match to match->worker

* Adjusts tests.

* formatting

* Resets implementation to worker->match

* Provisions match path rules.

* Allows matching multiple paths

* Fixes var

* Formatting.

* refactoring.

* Update frankenphp.go

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

* Update caddy/workerconfig.go

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

* Update caddy/workerconfig.go

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

* Update caddy/module.go

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

* Update caddy/module.go

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

* Fixes suggestion

* Refactoring.

* Adds 'match' configuration

* test

* Adds Caddy's matcher.

* Adds no-fileserver test.

* Prevents duplicate path calculations and optimizes worker access.

* trigger

* Changes worker->match to match->worker

* Adjusts tests.

* formatting

* Resets implementation to worker->match

* Provisions match path rules.

* Allows matching multiple paths

* Fixes var

* Formatting.

* refactoring.

* Adds docs.

* Fixes merge removal.

* Update config.md

* go fmt.

* Adds line ending to static.txt and fixes tests.

* Trigger CI

* fix Markdown CS

---------

Co-authored-by: Alliballibaba <alliballibaba@gmail.com>
Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
2025-07-01 10:27:11 +02:00
demouth
94c3fac556 docs: removed unnecessary triple backtick block (#1709) 2025-07-01 07:01:15 +02:00
Thérage Kévin
fcc5299a20 docs: add precision on where to add max_wait_time configuration (#1640)
* Adding precision on where to add max_wait_time configuration

* Adding precision on where to add max_wait_time configuration

* fix review
2025-06-30 22:08:08 +02:00
Alexandre Daubois
92abb16bc0 docs: add French translation for extensions (#1705) 2025-06-30 16:54:43 +02:00
Alexandre Daubois
94ac4b4935 chore: use modern ZPP macros in the extension generator (#1703) 2025-06-30 14:50:36 +02:00
Alexandre Daubois
29c88c0fec feat: use modern ZEND_PARSE_PARAMETERS_NONE() macro (#1704) 2025-06-30 14:50:11 +02:00
Kévin Dunglas
80de1f8bc7 chore: bump deps (#1702) 2025-06-30 14:36:26 +02:00
Alexandre Daubois
93f2384749 docs: add extension and extension generator docs (#1652) 2025-06-30 14:29:25 +02:00
Marc
db9c8446ef feat(static): add brotli and xz extensions (#1647) 2025-06-30 13:32:46 +02:00
Alexandre Daubois
995c829247 feat: add logs on up and down scaling threads (#1695) 2025-06-30 12:01:25 +02:00
Alexandre Daubois
96400a85d0 feat(worker): make maximum consecutive failures configurable (#1692) 2025-06-30 09:38:18 +02:00
Alexandre Daubois
58fde42654 fix: improve generated C extension code (#1698) 2025-06-30 09:23:21 +02:00
Kévin Dunglas
291dd4eed9 chore!: uniformize thread attribute name in logs (#1699) 2025-06-29 09:33:06 +02:00
Alexandre Daubois
30ef5f6657 chore: use filepath.Separator instead of hardcoded separator (#1685) 2025-06-27 14:36:31 +02:00
Alexandre Daubois
8d88c13795 chore: remove TODO items not relevant anymore (#1694) 2025-06-27 14:36:09 +02:00
Alexandre Daubois
d2a1b619a5 feat: expose SSL_CIPHER env var (#1693) 2025-06-27 14:27:20 +02:00
Alexandre Daubois
9e3b47c52f fix(extgen): capitalize cgo handle function call (#1696) 2025-06-27 14:26:09 +02:00
Kévin Dunglas
abfd893d88 feat: FrankenPHP extensions (#1651)
* feat: add helpers to create PHP extensions (#1644)

* feat: add helpers to create PHP extensions

* cs

* feat: GoString

* test

* add test for RegisterExtension

* cs

* optimize includes

* fix

* feat(extensions): add the PHP extension generator (#1649)

* feat(extensions): add the PHP extension generator

* unexport many types

* unexport more symbols

* cleanup some tests

* unexport more symbols

* fix

* revert types files

* revert

* add better validation and fix templates

* remove GoStringCopy

* small fixes

---------

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

* try to fix tests

* fix CS

* try some workarounds

* try some workarounds

* ingore TestRegisterExtension

* exclude cgo tests in Docker images

* fix

* workaround...

* race detector

* simplify tests and code

* make linter happy

* feat(gofile): use templates to generate the Go file (#1666)

---------

Co-authored-by: Alexandre Daubois <2144837+alexandre-daubois@users.noreply.github.com>
2025-06-25 10:18:22 +02:00
Alexandre Daubois
bbc3e49d6f ci: remove unneeded echoes (#1683) 2025-06-25 10:17:53 +02:00
Alexandre Daubois
2712876e95 ci(docker): authenticate GitHub API calls (#1680) 2025-06-24 16:51:21 +02:00
Alexandre Daubois
b2435183f4 feat: add support for SERVER_ROOT to provide a different app root (#1678) 2025-06-23 20:47:58 +02:00
Kévin Dunglas
cfb9d9f895 chore: bump deps (#1665) 2025-06-20 17:36:05 +02:00
Kévin Dunglas
5c69109011 chore: remove SPC_PHP_DEFAULT_OPTIMIZE_CFLAGS which doesn't exist annymore (#1669)
* chore(static): remove SPC_PHP_DEFAULT_OPTIMIZE_CFLAGS, which doesn't exist anymore

* dont use pre-built packages on ARM

* CS
2025-06-20 17:35:30 +02:00
Kévin Dunglas
12f469e701 chore: bump deps (#1643) 2025-06-19 19:10:59 +02:00
Max
71aebbe0e7 perf: add popular proxy headers (#1661)
* perf: add popular proxy headers

* X-Real-IP => X-Real-Ip
2025-06-19 14:05:26 +02:00
Marc
34a0255c15 fix: fix php-server command bug when built with nobrotli tag 2025-06-18 09:43:54 +02:00
Marc
9bd314d2fb feat(static): add HTTP/3 support for curl, add amqp and lz4 extensions (#1631)
* add amqp, ast, brotli, lz and xz extensions

* brotli and xz are not released yet

* retrigger CI
2025-06-10 10:51:41 +02:00
Rob Landers
3afb709f02 link directly to try_files in performance docs (#1633) 2025-06-09 16:57:48 +02:00
Alexander Stecher
82aeb128bc refactor: split caddy.go (#1629)
* Splits modules.

* trigger build

---------

Co-authored-by: Alliballibaba <alliballibaba@gmail.com>
2025-06-07 11:09:41 +02:00
Marc
6749ddbde5 ci: remove leading v from parsed version (#1626) 2025-06-03 15:01:24 +02:00
Kévin Dunglas
82ba882a4e chore: prepare release 1.7.0 2025-06-03 10:04:05 +02:00
Kévin Dunglas
4b1679e70f chore: bump deps 2025-06-02 17:36:51 +02:00
David Buchmann
75ce2e22c2 docs: clarify Mercure URLs (#916)
* clarify mercure URLs

* Update docs/mercure.md

Co-authored-by: David Buchmann <david@liip.ch>

* Update mercure.md

---------

Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
2025-06-02 16:04:56 +02:00
Marc
5a43e9f4de feat: make frankenphp directive optional in Caddyfile (#1601)
* make frankenphp directive optional, thanks @francislavoie

* get rid of global variable

* update workers when adding to app

* suggestions

* goto instead of continue outer?

* remove empty frankenphp directives

* update config to reflect the optional frankenphp directive

* AI translations

* restore eol newlines

* don't double check for duplicate worker name

* add short form for php_server worker too

* translations

* AI hates EOL newlines now?

* suggestion to check for nil

* suggestion to use else if block
2025-06-02 15:55:55 +02:00
Alexandre Daubois
5542044376 feat: allow omitting value with the --watch flag of php-server (#1595) 2025-06-02 15:54:01 +02:00
Marc
52b65311c2 ci: get package tag version from binary instead (#1606)
* get package tag version from binary instead

* capture output for better debugging instead
2025-06-01 23:55:50 +02:00
MaximAL
2dc8048ad2 docs: optimize PNG images losslessly: 2 → 1.3 MiB (−36%) (#1623) 2025-06-01 21:47:08 +02:00
Rob Landers
a59b649dac fix: headers before flushing (#1622)
* add tests

* fix test

* attempt to send headers when flushing

* Update testdata/only-headers.php

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

---------

Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
2025-06-01 14:58:36 +02:00
Indra Gunawan
68a4548bf4 skip worker name default value assignment on unmarshal (#1607) 2025-05-31 10:25:47 +02:00
Kévin Dunglas
6f049f9a9c ci: minor cleanup (#1619)
* ci: minor cleanup

* add .golangci.yaml
2025-05-31 08:01:38 +02:00
Kévin Dunglas
340b1fd1c2 docs: improve compilation instructions 2025-05-30 14:05:08 +02:00
wu
c9329bd717 docs: fix typo in French laravel.md (#1617) 2025-05-30 14:04:29 +02:00
Kévin Dunglas
f54a1fa85e fix: prevent cert install warning in Docker images 2025-05-30 14:03:50 +02:00
Kévin Dunglas
b4115ca9a2 fix: linking on OpenBSD 2025-05-29 08:23:17 +02:00
Kévin Dunglas
14469d4a0a chore: fix typo in test comment 2025-05-27 10:10:39 +02:00
Kévin Dunglas
ee394756b1 chore: prepare release 1.6.2 2025-05-23 10:41:10 +02:00
Laury S.
5a260c430a chore: improve style of the default index.php file (#1598)
* feat: improve style of index.php file

* feat: remove assets folder

* Update index.php

* Update index.php

---------

Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
2025-05-23 10:24:57 +02:00
Kévin Dunglas
b6fcab5a95 ci: always login to Docker if not a Pull Request (#1599) 2025-05-23 00:53:03 +02:00
Kévin Dunglas
1e49586b0e chore: prepare release 1.6.1 2025-05-22 16:49:58 +02:00
Kévin Dunglas
b27cd1c986 ci: fix packages building (#1596)
* ci: fix packages building

* fix groupdel
2025-05-22 16:44:09 +02:00
Kévin Dunglas
c6483088c5 fix(docker): command to create /etc/frankenphp 2025-05-22 00:57:01 +02:00
Kévin Dunglas
5a9785d0d9 fix(docker): prevent BC break with the new Caddyfile path 2025-05-21 01:19:58 +02:00
Kévin Dunglas
c522b52804 fix: exit(), die() and uncaught exceptions must stop the worker 2025-05-21 01:19:22 +02:00
Kévin Dunglas
9a8ad979e0 ci: don't login to the Docker hub for PRs 2025-05-21 01:18:33 +02:00
Alexandre Daubois
663aff7cc4 chore: improve Homebrew compatibility 2025-05-20 20:40:14 +02:00
Kévin Dunglas
79f2b2347b chore: reduce write error level to warn in logs 2025-05-20 20:38:49 +02:00
Kévin Dunglas
bf5c98410b chore: log thread (#1589) 2025-05-20 10:10:46 +02:00
Kévin Dunglas
cf7541fde6 chore: add more logs for the worker 2025-05-19 22:43:54 +02:00
dependabot[bot]
25491068df ci: bump super-linter/super-linter in the github-actions group
Bumps the github-actions group with 1 update: [super-linter/super-linter](https://github.com/super-linter/super-linter).


Updates `super-linter/super-linter` from 7.3.0 to 7.4.0
- [Release notes](https://github.com/super-linter/super-linter/releases)
- [Changelog](https://github.com/super-linter/super-linter/blob/main/CHANGELOG.md)
- [Commits](https://github.com/super-linter/super-linter/compare/v7.3.0...v7.4.0)

---
updated-dependencies:
- dependency-name: super-linter/super-linter
  dependency-version: 7.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-19 13:49:56 +02:00
Kévin Dunglas
d72751b9fd ci: always login to the Docker Hub to mitigate rate limiting issues 2025-05-19 13:46:55 +02:00
Kévin Dunglas
8820e53819 ci: fix typo in dev Dockerfile 2025-05-16 12:00:04 +02:00
Kévin Dunglas
d2b6f9e723 chore: prepare release 1.6.0 2025-05-16 10:16:35 +02:00
Kévin Dunglas
13fbe126ea fix: automatically change cwd when embedding an app 2025-05-16 10:10:01 +02:00
Kévin Dunglas
afa7dafe1c chore: bump deps 2025-05-16 09:22:00 +02:00
Marc
39e22bd5e0 fix: use sudo to build packages (#1568) 2025-05-15 23:12:38 +02:00
Marc
bbbfdb31b5 ci: build .rpm and .deb packages (#1497)
* add ./create-rpm.sh file to build a "frankenphp" rpm package

* also build a deb package

* renamed to build-packages

* linter...

* add depends

* linter again?

* linter number 3

* linter number 4

* set default locations for ini file, conf files and extensions

* set unified path for modules that should be ok on all dists

* add default content into "package" folder

* make file executable

* worker is in public folder

* what on earth did I do x)

* use same FRANKENPHP_VERSION and make sure to let pr's run the rpm generation too (version 0.0.0) to see issues

* install ruby, fpm and rpm-build

* move to after changing base urls because it would fail with packages not found

* ruby 3 build needs gcc 10

* rpm-build is necessary too...

* and I forgot to link the package folder

* create directories if they don't exist

* copy out all frankenphp* files?

* lint fix

* only copy frankenphp-* files

* only copy frankenphp-* files

* the .deb file is name frankenphp_1.5.0... - create output folder instead and upload all things inside that
will simplify things when later adding xdebug.so and ffi.so

* update the last two steps to use the gh-output directory

* add post install script to set frankenphp able to bind to port 80 for non-root users

* dnf over yum, I think the yum alias was removed in RH 9.5

* newlines

* newlines

* add text what missing libcap means

* copy php.ini-production from php-src, linter, update ruby version

* move Caddyfile to /etc/frankenphp/Caddyfile

* linter

* fix a copy and paste error

* better describe fallback to 0.0.0

* linter

* copy installation scripts from official caddy packages, change user to frankenphp too

* bombombom

* make files executable

* tabs

* linter

* linter again

* use empty directory for three different destinations instead of keeping three empty local directories

* caddy says the file is incorrectly formatted without these spaces

* remove wildcard matcher from root directive

* Apply suggestions from code review

commit suggested changes to preinstall/postinstall scripts

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

* Update dev.Dockerfile

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

* remove misleading comment

* update documentation for paths

* update documentation for paths some more

* fix musl opcache-jit issue

* markdown linter

* the damn tab

* Apply suggestions from code review

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

* drop dev.Dockerfile php location from config.md

* add php config note to CONTRIBUTING.md

* dashes instead of asterisks in chinese docs

* fix package building

* create frankenphp user in case it doesn't exist for deb packages

* create users if they don't exist, delete them again if they didn't exist

* satisfy linter

* create the user with the same commands as the postinst/preinstall scripts

* Removes toolchain requirements.

* trigger

* Removes explicit calls to go get

* trigger

* setcap by default

* simplify example project

* bring page more in line with the caddy / apache / nginx default page

* update to html 5

* oopsies

* revert style to original

* remove https:// (caddy uses http:// on RHEL, :80 on Debian)

---------

Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
Co-authored-by: Alliballibaba <alliballibaba@gmail.com>
2025-05-14 07:33:05 +02:00
Alexander Stecher
0b83602575 fix: makes response writer error a debug message. (#1549)
* Makes response writer error a debug message.

* trigger

* log at warn level

* Update frankenphp.go

---------

Co-authored-by: Alliballibaba <alliballibaba@gmail.com>
Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
2025-05-13 17:34:10 +02:00
Kévin Dunglas
ecca9dc01d ci: use latest stable Go version for the mostly static binary (#1558)
* ci: use latest stable Go version for the mostly static binary

* fix
2025-05-13 16:10:02 +02:00
Kévin Dunglas
eb40c03a21 chore: use strings.ContainsAny() for needReplacement() 2025-05-13 10:27:35 +02:00
Alexander Stecher
c2390e7c3b fix: php-cli flag parsing conflicts (#1559)
* Fixes flag parsing.

* trigger

* trigger

* Fixes flag parsing.

---------

Co-authored-by: Alliballibaba <alliballibaba@gmail.com>
2025-05-13 10:24:59 +02:00
Kévin Dunglas
0d12a5162d fix: use local Go toolchain (#1546) 2025-05-11 22:30:19 +02:00
Alexander Stecher
a48db9422d fix: go toolchain versioning (#1545)
* Removes toolchain requirements.

* trigger

* Removes explicit calls to go get

* trigger

* Update static-builder-musl.Dockerfile

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

* Update static-builder-musl.Dockerfile

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

* Update static-builder-gnu.Dockerfile

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

* Update alpine.Dockerfile

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

* Update Dockerfile

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

* Update Dockerfile

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

* Update alpine.Dockerfile

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

* trigger

* trigger

---------

Co-authored-by: Alliballibaba <alliballibaba@gmail.com>
Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
2025-05-11 09:18:45 +02:00
Alexander Stecher
ab0fcd80de Fixes metrics also with regular request timeouts. (#1550) 2025-05-10 14:31:58 +02:00
Tolsee
2f7b987198 feat: dequeue worker request on timeout (#1540) 2025-05-09 18:01:49 +02:00
Marc
1d74b2caa8 feat: define domain specific workers in php_server and php blocks (#1509)
* add module (php_server directive) based workers

* refactor moduleID to uintptr for faster comparisons

* let workers inherit environment variables and root from php_server

* caddy can shift FrankenPHPModules in memory for some godforsaken reason, can't rely on them staying the same

* remove debugging statement

* fix tests

* refactor moduleID to uint64 for faster comparisons

* actually allow multiple workers per script filename

* remove logging

* utility function

* reuse existing worker with same filename and environment when calling newWorker with a filepath that already has a suitable worker, simply add number of threads

* no cleanup happens between tests, so restore old global worker overwriting logic

* add test, use getWorker(ForContext) function in frankenphp.go as well

* bring error on second global worker with the same filename again

* refactor to using name instead of moduleID

* nicer name

* nicer name

* add more tests

* remove test case already covered by previous test

* revert back to single variable, moduleIDs no longer relevant

* update comment

* figure out the worker to use in FrankenPHPModule::ServeHTTP

* add caddy/config_tests, add --retry 5 to download

* add caddy/config_tests

* sum up logic a bit, put worker thread addition into moduleWorkers parsing, before workers are actually created

* implement suggestions as far as possible

* fixup

* remove tags

* feat: download the mostly static binary when possible (#1467)

* feat: download the mostly static binary when possible

* cs

* docs: remove wildcard matcher from root directive (#1513)

* docs: update README with additional documentation links

Add link to classic mode, efficiently serving large static files and monitoring FrankenPHP

Signed-off-by: Romain Bastide <romain.bastide@orange.com>

* ci: combine dependabot updates for one group to 1 pull-request

* feat: compatibility with libphp.dylib on macOS

* feat: upgrade to Caddy 2.10

* feat: upgrade to Caddy 2.10

* chore: run prettier

* fix: build-static.sh consecutive builds (#1496)

* fix consecutive builds

* use minor version in PHP_VERSION

* install jq in centos container

* fix "arm64" download arch for spc binary

* jq is not available as a rpm download

* linter

* specify php 8.4 default

specify 8.4 so we manually switch to 8.5 when we make sure it works
allows to run without jq installed

* Apply suggestions from code review

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

---------

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

* chore: update Go and toolchain version (#1526)

* apply suggestions one be one - scriptpath only

* generate unique worker names by filename and number

* support worker config from embedded apps

* rename back to make sure we don't accidentally add FrankenPHPApp workers to the slice

* fix test after changing error message

* use 🧩 for module workers

* use 🌍 for global workers :)

* revert 1c414cebbc

* revert 4cc8893ced

* apply suggestions

* add dynamic config loading test of module worker

* fix test

* minor changes

---------

Signed-off-by: Romain Bastide <romain.bastide@orange.com>
Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
Co-authored-by: Indra Gunawan <hello@indra.my.id>
Co-authored-by: Romain Bastide <romain.bastide@orange.com>
2025-05-05 16:14:19 +02:00
Kévin Dunglas
92e92335e3 docs: fix warning markup 2025-05-05 15:12:04 +02:00
dependabot[bot]
8f5f9e4c8b 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 7 to 8
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/v7...v8)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-05 14:28:35 +02:00
Kévin Dunglas
5b7fc5ec52 chore: make the linter happy (#1537) 2025-05-02 11:43:54 +02:00
Marc
05220de0e3 typo snuck into last pr (#1536) 2025-05-01 16:31:18 +02:00
Alexander Stecher
3741782330 feat: '-r' option for php-cli (#1482) 2025-05-01 02:06:31 +02:00
Indra Gunawan
a6e1d3554d fix negative frankenphp_ready_workers metrics (#1491) 2025-05-01 02:05:23 +02:00
Kévin Dunglas
6f1b4f3bae ci: fix GNU manifest (#1535) 2025-04-30 14:52:40 +02:00
Thomas Cochard
cd540bda11 Fix -d / --wait arguments (#1531) 2025-04-29 16:36:23 +02:00
Alexander Stecher
8125993001 fix: disallow 2 workers with same filename (#1492)
* Disallows 2 workers with the same filename.

* Adds test.

* Prevent duplicate names.

---------

Co-authored-by: a.stecher <a.stecher@sportradar.com>
Co-authored-by: Alliballibaba <alliballibaba@gmail.com>
2025-04-29 10:18:24 +02:00
Kévin Dunglas
8583afd83e chore: add context to logs to make the linter happy (#1533) 2025-04-29 01:08:15 +02:00
Kévin Dunglas
d10a243f86 ci: fix GNU manifest (#1534) 2025-04-29 01:07:37 +02:00
Indra Gunawan
1ec37f6cc9 feat: replace zap with slog (#1527) 2025-04-26 11:04:46 +02:00
Kévin Dunglas
4ad5e870ec ci: fix static GNU binary copy (#1528) 2025-04-26 11:03:36 +02:00
Kévin Dunglas
49d2e62996 chore: bump Mercure and downgrade cbrotli (#1525)
* chore: bump Mercure

* downgrade cbrotli
2025-04-23 14:01:33 +02:00
Indra Gunawan
8febee71af chore: update Go and toolchain version (#1526) 2025-04-23 11:02:37 +02:00
Marc
16814581f9 fix: build-static.sh consecutive builds (#1496)
* fix consecutive builds

* use minor version in PHP_VERSION

* install jq in centos container

* fix "arm64" download arch for spc binary

* jq is not available as a rpm download

* linter

* specify php 8.4 default

specify 8.4 so we manually switch to 8.5 when we make sure it works
allows to run without jq installed

* Apply suggestions from code review

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

---------

Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
2025-04-23 09:44:02 +02:00
Kévin Dunglas
ffa52f7c8d chore: run prettier 2025-04-23 01:02:44 +02:00
Kévin Dunglas
4550027de4 feat: upgrade to Caddy 2.10 2025-04-22 17:34:11 +02:00
Kévin Dunglas
7f8e43fd62 feat: upgrade to Caddy 2.10 2025-04-22 16:15:11 +02:00
Kévin Dunglas
254c0a8a55 feat: compatibility with libphp.dylib on macOS 2025-04-22 16:07:07 +02:00
Indra Gunawan
22cf94d556 ci: combine dependabot updates for one group to 1 pull-request 2025-04-22 16:06:31 +02:00
Romain Bastide
a4dc93f831 docs: update README with additional documentation links
Add link to classic mode, efficiently serving large static files and monitoring FrankenPHP

Signed-off-by: Romain Bastide <romain.bastide@orange.com>
2025-04-22 16:05:06 +02:00
Indra Gunawan
c276a3f434 docs: remove wildcard matcher from root directive (#1513) 2025-04-22 11:27:29 +02:00
Kévin Dunglas
02a1c92b88 feat: download the mostly static binary when possible (#1467)
* feat: download the mostly static binary when possible

* cs
2025-04-18 14:22:58 +02:00
Kévin Dunglas
8092f4a35c chore!: update to golangci-lint-action 7 (#1508) 2025-04-17 20:33:22 +02:00
David Legrand
b250bd9a07 docs: add instructions to run Caddyfile from static binary (#1501) 2025-04-17 15:31:29 +02:00
Pierre du Plessis
99064ee3e1 fix: build-static.sh (#1474)
* Fixes build-static script

* Add composer to gnu image

* Fix syntax
2025-04-17 14:56:33 +02:00
Marc
58a728b790 docs: add configuration note about the ominous php directive (#1495)
* add note about the `php` directive in the configuration page

* Update config.md

* Update config.md

---------

Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
2025-04-17 14:54:58 +02:00
Marc
66aa975d47 fix: disable -march-native in case that lead to the illegal instruction in de265_init->init_scan_orders #1460 (#1493) 2025-04-15 15:29:51 +02:00
Kévin Dunglas
5e1ad5d797 docs: efficiently serving large static files (X-Sendfile/X-Accel-Redirect) (#896)
* docs: X-Sendfile/X-Accel-Redirect

* lint

* fix
2025-04-14 17:18:50 +02:00
Romain Bastide
96dd739064 docs: sync French docs with English (#1475)
* docs: update configuration options for frankenphp and add file watching details

* docs: add classic mode usage in french documentation

Signed-off-by: Romain Bastide <romain.bastide@orange.com>

* docs: add French metrics documentation

Signed-off-by: Romain Bastide <romain.bastide@orange.com>

* docs: improve formatting and clarity in configuration documentation

Signed-off-by: Romain Bastide <romain.bastide@orange.com>

* docs: update contributing guide with build instructions and debugging tips

Signed-off-by: Romain Bastide <romain.bastide@orange.com>

* docs: fix link formatting in classic mode documentation

Signed-off-by: Romain Bastide <romain.bastide@orange.com>

* docs: enhance Docker documentation with tag pattern for FrankenPHP image and usage details

Signed-off-by: Romain Bastide <romain.bastide@orange.com>

* docs: enhance embed documentation with PHP extensions customization

Signed-off-by: Romain Bastide <romain.bastide@orange.com>

* docs: add static binary packaging steps

Signed-off-by: Romain Bastide <romain.bastide@orange.com>

* docs: add troubleshooting for TLS/SSL issues with static binaries

Signed-off-by: Romain Bastide <romain.bastide@orange.com>

* docs: add max_threads and try_files configuration details to performance documentation

Signed-off-by: Romain Bastide <romain.bastide@orange.com>

* docs: update Docker run command syntax for clarity

Signed-off-by: Romain Bastide <romain.bastide@orange.com>

* docs: add optional dependencies and build tags for Brotli and file watcher features

Signed-off-by: Romain Bastide <romain.bastide@orange.com>

* docs: enhance static binary documentation with build instructions and performance tips

Signed-off-by: Romain Bastide <romain.bastide@orange.com>

* docs: add file change watch and manual worker restart instructions to worker documentation

Signed-off-by: Romain Bastide <romain.bastide@orange.com>

* docs: typo

* docs: remove english text in french doc

* docs: last missing translations for worker failures

* docs: typo

* docs: typo

* docs: fix lint errors

* docs: add max_wait_time configuration details and clarify thread scaling

Signed-off-by: Romain Bastide <romain.bastide@orange.com>

* docs: add missing translations for thread pool and max_threads configuration

Signed-off-by: Romain Bastide <romain.bastide@orange.com>

---------

Signed-off-by: Romain Bastide <romain.bastide@orange.com>
2025-04-08 11:01:37 +02:00
Pierre Tondereau
729cf9bba1 fix: module reload on request startup (#1476) 2025-04-01 20:54:24 +02:00
Alexander Stecher
c5752f9e3b docs: max_wait_time (#1465) 2025-04-01 20:53:04 +02:00
Pierre du Plessis
ba36f92a35 fix: remove extra -gnu suffic in static build images (#1472) 2025-04-01 08:33:09 +02:00
Kévin Dunglas
d3589f9770 chore: prepare release 1.5.0 2025-03-25 20:29:55 +01:00
Marc
8e6a183bda refactor: simplify using mimalloc (#1454)
* simplify using mimalloc

* fix the duplication issue of mimalloc.o since the linker deduplicates archives automatically, but it's slightly suboptimal. better would be to prevent cgo from duplicating it in the first place.

* only set stack size for musl

* Update build-static.sh
2025-03-25 15:22:46 +01:00
Indra Gunawan
855b3f93b1 metrics: register prometheus collectors only if enabled (#1457)
* collect metrics only if enabled

* Update caddy/caddy.go

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

* Update caddy/caddy.go

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

---------

Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
2025-03-25 11:38:54 +01:00
Marc
f85ca1c2d2 docs: glibc-based mostly static builds and loading extensions (#1453)
* add glibc based static builder to documentation

* english docs for gnu/extensions

* remove source again

* lint fixes

* why is there no .editorconfig :(

* apply suggestions

* Update docs/static.md

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

* remove list

* Update performance.md

* Update static.md

* Update static.md

---------

Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
2025-03-24 12:00:12 +01:00
Kévin Dunglas
a30ed2e9d3 ci: use latest version of watcher (#1456) 2025-03-24 11:58:00 +01:00
Kévin Dunglas
565b3a9629 chore: bump deps (#1455) 2025-03-24 11:56:20 +01:00
Gina Peter Banyard
3701516e5e refactor: call opcache_reset PHP function directly (#1401)
* Call opcache_reset PHP function directly

* prevent warning

* cleanup

* remove frankenphp_execute_php_function

* cs

---------

Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
2025-03-24 11:29:13 +01:00
Alexander Stecher
f36bd51163 perf(metrics): use WithLabelValues instead (#1450)
* Uses WithMetricLabels instead.

* trigger build

---------

Co-authored-by: Alliballibaba <alliballibaba@gmail.com>
2025-03-24 10:14:02 +01:00
Ian
45bba2101f docs: update linking to binary (#1452) 2025-03-23 07:53:28 +01:00
Indra Gunawan
87315a19ae feat: introduces worker name option, use label on worker metrics instead (#1376)
* add worker name option and use it in logs and metrics, update tests

* fix missing reference for collector

* update tests

* update docs

* fix conflict

* add missing allowedDirectives

* update tests
2025-03-22 12:32:59 +01:00
Jerry Ma
3bc426482a feat: add glibc-based static binary (#1438)
* Add gnu static binary build support

* Remove --libc option

* configure ./build-static.sh to allow extension loading with glibc

* use tabs everywhere

* do not use prebuilt sources for glibc build

* ffi does not work with musl builds

* remove unnecessary tabs

* disable opcache jit on musl

* disable opcache jit on musl again

* err, build command, not download command

* cs fixes

* spellcheck

* even more cs fixes

* fix ar removing .a libs

* disable ffi extension for now

* add gnu static action

* add gnu-static target

* skip CHECKOV 2 and 3

* rename static-builder to static-builder-musl, gnu-static to static-builder-gnu
run arm64 gnu job on ubuntu-arm

* rename build-linux to build-linux-musl

* rename job description to specify musl

* higher optimisation flags

* Update docker-bake.hcl

---------

Co-authored-by: DubbleClick <m@pyc.ac>
Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
2025-03-22 11:41:47 +01:00
Kévin Dunglas
341b0240c9 ci: include version in BuildInfo and Prometheus metrics (#1418) 2025-03-19 13:27:28 +01:00
Alexander Stecher
432824edf1 fix: ensure env is not in an invalid state on shutdown (#1442) 2025-03-19 13:22:06 +01:00
Alexander Stecher
9cca12858b feat: maximum wait times (#1445) 2025-03-19 13:21:37 +01:00
Alexander Stecher
cc473ee03e fix: better max_threads calculation (#1446) 2025-03-19 13:21:10 +01:00
Alexander Stecher
93266dfcad feat(watcher): log last changed file (#1447)
* logs last changed file.

* Fixes race condition.

---------

Co-authored-by: Alliballibaba <alliballibaba@gmail.com>
2025-03-19 13:10:02 +01:00
dependabot[bot]
6203d207fa chore(caddy): bump github.com/caddyserver/certmagic in /caddy
Bumps [github.com/caddyserver/certmagic](https://github.com/caddyserver/certmagic) from 0.21.7 to 0.22.0.
- [Release notes](https://github.com/caddyserver/certmagic/releases)
- [Commits](https://github.com/caddyserver/certmagic/compare/v0.21.7...v0.22.0)

---
updated-dependencies:
- dependency-name: github.com/caddyserver/certmagic
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-12 15:30:22 +01:00
Kévin Dunglas
424ca426cb fix: timeouts handling on macOS (#1435)
* ci: run tests on macOS

* debug

* debug

* fix

* nobrotli

* install brotli

* fix

* fix

* Also registers php.ini if ZEND_MAX_EXECUTION_TIMERS is disabled.

* Removes max_execution_time from tests (it gets overwritten on mac)

* tiny refacto

* fix free

* cs

---------

Co-authored-by: Alliballibaba <alliballibaba@gmail.com>
2025-03-11 17:34:49 +01:00
Alexander Stecher
a9cf944b62 ci: env test remediation (#1436)
* nbParallell

* trigger build

* Update frankenphp_test.go

---------

Co-authored-by: Alliballibaba <alliballibaba@gmail.com>
Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
2025-03-10 22:59:18 +01:00
Alexander Stecher
8d9ce15849 fix: log worker failures (#1437)
* Small fixes on error.

* Adds comments.

---------

Co-authored-by: Alliballibaba <alliballibaba@gmail.com>
2025-03-10 22:49:58 +01:00
Kévin Dunglas
409c0fdf5f chore: bump deps (#1434) 2025-03-10 15:35:17 +01:00
Alexander Stecher
f50248a7d2 refactor: removes context on the C side (#1404) 2025-03-10 08:44:03 +01:00
Alexander Stecher
09b8219ad4 fix(caddy): stricter configuration handling (#1424)
* Adds warnings.

* trigger build

* Errors on wrong configuration.

---------

Co-authored-by: Alliballibaba <alliballibaba@gmail.com>
2025-03-10 08:43:37 +01:00
Alexander Stecher
f2bae25a78 chore: update static build cli PHP version to 8.4 (#1425) 2025-03-09 17:04:06 +01:00
dependabot[bot]
3dd90a3071 ci: bump super-linter/super-linter from 7.2.1 to 7.3.0
Bumps [super-linter/super-linter](https://github.com/super-linter/super-linter) from 7.2.1 to 7.3.0.
- [Release notes](https://github.com/super-linter/super-linter/releases)
- [Changelog](https://github.com/super-linter/super-linter/blob/main/CHANGELOG.md)
- [Commits](https://github.com/super-linter/super-linter/compare/v7.2.1...v7.3.0)

---
updated-dependencies:
- dependency-name: super-linter/super-linter
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-03 23:22:37 +01:00
Alexander Stecher
c57f741d83 fix: concurrent env access (#1409) 2025-03-01 14:45:04 +01:00
Alexander Stecher
3ba4e257a1 fix: only drain workers on graceful shutdown (#1405)
* Only drains workers on shutdown.

* trigger build

* Marks func as experimental.

---------

Co-authored-by: Alliballibaba <alliballibaba@gmail.com>
2025-02-28 12:10:00 +01:00
Alexander Stecher
619c903386 perf: nocallback and noescape cgo flags (#1406) 2025-02-28 12:08:08 +01:00
Kévin Dunglas
78824107f0 docs: Homebrew installation instructions 2025-02-27 17:17:10 +01:00
Kévin Dunglas
f64c0f948e chore: remove unused executePHPFunction (#1398) 2025-02-21 19:09:54 +01:00
Alexander Stecher
db3e1a047c fix: race condition revealed by tests (#1403)
* Resolves a race condition

* Removes unused code.

* trigger build

* Removes accidental files.

---------

Co-authored-by: Alliballibaba <alliballibaba@gmail.com>
2025-02-21 19:09:08 +01:00
Kévin Dunglas
80f13f07ea docs: fix typos (#1399) 2025-02-21 13:55:37 +01:00
Alliballibaba2
072151dfee feat: Adds automatic thread scaling at runtime and php_ini configuration in Caddyfile (#1266)
Adds option to scale threads at runtime

Adds php_ini configuration in Caddyfile
2025-02-19 20:39:33 +01:00
Kévin Dunglas
965fa6570c chore: prepare release 1.4.4 2025-02-19 12:43:26 +01:00
Kévin Dunglas
251567a617 fix: Mercure duplicate metrics panic (#1393)
* fix: Mercure duplicate metrics panic

* tidy

* ci: clang-format
2025-02-19 12:40:59 +01:00
Indra Gunawan
57e7747b9b fix: duplicate metrics collector registration attempted panic 2025-02-19 12:18:40 +01:00
Niels Dossche
f109f0403b perf: avoid redundant work in frankenphp_release_temporary_streams()
Persistent streams are of type le_pstream, not le_stream. Therefore, the
persistent check will always be false. We can thus replace that check
with an assertion.

`zend_list_delete` removes the entry from the regular_list table, and
calls `zend_resource_dtor` via the table destructor.
When the refcount is 1, `zend_list_close` calls `zend_resource_dtor`,
but keeps the entry in the table.
Multiple calls to `zend_resource_dtor` have no effect: the destructor is
only called once.
Therefore, the `zend_list_close` operation is redundant because it is
fully included in the work done by `zend_list_delete`.
2025-02-19 00:16:00 +01:00
Kévin Dunglas
d407dbd498 chore: prepare release 1.4.3 2025-02-18 09:19:00 +01:00
Kévin Dunglas
d970309544 ci: upgrade watcher to the latest stable version (#1385)
* ci: workaround to compile the latest version of watcher

* remove workaround
2025-02-18 09:17:44 +01:00
Niels Dossche
30bf69cbe5 perf: avoid extra string allocation in get_full_env() (#1382)
* Avoid extra string allocation in get_full_env()

We can pass the key directly to add_assoc_str().

* Use add_assoc_str_ex
2025-02-18 09:11:23 +01:00
Kévin Dunglas
f61bc180c4 chore: upgrade to Go 1.24 2025-02-18 07:33:36 +01:00
Alexander Stecher
9f5e7a9eaa fix(watcher): handles associated events (#1379)
* Handles associated events.

* triggers pipeline

* Adjusts comment.

* Uses fixed version.

* Update watch_pattern_test.go

---------

Co-authored-by: Alliballibaba <alliballibaba@gmail.com>
Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
2025-02-17 23:47:27 +01:00
Kévin Dunglas
a5ca60da44 chore: fix markdown linter (#1384) 2025-02-17 23:46:11 +01:00
Indra Gunawan
1c097a6fdf feat(caddy): use logger from Caddy context (#1369) 2025-02-17 10:32:15 +01:00
Indra Gunawan
233753ca6b docs: update docs for first-time contributor (#1368) 2025-02-17 10:31:33 +01:00
Indra Gunawan
9dd05b0b1b docs: link metrics docs to website (#1370) 2025-02-17 10:30:58 +01:00
Indra Gunawan
4c92633396 fix: missing metrics with Caddy 2.9 (#1366)
* fix missing metrics

* update tests

* use interface instead
2025-02-12 12:55:53 +01:00
Zhanbolat Yerkinbay
be2e4714f5 docs: translate to RU (#1325)
* README.md

* worker.md

* early-hints.md

* config.md

* docker.md

* production.md

* fix

* mercure.md

* performance.md

* embed.md

* compile.md

* static.md

* laravel.md

* known-issues.md

* fix links

* github-actions.md

* metrics.md

* CONTRIBUTING.md

* fix

* fix

* fix

* main review fix

---------

Co-authored-by: zhanbolat <z.yerkinbay@slotegrator.space>
2025-01-29 18:09:48 +01:00
Kévin Dunglas
941f218a79 chore: prepare release 1.4.2 2025-01-28 11:22:00 +01:00
Kévin Dunglas
7bd6ca89b0 chore: bump deps 2025-01-28 11:19:14 +01:00
dependabot[bot]
5342d34126 ci: bump docker/bake-action from 5 to 6
Bumps [docker/bake-action](https://github.com/docker/bake-action) from 5 to 6.
- [Release notes](https://github.com/docker/bake-action/releases)
- [Commits](https://github.com/docker/bake-action/compare/v5...v6)

---
updated-dependencies:
- dependency-name: docker/bake-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-27 23:07:00 +01:00
Alexander Stecher
dd250e3bda perf: optimized request headers (#1335)
* Optimizes header registration.

* Adds malformed cookie tests.

* Sets key to NULL (releasing them is unnecessary)

* Adjusts test.

* Sanitizes null bytes anyways.

* Sorts headers.

* trigger

* clang-format

* More clang-format.

* Updates headers and tests.

* Adds header test.

* Adds more headers.

* Updates headers again.

* ?Removes comments.

* ?Reformats headers

* ?Reformats headers

* renames header files.

* ?Renames test.

* ?Fixes assertion.

* test

* test

* test

* Moves headers test to main package.

* Properly capitalizes headers.

* Allows and tests multiple cookie headers.

* Fixes comment.

* Adds otter back in.

* Verifies correct capitalization.

* Resets package version.

* Removes debug log.

* Makes persistent strings also interned and saves them once on the main thread.

---------

Co-authored-by: Alliballibaba <alliballibaba@gmail.com>
2025-01-27 21:48:20 +01:00
Alexander Stecher
7e39e0a201 Fix: only flush temporary unreferenced streams (#1351)
* Only flush temporary streams.
---------

Co-authored-by: Alliballibaba <alliballibaba@gmail.com>
2025-01-27 00:25:12 +01:00
Rob Landers
05aafc7c44 fix memory leaks (#1350)
* fix a memory leak on thread shutdown

* clean up unused resources at end of request

* try the obvious

* Test

* clang-format

* Also ignores persistent streams.

* Adds stream test.

* Moves clean up function to frankenphp_worker_request_shutdown.

* Fixes test on -nowatcher

* Fixes test on -nowatcher

* Update testdata/file-stream.txt

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

* Update frankenphp_test.go

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

---------

Co-authored-by: Alliballibaba <alliballibaba@gmail.com>
Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
2025-01-25 22:54:04 +01:00
Viktor Szépe
eee1de147e chore: fix CS (#1345) 2025-01-21 11:27:46 +01:00
Viktor Szépe
ece420c569 chore: fix typos (#1328)
* Fix typos

* Fix indentation
2025-01-21 00:32:52 +01:00
Alexander Stecher
2f4c8310e2 fix - flushing temporary files after each worker request (#1321)
* Removes temporary fix and flushes files on each request.
2025-01-20 18:45:00 +01:00
Kévin Dunglas
d712aed2a5 chore: prepare release 1.4.1 2025-01-19 23:41:10 +01:00
Rob Landers
d0b259df42 ensure worker failures do not count fatal errors during the request (#1336)
* ensure worker failures do not count fatal errors during the request

* only count towards the backoff if it was not in a request
2025-01-18 19:30:25 +01:00
Bruno Dabo
0681c63bc9 docs(fr): fix grammar in known-issues.md (#1339) 2025-01-18 17:50:16 +01:00
Kévin Dunglas
92e6b48156 ci: use the new ARM runners for ARM builds (#1333) 2025-01-17 19:12:31 +01:00
Alexander Stecher
e53ba345a1 docs: try_files performance (#1311)
* Updates most performant file_server solution.

* Updates most performant file_server solution.

* trigger build

* Fixes linting.

* Shortens the configuration.

* Updates title.

* Adds try_files optimization.

* Ads file_server docs back in.

* Update docs/performance.md

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

* Update docs/performance.md

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

---------

Co-authored-by: Alliballibaba <alliballibaba@gmail.com>
Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
2025-01-17 12:00:52 +01:00
Sylvain Dherbecourt
34dfd8789a docs: add link to skeleton Magento 2 on fr/cn/tr Readme (#1246) (#1331)
* docs: add link to skeleton Magento 2 (#1246)

* docs: add link to skeleton Magento 2 on fr/cn Readme (#1246)

* docs: add link to skeleton Magento 2 on fr/cn/tr Readme (#1246)

---------

Co-authored-by: Sylvain Dherbecourt <sylvain.dherbecourt@ekino.com>
2025-01-17 11:59:50 +01:00
Kévin Dunglas
16bb790d52 fix: rollback to stock Go version 2025-01-17 10:59:09 +01:00
Sylvain Dherbecourt
1e56edceb8 docs: add link to skeleton Magento 2 (#1246) (#1322)
Co-authored-by: Sylvain Dherbecourt <sylvain.dherbecourt@ekino.com>
2025-01-14 11:07:34 +01:00
Hanno Fellmann
f05f3b3d13 docs: explicitly explain how to use without worker mode (#1275) 2025-01-10 10:04:47 +01:00
Kévin Dunglas
c3031ea07f chore: prepare release 1.4.0 2025-01-09 14:50:26 +01:00
Kévin Dunglas
39a88c3e83 chore: bump deps 2025-01-08 22:23:38 +01:00
Kévin Dunglas
19344a0dfe chore: bump deps 2025-01-08 20:54:21 +01:00
Kévin Dunglas
5b86f2c554 ci: fix build-static.sh CS 2025-01-08 11:38:45 +01:00
jaap
fd6cc7148d fix(static): removed redundant ext-json for embedded apps (#1300)
Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
2025-01-08 11:37:42 +01:00
Denny Septian Panggabean
72120d7a2c fix(static): check command go and xcaddy in build-static.sh (#1298)
Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
2025-01-08 11:37:15 +01:00
Alexander Stecher
479ba0a063 fix: log error if FrankenPHP is not properly started (#1314)
Co-authored-by: Alliballibaba <alliballibaba@gmail.com>
2025-01-08 11:22:17 +01:00
Alexander Stecher
2b7b3d1e4b perf: put all $_SERVER vars into one function call. (#1303)
* Puts everything into one function call.

* Clang-format off.

* Cleans up.

* Changes function name.

* Removes unnecessary check.

* Passes hash table directly.

* Also tests that the original request path is passed.

* Puts vars into a struct.

* clang-format

---------

Co-authored-by: Alliballibaba <alliballibaba@gmail.com>
2025-01-08 08:23:23 +01:00
Yohan Giarelli
ec8eea0c7d fix(static): update patch from rust-alpine-mimalloc for mimalloc >= 2.1.8 (#1310) 2025-01-07 10:11:08 +01:00
Kévin Dunglas
c2ca4dbf03 feat(caddy): use new first_exist_fallback try policy 2025-01-06 13:34:53 +01:00
Kévin Dunglas
2276129c6d feat(caddy): upgrade to Caddy 2.9.0 2025-01-06 13:34:53 +01:00
Alexander Stecher
045ce00c15 perf: remove some useless string pinning (#1295)
* Removes pinning.

* trigger build

* Cleans up function params.

---------

Co-authored-by: Alliballibaba <alliballibaba@gmail.com>
2025-01-05 10:07:45 +01:00
Leo Lutz
43c1de2372 Update config.md (#1290)
Fix example that doesn't work with the currently used Caddy version
2024-12-28 21:28:48 +01:00
Kévin Dunglas
5a148342b0 docs: add link for musl-related problems 2024-12-23 00:32:51 +01:00
Kévin Dunglas
2f93baf984 chore: prepare release 1.3.6 2024-12-22 17:06:55 +01:00
Kévin Dunglas
7aaea72f14 ci: fix linter 2024-12-22 12:30:43 +01:00
Kévin Dunglas
028bad3e54 ci: try to fix static binary copy 2024-12-22 02:29:25 +01:00
Kévin Dunglas
851ff9976e fix(static): ARM Linux builds 2024-12-22 00:37:23 +01:00
Kévin Dunglas
07622be221 ci: try to fix static binary copy 2024-12-21 19:31:01 +01:00
Kévin Dunglas
d8f393900b fix(static): add back the cbrotli Caddy module (#1280) 2024-12-21 19:06:14 +01:00
Kévin Dunglas
e2687dbeb9 chore: bump deps 2024-12-21 19:05:53 +01:00
Kévin Dunglas
43984c3990 ci: try to fix static binary copy 2024-12-21 12:55:09 +01:00
Kévin Dunglas
e874ea8710 ci: always upload release binary (#1277) 2024-12-21 02:38:29 +01:00
Alliballibaba2
92f95342d1 fix: SIGSEGV with env vars (#1278) 2024-12-21 02:38:01 +01:00
Kévin Dunglas
0fc6ccc5ce ci: automatically create the Brew formula PR on release 2024-12-20 18:57:00 +01:00
Kévin Dunglas
13eb9e8534 chore: prepare release 1.3.5 2024-12-20 15:39:18 +01:00
Kévin Dunglas
cb37c3d66d docs: remove fibers from known issues 2024-12-20 15:38:31 +01:00
Vincent Amstoutz
f288c3688e ci: bump super-linter from 6.8 to 7.2.1 and fix codebase (#1260) 2024-12-20 15:38:13 +01:00
Kévin Dunglas
8cf6616ed6 fix: SIGSEGV when an env var is empty (#1271) 2024-12-20 15:37:42 +01:00
Richard Quadling
a3e5af523c docs: update CONTRIBUTING.md (#1270) 2024-12-20 15:27:07 +01:00
Kévin Dunglas
1bebb12ad9 chore: prepare release 1.3.4 2024-12-20 11:18:29 +01:00
Vincent Amstoutz
57bc54864e ci: update static artifact actions to v4 (#1264) 2024-12-20 10:56:44 +01:00
Kévin Dunglas
d276032e20 feat(static): add custom Caddy modules support (#1210)
* feat: add custom Caddy modules to the static binary

* cs

* missing Docker ARG and docs

* fix

* improve
2024-12-18 22:34:09 +01:00
Kévin Dunglas
20eaecf325 feat(static): better libphp.a cache strategy (#1262)
* feat(static): better libphp.a cache strategy

* cs
2024-12-18 22:33:34 +01:00
Kévin Dunglas
b16b60b053 ci: fix artifact upload 2024-12-18 22:08:21 +01:00
Vincent Amstoutz
85c273543d ci: update artifact actions to v4 (#1255) 2024-12-18 18:03:10 +01:00
Kévin Dunglas
ec99f6a761 fix(static): remove libphp.a from the Docker image 2024-12-18 17:13:07 +01:00
Kévin Dunglas
79ab84dad7 chore: bump deps 2024-12-18 17:12:50 +01:00
Kévin Dunglas
c0e0c2d07f fix(static): fix builds by switching to spc-config (#1231)
* chore: remove useless SPC workarounds

* use spc-config
2024-12-18 07:17:42 +01:00
Alliballibaba2
fbbc129e4d fix: graceful shutdown (#1242)
* Shuts caddy down gracefully.

* Moves isRunning to the very end.

* Changes check to Exiting().
2024-12-17 18:10:07 +01:00
Alliballibaba2
f592e0f47b refactor: decouple worker threads from non-worker threads (#1137)
* Decouple workers.

* Moves code to separate file.

* Cleans up the exponential backoff.

* Initial working implementation.

* Refactors php threads to take callbacks.

* Cleanup.

* Cleanup.

* Cleanup.

* Cleanup.

* Adjusts watcher logic.

* Adjusts the watcher logic.

* Fix opcache_reset race condition.

* Fixing merge conflicts and formatting.

* Prevents overlapping of TSRM reservation and script execution.

* Adjustments as suggested by @dunglas.

* Adds error assertions.

* Adds comments.

* Removes logs and explicitly compares to C.false.

* Resets check.

* Adds cast for safety.

* Fixes waitgroup overflow.

* Resolves waitgroup race condition on startup.

* Moves worker request logic to worker.go.

* Removes defer.

* Removes call from go to c.

* Fixes merge conflict.

* Adds fibers test back in.

* Refactors new thread loop approach.

* Removes redundant check.

* Adds compareAndSwap.

* Refactor: removes global waitgroups and uses a 'thread state' abstraction instead.

* Removes unnecessary method.

* Updates comment.

* Removes unnecessary booleans.

* test

* First state machine steps.

* Splits threads.

* Minimal working implementation with broken tests.

* Fixes tests.

* Refactoring.

* Fixes merge conflicts.

* Formatting

* C formatting.

* More cleanup.

* Allows for clean state transitions.

* Adds state tests.

* Adds support for thread transitioning.

* Fixes the testdata path.

* Formatting.

* Allows transitioning back to inactive state.

* Fixes go linting.

* Formatting.

* Removes duplication.

* Applies suggestions by @dunglas

* Removes redundant check.

* Locks the handler on restart.

* Removes unnecessary log.

* Changes Unpin() logic as suggested by @withinboredom

* Adds suggestions by @dunglas and resolves TODO.

* Makes restarts fully safe.

* Will make the initial startup fail even if the watcher is enabled (as is currently the case)

* Also adds compareAndSwap to the test.

* Adds comment.

* Prevents panic on initial watcher startup.
2024-12-17 11:28:51 +01:00
Kévin Dunglas
2676bffa98 docs: apply #1243 to other languages 2024-12-14 01:45:01 +01:00
MK
047ce0c8b2 docs: fix user creation example in Ubuntu-based Docker images (#1243)
* Fix user creation in default docker images

The `adduser` command uses `-D` to mean "create with defaults". The `useradd` command uses `-D` to mean "show or edit the defaults".

man pages:

- [`useradd`](https://manpages.debian.org/jessie/passwd/useradd.8.en.html)
- [`adduser](https://manpages.debian.org/jessie/adduser/adduser.8.en.html)

(Those are for Debian, but they are very similar for every other distro that I checked)

* Use a different username that doesn't already exist
2024-12-14 01:41:56 +01:00
Kévin Dunglas
2f3e4b650b chore: bump deps (#1235)
* chore: bump deps

* chore: bump indirect deps

* downgrade Brotli
2024-12-10 14:58:26 +01:00
dependabot[bot]
61922473ac ci: bump actions/attest-build-provenance from 1 to 2 (#1234)
Bumps [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance) from 1 to 2.
- [Release notes](https://github.com/actions/attest-build-provenance/releases)
- [Changelog](https://github.com/actions/attest-build-provenance/blob/main/RELEASE.md)
- [Commits](https://github.com/actions/attest-build-provenance/compare/v1...v2)

---
updated-dependencies:
- dependency-name: actions/attest-build-provenance
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-09 14:57:27 +01:00
Kévin Dunglas
fc6be5d09e chore: prepare release 1.3.3 2024-11-27 14:03:26 +01:00
Kévin Dunglas
9e2433e39b fix: display PHP version in static build (#1213) 2024-11-27 13:22:39 +01:00
Kévin Dunglas
8a199bb4d7 chore: remove useless EDANT_WATCHER_VERSION Docker ARG 2024-11-27 12:03:01 +01:00
Kévin Dunglas
fd49eade1a chore: rename php-thread.go to phpthread.go 2024-11-27 09:19:51 +01:00
Kévin Dunglas
a396e64ad6 feat: build static binaries with 8.4 (#1193) 2024-11-25 19:23:02 +01:00
Rob Landers
15ad1b4cb4 ci: add reminder to delete sensitive environment variables (#1197)
Thanks!
2024-11-24 11:09:36 +01:00
Alexander Stecher
ccf2af7ab2 fix: properly close context on worker script timeouts and crashes (#1184)
* Properly closes context on script timeouts and crashes.

* trigger pipeline

---------

Co-authored-by: Alliballibaba <alliballibaba@gmail.com>
2024-11-23 20:45:17 +01:00
Kévin Dunglas
6d123c7e66 chore: prepare release 1.3.2 2024-11-23 13:53:59 +01:00
Kévin Dunglas
a1797c49b0 chore: bump deps (#1187) 2024-11-23 13:53:08 +01:00
Rob Landers
1e279bc348 refactor: simplify exponential backoff and refactor env (#1185)
Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
2024-11-23 11:29:56 +01:00
Kévin Dunglas
449a0e7191 ci: build PHP 8.4 Docker images (#1183)
* ci: add PHP 8.4 to the CD pipeline

* fix: revert to 8.3 for Static PHP CLI

* fix
2024-11-22 18:17:12 +01:00
Rob Landers
08e99fc85f fix(metrics): handle the case where the worker is already assigned to a thread (#1171) 2024-11-21 13:23:41 +01:00
Kévin Dunglas
2d6a299dbc perf: improve php_server directive (#1180) 2024-11-21 13:22:24 +01:00
Alliballibaba2
b4748ee110 fix: don’t ignore empty request headers (#1182)
* Fixes empty request headers.

* Formatting
2024-11-21 12:58:01 +01:00
Alexander Hofbauer
b40c5a64a8 docs: update instructions for xcaddy in Dockerfile (#1170) 2024-11-18 13:45:11 +01:00
Rob Landers
0c123a2563 remove opcache_reset (#1173)
* remove opcache_reset

* reset opcache if the function exists

* simplify the check

* reformat
2024-11-17 19:19:53 +01:00
Djordje Lukic
fa64198d52 docs: simplify docker run command (#1168) 2024-11-15 15:21:03 +01:00
Brandon Kiefer
a441e22a1b fix: ignore watcher dir creation error if the watcher exists (#1165) 2024-11-14 23:45:59 +01:00
Kévin Dunglas
7af06f18d7 chore: prepare release 1.3.1 2024-11-14 17:47:11 +01:00
Kévin Dunglas
9ad06f11d3 chore: simplify benchmark.Caddyfile 2024-11-14 00:37:20 +01:00
Kévin Dunglas
0328d0600e fix: missing build tag for fastabs 2024-11-13 20:08:42 +01:00
Kévin Dunglas
2538849433 docs: fix xcaddy instructions 2024-11-13 07:11:39 +01:00
Kévin Dunglas
843d199469 perf: cache computations in WithRequestDocumentRoot (#1154) 2024-11-13 07:10:53 +01:00
Kévin Dunglas
102b4d1ad0 chore: prepare release 1.3.0 2024-11-11 23:25:55 +01:00
Kévin Dunglas
51e4445c00 docs: update php.ini path (#1110)
Co-authored-by: Rob Landers <landers.robert@gmail.com>
2024-11-11 23:25:01 +01:00
Kévin Dunglas
172b598f3b chore: bump deps (#1150) 2024-11-11 23:24:42 +01:00
Rob Landers
022b8f1094 perf: use buffered chans for requests (#1146) 2024-11-11 18:11:01 +01:00
Kévin Dunglas
9013614801 fix: sapi_module.getenv() should delegate to Go 2024-11-10 15:49:05 +01:00
Alexander Stecher
56d5d50ea9 fix: watcher pattern matching and retrying (#1143)
Co-authored-by: Alliballibaba <alliballibaba@gmail.com>
2024-11-10 15:48:47 +01:00
Kévin Dunglas
75dab8f33d chore: bump deps and misc improvements (#1135) 2024-11-04 16:42:15 +01:00
Alexander Stecher
1c3ce114f6 perf: use hot worker threads when possible (#1126)
Co-authored-by: Alliballibaba <alliballibaba@gmail.com>
Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
2024-11-04 16:18:44 +01:00
Alexander Stecher
e5ca97308e perf: optimize $_SERVER import (#1106)
Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
Co-authored-by: a.stecher <a.stecher@sportradar.com>
Co-authored-by: Alliballibaba <alliballibaba@gmail.com>
2024-11-04 15:34:00 +01:00
Alexander Stecher
ee8e1b97b9 fix: default split path for php-server command (#1127) 2024-11-02 16:45:25 +01:00
Kévin Dunglas
69c43ee43d chore: use upstream e-dant/watcher headers and build system (#1119) 2024-10-31 09:39:51 +01:00
Kévin Dunglas
dad858b697 ci: remove remaining latest- prefix 2024-10-28 16:18:36 +01:00
Kévin Dunglas
f567318e19 ci: don't compress using UPX for PRs 2024-10-27 11:51:40 +01:00
Kévin Dunglas
afedeb9d58 refactor: use build tags to disable, instead of to enable a feature (#1113) 2024-10-24 14:14:47 +02:00
Kévin Dunglas
d53f909d20 chore: various cleanups 2024-10-23 22:33:58 +02:00
Kévin Dunglas
2532eb5887 chore: compile without nosql's support for Postgres and MySQL (#1112) 2024-10-22 22:50:31 +02:00
Benoit Esnard
17e57287eb docs: fix link in SECURITY.md (#1111) 2024-10-22 17:04:43 +02:00
Kévin Dunglas
ed3703a16a docs: update SECURITY.md 2024-10-21 13:48:42 +02:00
Kévin Dunglas
f43de0ccf5 chore: bump deps 2024-10-18 15:47:09 +02:00
Rob Landers
e812473fe1 implement getenv and putenv in go (#1086)
* implement getenv and putenv in go

* fix typo

* apply formatting

* return a bool

* prevent ENV= from crashing

* optimization

* optimization

* split env workflows and use go_strings

* clean up unused code

* update tests

* remove useless sprintf

* see if this fixes the asan issues

* clean up comments

* check that VAR= works correctly and use actual php to validate the behavior

* move all unpinning to the end of the request

* handle the case where php is not installed

* fix copy-paste

* optimization

* use strings.cut

* fix lint

* override how env is filled

* reuse fullenv

* use corect function
2024-10-18 13:47:11 +02:00
jaap
5ec030830a fix: always untar embedded app on init (#1065) 2024-10-18 11:57:37 +02:00
Kévin Dunglas
dbd3ae54af fix: always ignore SIGPIPE (#1101) 2024-10-18 11:52:29 +02:00
Kévin Dunglas
5601cc9640 chore(docker): download mlocati/docker-php-extension-installer (#1049) 2024-10-18 11:50:59 +02:00
Kévin Dunglas
aa98b8c014 feat(static): re-enable ext-parallel 2024-10-18 11:50:25 +02:00
Kévin Dunglas
cc21b4dfd3 docs(octane): explain how to get structured JSON logs 2024-10-18 11:50:04 +02:00
Kévin Dunglas
e864142a7b fix: always include pthread.h 2024-10-18 11:36:53 +02:00
soyuka
cda74730ae fix: term capability code may not be available 2024-10-16 14:28:20 +02:00
Kévin Dunglas
334139ca2b feat: improve install script (#1097) 2024-10-15 18:06:21 +02:00
Alexander Stecher
ea7a514389 perf: only import os environment variables once per worker thread (#1080)
Co-authored-by: a.stecher <a.stecher@sportradar.com>
2024-10-15 12:03:58 +02:00
Arnaud Lemercier
f1e2b3ad07 docs(fr): minor improvements in performance.md (#1091)
Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
2024-10-15 11:55:19 +02:00
Alexander Stecher
8bbd16d585 Removes worker panic when the watcher is enabled. (#1092)
* Removes the worker panic when the watcher is enabled.

* Only panics on the initial boot.

* Only panics on the initial boot.

* Resets to always panic when watcher is disabled.

---------

Co-authored-by: a.stecher <a.stecher@sportradar.com>
2024-10-14 20:58:26 +02:00
Will
9acfb8be20 chore: make the branch from which Watcher is built a release branch (#1072)
Co-authored-by: Will <edant.io@proton.me>
2024-10-11 17:37:53 +02:00
Alexander Stecher
d99b16a158 perf: remove all cgo handles (#1073)
* Removes Cgo handles and adds phpThreads.

* Changes variable name.

* Changes variable name.

* Fixes merge error.

* Removes unnecessary 'workers are done' check.

* Removes panic.

* Replaces int with uint32_t.

* Changes index type to uintptr_t.

* simplify

---------

Co-authored-by: a.stecher <a.stecher@sportradar.com>
Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
2024-10-09 07:31:09 +02:00
Kévin Dunglas
e9c075a4a5 feat: add build tag to skip Watcher support (#1076)
* feat: add build tag to skip Watcher support

* fix

* fix

* cleanup
2024-10-08 23:23:53 +02:00
Kévin Dunglas
ce13140d6b chore: fix linters 2024-10-08 22:08:44 +02:00
Pulkit Kathuria
f2d7e212bd feat: add one line install script (#594)
* (feat) adds install.sh CI that auto downloads bin from github releases to current directory

* false positive

* -k gone

* Update README.md

* Update README.md

* Update install.sh

* Update install.sh

Co-authored-by: Rob Landers <landers.robert@gmail.com>

* cleanup

---------

Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
Co-authored-by: Rob Landers <landers.robert@gmail.com>
2024-10-08 21:23:56 +02:00
Kévin Dunglas
95c381ec78 feat: add build tag to skip Brotli support (#1070)
* feat: add build tag to skip Brotli support

* update docs
2024-10-08 18:57:34 +02:00
Kévin Dunglas
56d2f99548 chore: make the watcher module internal 2024-10-07 15:37:40 +02:00
Kévin Dunglas
029ce7e0ad chore: use cc instead of hardcoding compiler 2024-10-07 15:37:26 +02:00
Alexander Stecher
8d9b6e755b feat: restart workers when on source changes (#1013)
* Adds filesystem watcher with tests.

* Refactoring.

* Formatting.

* Formatting.

* Switches to absolute path in tests.

* Fixes race condition from merge conflict.

* Fixes race condition.

* Fixes tests.

* Fixes markdown lint errors.

* Switches back to absolute paths.

* Reverts back to relative file paths.

* Fixes golangci-lint issues.

* Uses github.com/dunglas/go-fswatch instead.

* Stops watcher before stopping workers.

* Updates docs.

* Avoids segfault in tests.

* Fixes watcher segmentation violations on shutdown.

* Adjusts watcher latencies and tests.

* Adds fswatch to dockerfiles

* Fixes fswatch in alpine.

* Fixes segfault (this time for real).

* Allows queueing new reload if file changes while workers are reloading.

* Makes tests more consistent.

* Prevents the watcher from getting stuck if there is an error in the worker file itself.

* Reverts changing the image.

* Puts fswatch version into docker-bake.hcl.

* Asserts instead of panicking.

* Adds notice

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

* Update dev.Dockerfile

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

* Update Dockerfile

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

* Update Dockerfile

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

* Update alpine.Dockerfile

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

* Update alpine.Dockerfile

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

* Update dev-alpine.Dockerfile

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

* Update dev-alpine.Dockerfile

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

* Update dev.Dockerfile

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

* Update docs/config.md

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

* Runs fswatch version.

* Removes .json.

* Replaces ms with s.

* Resets the channel after closing it.

* Update watcher_options.go

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

* Update watcher_test.go

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

* Asserts no error instead.

* Fixes a race condition where events are fired after frankenphp has stopped.

* Updates docs.

* Update watcher_options_test.go

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

* Allows queuing events while watchers are reloading.

* go fmt

* Refactors stopping and draining logic.

* Allows extended watcher configuration with dirs, recursion, symlinks, case-sensitivity, latency, monitor types and regex.

* Updates docs.

* Adds TODOS.

* go fmt.

* Fixes linting errors.

* Also allows wildcards in the longform and adjusts docs.

* Adds debug log.

* Fixes the watcher short form.

* Refactors sessions and options into a struct.

* Fixes an overflow in the 'workersReadyWG' on unexpected terminations.

* Properly logs errors coming from session.Start().

* go fmt.

* Adds --nocache.

* Fixes lint issue.

* Refactors and resolves race condition on worker reload.

* Implements debouncing with a timer as suggested by @withinboredom.

* Starts watcher even if no workers are defined.

* Updates docs with file limit warning.

* Adds watch config unit tests.

* Adjusts debounce timings.

* go fmt.

* Adds fswatch to static builder (test).

* Adds a short grace period between stopping and destroying the watcher sessions.

* Adds caddy test.

* Adjusts sleep time.

* Swap to edant/watcher.

* Fixes watch options and tests.

* go fmt.

* Adds TODO.

* Installs edant/watcher in the bookworm image.

* Fixes linting.

* Refactors the watcher into its own module.

* Adjusts naming.

* ADocker image adjustments and refactoring.

* Testing installation methods.

* Installs via gcc instead.

* Fixes pointer formats.

* Fixes lint issues.

* Fixes arm alpine and updates docs.

* Clang format.

* Fixes dirs.

* Adds watcher version arg.

* Uses static lib version.

* Adds watcher to tests and sanitizers.

* Uses sudo for copying the shared lib.

* Removes unnused func.

* Refactoring.

* Update .github/workflows/sanitizers.yaml

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

* Adds fpic.

* Fixes linting.

* Skips tests in msan.

* Resets op_cache in every worker thread after termination

* Review fixes part 1.

* Test: installing libstc++ instead of gcc.

* Test: using msan ignorelist.

* Test: using msan ignorelist.

* Test: using msan ignorelist.

* Allows '/**/' for global recursion and '**/' for relative recursion.

* Reverts using the ignorelist.

* Calls opcache directly.

* Adds --watch to php-server command

* Properly free CStrings.

* Sorts alphabetically and uses curl instead of git.

* Labeling and formatting.

* Update .github/workflows/sanitizers.yaml

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

* Update .github/workflows/sanitizers.yaml

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

* Update .github/workflows/tests.yaml

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

* Update .github/workflows/tests.yaml

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

* Update caddy/caddy.go

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

* Update docs/config.md

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

* Update frankenphp_with_watcher_test.go

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

* Update watcher/watcher.h

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

* Update frankenphp.c

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

* Update watcher/watcher.go

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

* Update docs/config.md

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

* Update frankenphp_with_watcher_test.go

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

* Update testdata/files/.gitignore

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

* Update watcher/watcher-c.h

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

* Update watcher/watcher.c

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

* Fixes test and Dockerfile.

* Fixes Dockerfiles.

* Resets go versions.

* Replaces unsafe.pointer with uintptr_t

* Prevents worker channels from being destroyed on reload.

* Minimizes the public api by only passing a []string.

* Adds support for directory patterns and multiple '**' globs.

* Adjusts label.

* go fmt.

* go mod tidy.

* Fixes merge conflict.

* Refactoring and formatting.

* Cleans up unused vars and functions.

* Allows dirs with a dot.

* Makes test nicer.

* Add dir tests.

* Moves the watch directive inside the worker directive.

* Adds debug log on special events.

* Removes line about symlinks.

* Hints at multiple possible --watch flags.

* Adds ./**/*.php as default watch configuration.

* Changes error to a warning.

* Changes the default to './**/*.{php,yaml,yml,twig,env}' and supports the {bracket} pattern.

* Fixes linting.

* Fixes merge conflict and adjust values.

* Adjusts values.

---------

Co-authored-by: a.stecher <a.stecher@sportradar.com>
Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
2024-10-07 13:17:24 +02:00
Rob Landers
aa585f7da0 handle worker failures gracefully (#1038)
* handle failures gracefully

* fix super-subtle race condition

* address feedback: panic instead of fatal log and make vars into consts

* pass the frankenphp context to worker-ready function

* reset backoff and failures on normal restart

* update docs

* add test and fix race condition

* fail sometimes but do not be pathological about it

* Use title case

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

* fix code style in php

* define lifecycle metrics

* ensure we update unregister the metrics and fix tests

* update caddy tests and fix typo

* update docs

* no need for this

---------

Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
2024-10-03 21:53:12 +02:00
Jamie Spittal
b8e5ad16cd docs: expand on how Laravel Octane uses Caddyfiles (#1028)
* Update laravel.md

* Update laravel.md

* Update laravel.md

---------

Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
2024-10-03 15:28:47 +02:00
Robert William Vesterman
1e20f65e26 fix: pthread include for FreeBSD (#1058)
Compilation would fail on FreeBSD due to a missing header file (``pthread.h``). This commit adds a ``#include`` for it.
2024-10-02 12:38:05 +02:00
Kévin Dunglas
59f1690596 ci: better Docker cache 2024-09-26 15:44:56 +02:00
Rob Landers
5d43fc2c8d add basic metrics (#966)
* add metrics

* change how counting works

* also replace dots

* check that metrics exist

* rename NullMetrics to nullMetrics

* update go.sum

* register collectors only once

* add tests

* add tests for metrics and fix bugs

* keep old metrics around for test

* properly reset during shutdown

* use the same method as frankenphp

* Revert "keep old metrics around for test"

This reverts commit 1f0df6f6bdaebf32aec346f068d6f42a0b5f4007.

* change to require.NoError

* compile regex only once

* remove name sanitizer

* use require

* parameterize host port because security software sucks

* remove need for renaming workers

* increase number of threads and add tests

* fix where frankenphp configuration was bleeding into later tests

* adds basic docs for metrics

* Add caddy metrics link

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

* Fix typos

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

* address feedback

* change comment to be much more "dangerous"

---------

Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
2024-09-26 09:53:37 +02:00
Kévin Dunglas
861fd8b2fa fix: remove toolchain in go.mod 2024-09-25 19:28:24 +02:00
nicolasbonnici
ae31cb9995 docs: fix link for getting started with API Platform (#1040)
* fix: link typo for getting started with apip

* Update README.md

Reviewer asked change

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

---------

Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
2024-09-24 10:08:47 +02:00
Kévin Dunglas
f62244fb69 docs: fix markdown notices 2024-09-24 10:07:42 +02:00
Davy Goossens
6c0292af99 fix(static): remove -lgcc_s flag (#1026)
undefined
2024-09-15 23:49:03 +02:00
Alexander Stecher
a4ac4eb3fb fix: placeholders in environement variables (#1018)
* Fixes checking the env key instead of the value.

* Adds custom variable test.

---------

Co-authored-by: a.stecher <a.stecher@sportradar.com>
2024-09-04 20:57:17 +02:00
Kévin Dunglas
dcf190ebcb chore: prepare release 1.2.5 2024-08-27 12:03:36 +02:00
Kévin Dunglas
d968334371 chore: bump deps 2024-08-27 12:02:39 +02:00
Kévin Dunglas
47257ec919 docs: add performance docs (#1004)
* docs: add performance docs

* docs: add PHP performance section

* Update docs/performance.md

Co-authored-by: Jacob Dreesen <jacob@hdreesen.de>

* Update docs/performance.md

Co-authored-by: Jacob Dreesen <jacob@hdreesen.de>

* Update docs/performance.md

Co-authored-by: Jacob Dreesen <jacob@hdreesen.de>

* Update docs/performance.md

Co-authored-by: Jacob Dreesen <jacob@hdreesen.de>

* Update docs/performance.md

Co-authored-by: Jacob Dreesen <jacob@hdreesen.de>

* Update docs/performance.md

Co-authored-by: Jacob Dreesen <jacob@hdreesen.de>

* Update docs/performance.md

Co-authored-by: Jacob Dreesen <jacob@hdreesen.de>

* typo

* musl

* musl fixes

* add log section

* french translation

* typo

---------

Co-authored-by: Jacob Dreesen <jacob@hdreesen.de>
2024-08-27 11:45:56 +02:00
Kévin Dunglas
a16076e082 perf: prevent useless logger allocs 2024-08-27 11:03:01 +02:00
Alexander Stecher
f5bec5c13c fix(caddy): resolve_root_symlink not taken into account (#1001)
* Prevents parsing from always throwing an error.

* Removes empty line.

* Accepts suggestions.

* Accepts suggestions.

* Fixes syntax.

* Fixes formatting.

* chore: use a guard close

---------

Co-authored-by: a.stecher <a.stecher@sportradar.com>
Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
2024-08-26 20:30:42 +02:00
Kévin Dunglas
db12b4e113 perf: cache document root resolution when possible 2024-08-26 20:26:27 +02:00
Kévin Dunglas
4a8555571c docs: fix build/curl instructions in more languages 2024-08-24 08:30:00 +02:00
David Legrand
d12551762f docs: fix build/curl instructions (#998)
The `z` was missing and there was an error after download:

```
tar: Archive is compressed. Use -z option
tar: Error is not recoverable: exiting now
```
2024-08-23 16:07:42 +02:00
Kévin Dunglas
27ca1ae4f7 feat(static): add HTTP/2 support for ext-curl 2024-08-20 16:38:53 +02:00
Kévin Dunglas
60e3aba981 docs: fix and improve compilation docs 2024-08-20 09:55:33 +02:00
Kévin Dunglas
7a524ddbd5 ci: add back -Wall -Werror 2024-08-20 01:36:26 +02:00
Kévin Dunglas
ac37760e37 docs: create SECURITY.md 2024-08-13 16:08:59 +02:00
Kévin Dunglas
496831329f ci: run tests with PHP 8.4 2024-08-13 09:46:54 +02:00
Kévin Dunglas
ba58e3d829 chore: prepare release 1.2.4 2024-08-12 10:05:10 +02:00
Rob Landers
d532772355 fix: reset sapi response code (#964)
* fix: reset sapi response code
It turns out that the sapi response code is NOT reset between requests by the zend engine. This resets the code for cgi-based requests.
fixes: #960

* update response header test

* fix assertion

* appears to affect workers too
2024-08-11 22:34:50 +02:00
Kévin Dunglas
3ca52f5934 ci: generate SLSA attestations for static binaries 2024-08-09 22:47:50 +02:00
Kévin Dunglas
968176a948 ci: run tests with ASAN and MSAN (#955) 2024-08-09 18:18:15 +02:00
Kévin Dunglas
2af8fd2e31 chore: remove useless cgo directive 2024-08-06 22:57:04 +02:00
Kévin Dunglas
26cfcc145a chore: prepare release 1.2.3 2024-08-05 23:27:19 +02:00
Kévin Dunglas
fd97c977c1 chore: bump deps (#954) 2024-08-05 15:48:25 +02:00
Kévin Dunglas
6c708be99d ci: upgrade to super-linter 6 (#952) 2024-08-04 14:05:54 +02:00
Kévin Dunglas
bcc825a121 ci: switch to super-linter slim variant 2024-08-02 17:33:18 +02:00
Kévin Dunglas
4de9abb49d chore: bump deps 2024-08-02 12:19:03 +02:00
Alexander Makarov
93859e3149 docs: fix assorted typos (#942) 2024-07-27 00:07:52 +02:00
Rob Landers
fb23c64632 perf: cgi-mode 1700% improvement (#933)
* major perf

* clean up before thread returns

* fix lint

* clean up and refactor server-context initialization

* removing the request-startup memset
I'm electing to remove the memset at startup and keeping it in shutdown. Why? Security! Keeping around the request data after a request may result in a leak of important information in a core dump or something. By setting it to zero when we shutdown the request, we can ensure no information is left laying around
2024-07-26 09:22:08 +02:00
Jerry Ma
323edefc4b chore: add prefer-pre-built option for spc download (#921) 2024-07-14 18:13:22 +02:00
Fabien Papet
a6572225f6 docs: fix port number in French version (#919) 2024-07-12 10:55:21 +02:00
Kévin Dunglas
6d5cb37647 chore: prepare release 1.2.2 2024-07-11 14:09:52 +02:00
Kévin Dunglas
0751f453b9 fix: create a custom Go build when using musl to prevent a crash (#913) 2024-07-11 10:26:28 +02:00
Kévin Dunglas
4fab5a3169 docs: fix php.ini path for static binaries 2024-07-10 14:08:11 +02:00
Kévin Dunglas
e743f6ab87 docs: customizing the configuration (#911)
* docs: customizing the configuration

* Update docs/config.md

Co-authored-by: Jacob Dreesen <jacob@hdreesen.de>

* Update docs/fr/config.md

Co-authored-by: Jacob Dreesen <jacob@hdreesen.de>

* Update embed.md

---------

Co-authored-by: Jacob Dreesen <jacob@hdreesen.de>
2024-07-09 16:13:55 +02:00
Kévin Dunglas
0500ebc191 perf: improve PHP thread management (#898) 2024-07-09 09:39:03 +02:00
Kévin Dunglas
b87cf4e8b9 chore: bump deps 2024-07-08 16:21:06 +02:00
Kévin Dunglas
ebdb2656b6 fix: deprecated Dockerfile ENV syntax 2024-07-05 11:34:47 +02:00
Kévin Dunglas
29d47f42c8 chore: switch back to upstream Static PHP CLI 2024-07-05 11:12:34 +02:00
Kévin Dunglas
ae4ebd11f6 fix: downgrade to Go 1.22.4 for Alpine builds 2024-07-03 17:02:37 +02:00
Kévin Dunglas
8ff6cfdda8 refactor: prevent a useless allocation (#895)
* refactor: prevent a useless allocation

* cs
2024-06-28 16:46:34 +02:00
Kevin Detournay
952dd7a79b docs: use octane:frankenphp instead of octane:start (#893)
to be more consistent with OFFICIAL laravel documentation

(basically octane:start comment , will
check your env octane server and call octane:frankenphp )

Co-authored-by: kevin <kevin@popsell.com>
2024-06-28 12:11:43 +02:00
Kévin Dunglas
213be22967 docs: fix linter 2024-06-27 14:10:51 +02:00
Simon
11e3745b8b docs: explain how to fix SSL/TLS-related issues (#888)
* mail tls issues documentation

* fix linting

* Update known-issues.md

* Update known-issues.md

* Update known-issues.md

---------

Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
2024-06-27 13:49:16 +02:00
Kévin Dunglas
153e7d6686 chore: simplify log config (#887)
* chore: simplify log config

* review
2024-06-25 23:13:57 +02:00
Kévin Dunglas
a313f3a809 chore: prepare release 1.2.1 2024-06-24 17:37:19 +02:00
Kévin Dunglas
e45a788824 fix(caddy): incorrectly prepared environment variables when not using Caddyfile 2024-06-24 17:36:06 +02:00
Kévin Dunglas
549239d16f chore: bump deps 2024-06-24 13:57:41 +02:00
Kévin Dunglas
b47f4d3aa0 fix(static): unbundle parallel extension 2024-06-24 13:53:32 +02:00
Kévin Dunglas
d6d1b2731c feat: add support for max_input_time in worker mode (#874) 2024-06-18 12:00:01 +02:00
dependabot[bot]
e0ccd816e6 ci: bump docker/bake-action from 4 to 5
Bumps [docker/bake-action](https://github.com/docker/bake-action) from 4 to 5.
- [Release notes](https://github.com/docker/bake-action/releases)
- [Commits](https://github.com/docker/bake-action/compare/v4...v5)

---
updated-dependencies:
- dependency-name: docker/bake-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-17 14:42:51 +02:00
dependabot[bot]
039d021f51 chore(caddy): bump github.com/spf13/cobra from 1.8.0 to 1.8.1 in /caddy
Bumps [github.com/spf13/cobra](https://github.com/spf13/cobra) from 1.8.0 to 1.8.1.
- [Release notes](https://github.com/spf13/cobra/releases)
- [Commits](https://github.com/spf13/cobra/compare/v1.8.0...v1.8.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-17 14:42:21 +02:00
Benedikt Franke
6b44b532f5 docs: improve Docker tags documentation (#866) 2024-06-12 15:17:59 +02:00
Tim Düsterhus
c0c56a8cf8 Fix typo in code sample in worker.md 2024-06-11 11:02:06 +02:00
dependabot[bot]
d99ce659f6 chore: bump golang.org/x/net from 0.25.0 to 0.26.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.25.0 to 0.26.0.
- [Commits](https://github.com/golang/net/compare/v0.25.0...v0.26.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-10 13:30:22 +02:00
Kévin Dunglas
c4aba6be02 chore: uniformize C comment style 2024-06-09 12:13:15 +02:00
Kévin Dunglas
4537f27f67 fix: prevent crash when worker terminates after a file upload 2024-06-08 13:36:32 +02:00
DubbleClick
acf6d0ffc4 ci: add -lstdc++ to CGO_LDFLAGS if a C++ extension is enabled (#855) 2024-06-08 13:35:44 +02:00
DubbleClick
ce4732aa43 docs: fix worker example (#856)
* Update worker.md documentation if MAX_REQUESTS is not set

* Update worker.md
2024-06-08 13:06:40 +02:00
Kévin Dunglas
68c0a4c246 chore: prepare release 1.2.0 2024-06-05 15:27:02 +02:00
Kévin Dunglas
74e9a3c9e5 chore: upgrade to Caddy 2.8.4 2024-06-05 15:24:36 +02:00
Kévin Dunglas
3714fdf3a1 fix: superglobals-realated crash with custom extensions in worker mode (#796)
* test: failing test reproducing #767

* fix

* Update frankenphp.c

Co-authored-by: Tim Düsterhus <timwolla@googlemail.com>

* Update frankenphp.c

Co-authored-by: Tim Düsterhus <timwolla@googlemail.com>

* review

* ZVAL_COPY

* fix

* add back current $_SERVER behavior

* add docs

* bad fix for the leak

* clean test

* improve tests

* fix test

* fix

* cleanup

* clarify destroy super globals name

* micro-optim: use zval_ptr_dtor_nogc to destroy super globals

* style

* fix

* better name for frankenphp_free_server_context

* more cleanup

* remove useless memset

* more cleanup

* continue refactoring

* fix and update docs

* docs

---------

Co-authored-by: Tim Düsterhus <timwolla@googlemail.com>
2024-06-05 15:24:16 +02:00
Kévin Dunglas
0b4a427cac feat: use the new RegisterDirectiveOrder to simplify config 2024-05-31 17:47:24 +02:00
Rob Landers
b96db939b7 feat: option to enable full duplex for HTTP/1 connections (#692)
* found another spot that was preventing http1 writes

* remove full-duplex from caddyfile

* update documentation

* fix: update http name

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

* fix: update http name

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

* fix: names

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

* fix: update caddyfile name

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

* Add caution to docs

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

* Update config.md

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

* fix lint

---------

Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
2024-05-31 14:38:31 +02:00
jeremyj11
ad2c18a6b7 docs: add missing -z to tar (#828) 2024-05-30 16:47:15 +02:00
Kévin Dunglas
2183f29b18 feat: Caddy 2.8 support 2024-05-30 13:59:44 +02:00
Kévin Dunglas
90a7b98b10 feat: log the number of threads and workers during startup 2024-05-30 11:15:30 +02:00
Jerry Ma
322e45c186 fix: support libxml with latest static-php-cli 2024-05-30 11:15:12 +02:00
Stephen Miracle
da342b6f2f docs: recommend FrankenWP for WordPress (#785)
* updating reference location to provide WordPress implementation supprt.

* correcting autoformat issue from previous commit. Should now only be updating wordpress references.
2024-05-27 13:29:16 +02:00
Kévin Dunglas
3d065eda35 $_SERVER['FRANKENPHP_WORKER'] must not be NULL-terminated 2024-05-21 18:50:02 +02:00
Kévin Dunglas
c894a92135 ci: load setup-php debug symbols 2024-05-21 11:12:30 +02:00
Kévin Dunglas
835ad8acb2 ci: cleanup static build workflows 2024-05-17 16:14:22 +02:00
Kévin Dunglas
73e9b640d6 fix: skip installing Buildx when possible 2024-05-16 15:59:41 +02:00
Kévin Dunglas
d01733dd3e docs: better Mercure hub schema 2024-05-16 14:42:30 +02:00
Kévin Dunglas
f773c1f529 ci: clean Docker tags 2024-05-15 14:23:36 +02:00
Kévin Dunglas
469070ce85 chore: prepare release 1.1.5 2024-05-13 15:37:01 +02:00
Kévin Dunglas
ea83ea6dbd chore: bump deps 2024-05-13 15:35:26 +02:00
Kévin Dunglas
a2f0eb9140 ci: remove SHA tag for non-dev Docker images (#781)
* ci: remove SHA tag for non-dev Docker images

* docs: Docker variants
2024-05-13 14:52:13 +02:00
dependabot[bot]
e5fcea0690 ci: bump golangci/golangci-lint-action from 5 to 6
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 5 to 6.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/v5...v6)

---
updated-dependencies:
- dependency-name: golangci/golangci-lint-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-13 13:52:15 +02:00
Kévin Dunglas
cd2049f611 docs: rootless Docker images with no capabilities 2024-05-13 11:11:52 +02:00
Kévin Dunglas
3dbb3fd48d feat(static): add ftp, gettext, gmp, imagick, mbregex, parallel, protobuf, shmop, soap, ssh2, sysmsg, sysvshm, tidy, xlswriter, yaml and zstd extensions (#773) 2024-05-11 15:43:00 +02:00
dependabot[bot]
627f817b59 ci: bump golangci/golangci-lint-action from 4 to 5
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 4 to 5.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/v4...v5)

---
updated-dependencies:
- dependency-name: golangci/golangci-lint-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-09 21:46:45 +02:00
Kévin Dunglas
ea5e19ff4b fix: getallheaders() must return request headers (#772)
* fix: getallheaders() must return request headers

* cs
2024-05-08 11:43:39 +02:00
Kévin Dunglas
12fb11eead docs: embedding Laravel apps (#753)
* docs: embedding Laravel apps

* fix

* docs: embedding Octane apps

* fix

* fix

* cs

* cs

* fix md

* path explaination

* changing the storage path
2024-04-29 17:42:18 +02:00
Kévin Dunglas
25a858954c fix: temporary directory name for embed apps 2024-04-28 10:45:38 +02:00
Kévin Dunglas
593233db17 fix: DOMdocument not found when building embedded apps 2024-04-27 03:17:28 +02:00
Kévin Dunglas
fc76447cad chore: prepare release 1.1.4 2024-04-25 11:35:32 +02:00
Kévin Dunglas
ac2dd4ab56 ci: fix linux/amd64 static pipeline 2024-04-24 17:18:58 +02:00
Benjamin Eberlei
5d68a3c5e5 docs: Tideways now supports FrankenPHP in normal and worker mode (#745)
* Tideways now supports FrankenPHP worker mode

See https://support.tideways.com/documentation/setup/installation/frankenphp.html

* Update fr/known-issues.md

* Update tr/known-issues.md
2024-04-24 17:07:14 +02:00
Kévin Dunglas
6597b71f52 ci: fix static pipeline 2024-04-24 11:42:25 +02:00
Kévin Dunglas
fe7d69d01b ci: fix mimalloc pipeline 2024-04-24 00:40:31 +02:00
Kévin Dunglas
977cad0314 ci: fix debug and mimalloc pipeline 2024-04-23 21:34:09 +02:00
Kévin Dunglas
60b5a11e5a ci: fix mimalloc pipeline 2024-04-23 19:29:28 +02:00
Kévin Dunglas
a9ebc3aeea chore: prepare release 1.1.3 2024-04-23 14:50:00 +02:00
dependabot[bot]
4a97a40f4a chore: bump golang.org/x/net from 0.22.0 to 0.24.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.22.0 to 0.24.0.
- [Commits](https://github.com/golang/net/compare/v0.22.0...v0.24.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-23 14:35:38 +02:00
Kévin Dunglas
404086d4d5 feat: autodetect extensions to build for embedded apps (#717)
* feat: autodetect extensions to build for embeded apps

* fix: ext-libxml support
2024-04-23 14:04:47 +02:00
Kévin Dunglas
498294a561 feat: option to use mimalloc for static builds (#666)
* feat: use mimalloc for static builds

* fix: use Tweag's approach

* fix: debug build

* chore: mark USE_MIMALLOC as experimental

* ci: build a static binary using mimalloc
2024-04-23 14:04:25 +02:00
Kévin Dunglas
06f7c9448f chore: bump deps 2024-04-23 12:08:48 +02:00
Kévin Dunglas
85b8a8c805 fix(static): ensure that FRANKENPHP_VERSION is defined in child Docker images 2024-04-22 11:35:51 +02:00
Pierre
03c0247ae5 docs: translate the contribution guide in Chinese (#739)
Co-authored-by: Pierre Clavequin <pierre.clavequin@valueapex.com>
2024-04-22 10:49:21 +02:00
Kévin Dunglas
238a6ebe9f docs: remove Xdebug from the list of buggy extensions 2024-04-17 17:35:01 +02:00
Kévin Dunglas
5d1289cc0d feat: add an option to not compress the static binary 2024-04-16 09:27:46 +02:00
Michael Schubert
5cb5d0b8f1 docs: fix typo
`inlcuidng` -> `including`
2024-04-14 19:55:38 +02:00
Kévin Dunglas
a910e39b06 chore: enable the verbose mode of Static PHP CLI 2024-04-11 17:11:47 +02:00
Rob Landers
1abd549eb7 limit concurrency for static/docker builds (#711) 2024-04-09 23:11:21 +02:00
verytrap
15a600cdaa chore: fix function name in comment
Signed-off-by: verytrap <wangqiuyue@outlook.com>
2024-04-09 16:45:12 +02:00
Victor
ee05142582 docs: Added missing slash in multiline command. 2024-03-30 09:01:38 +01:00
Shyim
6f69939b0d docs: fix shebang for Composer workaround script (#696) 2024-03-30 08:59:59 +01:00
Kévin Dunglas
e7e0dbfa3d chore: prepare release 1.1.2 2024-03-24 20:35:19 +01:00
Rob Landers
e127cf5e1c update test (#688)
This makes the file size 6mb-ish, more than the 2mb batching that PHP does. I verified this fails on 0e163a0 (main prior to #686).
2024-03-24 16:28:15 +01:00
Rob Landers
d973206174 fix reading post bodies (#686)
Now that https://github.com/golang/go/issues/15527 is supposedly fixed, this condition should be no-longer needed. Further, if php didn't request enough bytes, this condition would be hit. It appears PHP requests chunks ~2mb in size at a time.
2024-03-24 12:18:46 +01:00
Ruben Kruiswijk
0e163a0a75 docs: mark New Relic PHP Agent as incompatible (#675) 2024-03-19 12:02:11 +01:00
Kévin Dunglas
eed3a019a6 chore: prepare release 1.1.1 2024-03-18 19:39:51 +01:00
Kévin Dunglas
40924d2996 chore: bump deps 2024-03-18 19:39:14 +01:00
Kévin Dunglas
2426a2fff7 docs: fix translation intructions 2024-03-18 11:54:24 +01:00
Kévin Dunglas
ba33754ea4 docs(fr): add known issues 2024-03-18 11:54:24 +01:00
Kévin Dunglas
c11488f99d docs: update known issues 2024-03-18 11:54:24 +01:00
Mert Simsek
6a3db9429d docs: Turkish translation (#654)
* Add Turkish translated docs files

---------

Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
2024-03-13 22:01:06 +01:00
Kévin Dunglas
7b4f34d7da docs: how to translate the docs 2024-03-13 22:00:00 +01:00
Kévin Dunglas
f182eba6f0 ci: prevent some useless runs when only docs are updated 2024-03-13 21:59:40 +01:00
Kévin Dunglas
b18a079eb9 perf: hint the number of threads to TSRM (#655)
* perf: hint the number of threads to TSRM

* fix: PHP 8.2 compat
2024-03-13 18:28:07 +01:00
Kévin Dunglas
71661c45e2 fix: crash when a write error occurs (#651) 2024-03-13 17:57:23 +01:00
Kévin Dunglas
408cc5fb5a ci: re-add non-debug builds to the matrix (#656) 2024-03-13 17:38:13 +01:00
Kévin Dunglas
822f80829e chore: bump deps 2024-03-12 22:20:16 +01:00
Kévin Dunglas
1511decad6 chore: some CS changes 2024-03-12 22:20:04 +01:00
Kévin Dunglas
07a74e5c5a perf: reduce allocs when creating $_SERVER (#540)
* perf: reduce allocs when creating $_SERVER

* improve

* refactor: prevent C allocs when populating $_SERVER

* cs

* remove append()

* simplify

* wip

* cleanup

* add cache

* cleanup otter init

* some fixes

* cleanup

* test with a leak

* remove const?

* add const

* wip

* wip

* allocate dynamic variables in Go memory

* cleanup

* typo

* bump otter

* chore: bump deps
2024-03-12 18:31:30 +01:00
Kévin Dunglas
9a88401b03 ci: fix upload debug builds 2024-03-11 18:03:10 +01:00
Jerry Ma
83aaa0977f ci: prevent failures when using custom libs for static binaries (#642)
* Fix Chinese docs to make it more readable

* prevent build-static.sh build with custom libs failure

* make libs brotli as required, others as optional
2024-03-11 16:37:58 +01:00
Silvio Ney
68b1d6f632 Update compile.md 2024-03-10 23:08:53 +01:00
Kévin Dunglas
e0531fa17c ci: build static binary with debug symbols 2024-03-09 00:33:11 +01:00
bofalke
7a81855f12 docs: warn about .dockerignore when embedding (#609)
* Add notice for .dockerignore issues in embed.md

* Add french notice for .dockerignore issues in embed.md

* Add chinese notice for .dockerignore issues in embed.md

* Fix whitespaces in documentation

* Apply suggestions for translations

Co-authored-by: Jerry Ma <jesse2061@outlook.com>
Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>

---------

Co-authored-by: Jerry Ma <jesse2061@outlook.com>
Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
2024-03-06 07:30:59 +01:00
flexponsive
90eaa3a680 docs: more robust workaround for -d command line params (#613)
* more robust workaround for -d command line params

unfortunately there were some corner cases (like first parameter not equal to -d) that are difficult to handle with sh. added a more robust workaround script (which requires bash) and made clear we have to set the PHP_BINARY env variable for this to work.

* Remove echo
2024-03-04 22:39:17 +01:00
Stephen Miracle
ecd7e4d5f2 docs: adding cbroti requirement for custom caddy build. (#626)
* adding cbroti requirement for custom caddy build.

* Update docker.md

* Update docker.md

---------

Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
2024-03-04 22:36:40 +01:00
Kévin Dunglas
14d1fd93a4 docs: remove the fixed Datadog know issue 2024-03-04 18:42:01 +01:00
Kévin Dunglas
2d87fdaf0d more deps 2024-03-04 18:40:42 +01:00
Kévin Dunglas
1768f8b073 chore: bump deps 2024-03-04 18:40:42 +01:00
Shin
d36a80c76e docs: remove unnecessary parenthesis
I'm sorry, there was still another unnecessary parenthesis in the link to the issue page.
2024-03-04 09:20:39 +01:00
Shin
3af07894d8 docs: remove unnecessary parenthesis
I removed an unnecessary parenthesis in the link to the issue page.
2024-03-04 08:04:29 +01:00
Steve Oliver
06dbc988b6 docs: fix --http-redirect option code formating (#622)
Adds missing tick marks around http-redirect option.
2024-03-03 15:25:31 +01:00
Jerry Ma
7f32ab6404 docs: fix Chinese docs to make it more readable (#615) 2024-03-01 09:40:27 +01:00
Kévin Dunglas
f5af2a2e87 docs: add more known issues and improve bug template 2024-02-28 22:14:37 +01:00
flexponsive
c9d33b981b docs: workaround to use PHP in Composer scripts (#610)
* add @php workaround to known-issues.md

* minor tweaks

* switch to sh

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

* Update known-issues.md: sh compat

* change language name

---------

Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
2024-02-28 18:17:14 +01:00
Pierre
2c6e201ea6 docs: Chinese translation (#597)
* docs: translated in Chinese

* fix: readme md links and duplicate images

* docs: lint and remove outdated parts

---------

Co-authored-by: Pierre Clavequin <pierre.clavequin@valueapex.com>
Co-authored-by: ginifizz <laury@les-tilleuls.coop>
Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
2024-02-28 15:29:20 +01:00
Kévin Dunglas
b71dae9b03 fix: prevent crash when calling apache_request_headers() in non-HTTP context 2024-02-27 17:51:10 +01:00
Laury S
963b3e0f59 docs: add FR translations and various EN improvements (#589)
* feat: add fr doc (#1)

* fix: fr translations

* fix: linter

* docs: various improvements

* fix: md links on readme fr

* fix: remove duplicate images

---------

Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
2024-02-27 17:21:48 +01:00
Kévin Dunglas
a6fc22505c feat: compress binary in Alpine with UPX 2024-02-20 19:38:03 +01:00
dependabot[bot]
c00a011221 ci: bump golangci/golangci-lint-action from 3 to 4
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 3 to 4.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: golangci/golangci-lint-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-13 05:18:19 +01:00
Kévin Dunglas
d1a6e38438 chore: reset stats in benchmarks 2024-02-12 14:07:11 +01:00
Kévin Dunglas
36b752d0a6 feat: compress Linux binaries with UPX (#572) 2024-02-12 10:01:27 +01:00
Kévin Dunglas
feaa950d89 feat: compile with Go 1.22 (#568) 2024-02-12 10:00:46 +01:00
Kévin Dunglas
f90e4614b6 ci: fix the Mac build (#569) 2024-02-11 22:53:21 +01:00
Hemanth Bollamreddi
f152a5fdaf docs: fix rootless Docker example (#565)
* Fix docs for running docker in non root mode

* Fix comments

* minor fixes

---------

Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
2024-02-11 11:33:08 +01:00
Holger Dörner
b60fc5c374 Make md5 binary OS dependent 2024-02-09 23:48:43 +01:00
Kévin Dunglas
5da805d9ee ci: use Apple Silicon machines when useful (#550)
* ci: add Apple Silicon build

* ci: let the CI build Apple Silicon binaries
2024-02-04 18:25:04 +01:00
Kévin Dunglas
a996c2ee28 chore: prepare release 1.1.0 2024-02-03 12:32:48 +01:00
Kévin Dunglas
4cb77b552d ci: add more headers to the k6 load test (#544) 2024-02-03 12:25:35 +01:00
Kévin Dunglas
241ca55d7a feat: automatically import environment variables in $_SERVER 2024-02-03 12:25:18 +01:00
Kévin Dunglas
ae958516ea feat: enable resolve_root_symlink by default (#546)
* feat: enable resolve_root_symlink by default

* Update docs/config.md

Co-authored-by: Francis Lavoie <lavofr@gmail.com>

* fix init

---------

Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2024-02-02 15:58:33 +01:00
Kévin Dunglas
b61900eae1 ci: fix push or Docker dev images 2024-02-02 12:21:59 +01:00
Kévin Dunglas
e53b1ce891 ci: use master Buildx version 2024-02-01 23:51:08 +01:00
Kévin Dunglas
6dee113a01 perf: add $_SERVER creation benchmark 2024-02-01 11:40:36 +01:00
Kévin Dunglas
fe7e9e7c79 fix: crash when using apache_request_headers() (#536)
* fix: potential crash when using apache_request_headers()

* refactor: better apache_request_headers
2024-02-01 10:00:11 +01:00
Kévin Dunglas
ab7ce9cb18 ci: temporary fix for Buildx crash 2024-01-31 16:29:45 +01:00
Kévin Dunglas
5afde55ebf ci: debug Docker buildx crash 2024-01-31 15:59:54 +01:00
Kévin Dunglas
da63e700b0 ci: fix scheduled Docker images build (ctd) 2024-01-31 15:21:03 +01:00
Kévin Dunglas
5a8e5f9518 feat: add apache_response_headers() function (#530) 2024-01-31 12:34:30 +01:00
Kévin Dunglas
3d9f344a50 docs: Docker image updates and tags 2024-01-30 22:07:33 +01:00
Kévin Dunglas
175b9a0296 feat: add Brotli compression support (#524) 2024-01-30 18:33:59 +01:00
Kévin Dunglas
65c8720250 ci: push dev Docker images to a dedicated repository 2024-01-30 18:17:02 +01:00
Kévin Dunglas
59dd04234f ci: fix scheduled Docker images build 2024-01-30 15:05:24 +01:00
Kévin Dunglas
36a6daa8ba docs: use tabs instead of spaces in Dockerfiles 2024-01-29 17:17:34 +01:00
Kévin Dunglas
85b36acce1 chore: comment logs directive 2024-01-29 17:17:12 +01:00
Kévin Dunglas
d9618ac917 chore: bump deps 2024-01-29 17:17:12 +01:00
Kévin Dunglas
be08c1e717 docs: run as non-root in Docker containers 2024-01-29 14:40:56 +01:00
Kévin Dunglas
62d53253d1 ci: add a bug report form 2024-01-29 14:08:16 +01:00
Leo Cavalcante
f35e276ee6 docs: fix worker mode Docker example (#516) 2024-01-29 14:06:27 +01:00
Leo Cavalcante
a736a5a2ce fix: adding extra libs to the static build (#517) 2024-01-29 13:59:46 +01:00
Rob Landers
c71e55551c add an issue template (#515) 2024-01-27 16:04:01 +01:00
Kévin Dunglas
7454826d4b ci: use cgocheck2 when possible 2024-01-23 23:14:24 +01:00
Kévin Dunglas
49baf02035 feat: add go_apache_request_headers() 2024-01-23 23:14:24 +01:00
Pierre du Plessis
a92d774742 fix: check for custom php.ini before checking for the Caddyfile (#498) 2024-01-22 22:26:20 +01:00
Pierre du Plessis
effb5805f1 feat: allow custom php.ini file in embedded apps (#497) 2024-01-22 14:01:27 +01:00
Pierre du Plessis
5d32f32639 feat: option to start a Mercure.rocks hub when running php-server (#489)
---------

Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
2024-01-22 13:35:19 +01:00
Pierre du Plessis
bccbaafc84 feat: load custom Caddyfile from embedded app if it exists (#494)
---------

Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
2024-01-22 13:31:12 +01:00
Pierre du Plessis
6f108a4203 fix: do not extract embedded app on every execution (#488)
* Do not extract embedded app on every execution

* Add app_checksum.txt to .dockerignore

* Rename embeddedAppHash to embeddedAppChecksum

* Remove check for empty directory
2024-01-21 18:13:08 +01:00
Kévin Dunglas
3bdd6fd072 docs: improved rendering of Laravel Octane options (#479) 2024-01-20 13:31:32 +01:00
Eduardo Rocha
183451a13f Add note about musl libc (#485)
* Add note about musl libc

* Update known-issues.md

* Update known-issues.md

---------

Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
2024-01-20 13:29:40 +01:00
Jochen
2ef7762068 docs: PHP configuration guide (#486)
* docs: php configuration guide

* Update config.md

---------

Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
2024-01-20 13:19:45 +01:00
Kévin Dunglas
b624a13430 docs: improve HTTPS documentation (#480) 2024-01-20 11:49:16 +01:00
Kévin Dunglas
00b1d0e4b6 docs: add production deployment guide (#471)
* docs: update README and add guide for deploying FrankenPHP on Digital Ocean

* Update docs/deploy-frankenphp-digital-ocean.md

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

* Update docs/deploy-frankenphp-digital-ocean.md

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

* rewrite

* add images

* linter

* missing dot

---------

Co-authored-by: Trusted97 <benux9700@gmail.com>
Co-authored-by: Gianluca <37697178+Trusted97@users.noreply.github.com>
2024-01-12 16:53:50 +01:00
Kévin Dunglas
d4c313f3db ci: fix static binary copy (#451)
* ci: fix static binary copy

* Update static.yaml

Co-authored-by: Rob Landers <landers.robert@gmail.com>

* fix

* fix

* fix

---------

Co-authored-by: Rob Landers <landers.robert@gmail.com>
2024-01-08 12:03:36 +01:00
Mr Alexandre J-S William ELISÉ
f19c153d06 docs: update worker.md to prevent infinite loop (#455)
* Update worker.md to prevent infinite loop

Quick fix typo `<=` rather than `>=` which prevents infinite loop and get out of the loop when MAX_REQUESTS is reached.

* Update worker.md to fix impossible evaluation

Fix impossible case where if MAX_REQUESTS is not defined the rest of the condition is not evaluated as it is an && operator as mentioned by https://github.com/dunglas/frankenphp/discussions/453#discussioncomment-7981535

* Update worker.md change to for loop and remove handler from the loop

Change to for loop and remove handler from the loop based on the feedback https://github.com/dunglas/frankenphp/pull/455#discussion_r1438918811
2024-01-08 08:17:08 +01:00
naoki kuroda
3692818d0c perf: optimize cookie handling in request (#459)
Signed-off-by: nnnkkk7 <kurodanaoki0711pana@gmail.com>
2024-01-08 08:14:49 +01:00
Rob Landers
7830aae549 Ensure we don't modify a shared env (#452)
* Ensure we don't modify a shared env

* Update worker.go

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

* Update worker.go

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

---------

Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
2023-12-31 20:02:11 +01:00
Rob Landers
2055142904 apply just an optimization (#450) 2023-12-30 11:21:21 +01:00
Kévin Dunglas
9b9957f5cf chore: prepare release 1.0.3 2023-12-29 22:50:56 +01:00
Rob Landers
5bda50cbd7 Fix memory leak (#442)
* do not use caddy context

* ensure all handles are cleaned up

* do not export types

* just panic when double deleting a handle

* set the minimal capacity

* remove micro-opt

* move handle cleanup to just before we return from serveHttp

* ensure we clean up cli scripts

* handle cgi-mode free

* micro-optimization: set initial capacity
2023-12-28 00:16:19 +01:00
Kévin Dunglas
11711bb6da chore: prepare release 1.0.2 2023-12-25 17:12:41 +01:00
Kévin Dunglas
497876443c ci: fix static image inspect 2023-12-25 10:20:28 +01:00
Kévin Dunglas
94776119bd ci: fix static image inspect 2023-12-25 02:02:34 +01:00
Kévin Dunglas
229cb9e3e6 ci: fix static tagged builds 2023-12-24 12:35:55 +01:00
Kévin Dunglas
67fdefc416 ci: build Linux aarch64 binaries (#432) 2023-12-24 11:35:59 +01:00
Kévin Dunglas
e962f4394f ci: fix Docker builds 2023-12-24 11:34:48 +01:00
Kévin Dunglas
0054b92115 docs: link to TYPO3 skeleton 2023-12-23 21:39:48 +01:00
Kévin Dunglas
34e4ef1e84 ci: fix scheduled builds 2023-12-23 15:02:09 +01:00
Kévin Dunglas
9bf991ca88 ci: add igbinary extension and LZ4 support to static builds 2023-12-23 14:22:06 +01:00
Kévin Dunglas
64672a267a chore: bump deps (#429) 2023-12-22 16:54:05 +01:00
Kévin Dunglas
f16cb0b5f0 ci: allow building manually (#427) 2023-12-22 16:53:42 +01:00
Kévin Dunglas
efa49848e4 ci: remove armv6 Debian images (#431) 2023-12-22 16:45:51 +01:00
Kévin Dunglas
2eabec8c92 ci: fix static binary release 2023-12-20 19:44:48 +01:00
thecaliskan
f6873efee4 Added Laravel Octane Documentation (#422)
* Added Laravel Octane Documentation

* fixed lint

---------

Co-authored-by: Emre Çalışkan <emre.caliskan@beyn.com.tr>
2023-12-20 18:07:24 +01:00
Kévin Dunglas
47ada94c41 simplify 2023-12-20 15:53:46 +01:00
Kévin Dunglas
a571d990ec ci: add sodium to static binaries 2023-12-20 15:53:31 +01:00
Rafael Kassner
b7c8d4cd49 Add ctype and session PHP extensions to static builder 2023-12-20 00:13:27 +01:00
Kévin Dunglas
7c5f18fe3f ci: fix changelog generation 2023-12-19 14:20:34 +01:00
Kévin Dunglas
0015493543 chore: prepare release 1.0.1 2023-12-18 19:39:47 +01:00
Kévin Dunglas
3709c2a50b chore: switch to upstream C-Thread-Pool 2023-12-18 18:50:50 +01:00
Kévin Dunglas
8dd22269dc ci: downgrade to GitHub Actions artifact v1 (#406) 2023-12-18 17:16:30 +01:00
Kévin Dunglas
f71f9875ca perf: add benchmark (#392) 2023-12-18 15:35:44 +01:00
Kévin Dunglas
184b86c6e3 ci: upgrade upload-artifact action 2023-12-18 15:35:18 +01:00
369 changed files with 47805 additions and 4930 deletions

1
.clang-format-ignore Normal file
View File

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

View File

@@ -1,13 +1,11 @@
# ignored
**/*
# authorized
!**/Caddyfile
!**/*.go
!**/go.*
!**/*.c
!**/*.h
!testdata/*.php
!testdata/*.txt
!build-static.sh
!app.tar
/caddy/frankenphp/frankenphp
/internal/testserver/testserver
/internal/testcli/testcli
/dist
.DS_Store
.idea/
.vscode/
__debug_bin
frankenphp.test
caddy/frankenphp/Build
*.log

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

91
.github/ISSUE_TEMPLATE/bug_report.yaml vendored Normal file
View File

@@ -0,0 +1,91 @@
---
name: Bug Report
description: File a bug report
labels: [bug]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report!
Before submitting a bug, please double-check that your problem [is not
a known issue](https://frankenphp.dev/docs/known-issues/)
(especially if you use XDebug or Tideways), and that is has not
[already been reported](https://github.com/php/frankenphp/issues).
- type: textarea
id: what-happened
attributes:
label: What happened?
description: |
Tell us what you do, what you get and what you expected.
Provide us with some step-by-step instructions to reproduce the issue.
validations:
required: true
- type: dropdown
id: build
attributes:
label: Build Type
description: What build of FrankenPHP do you use?
options:
- Docker (Debian Trixie)
- Docker (Debian Bookworm)
- Docker (Alpine)
- deb packages
- rpm packages
- Static binary
- Custom (tell us more in the description)
default: 0
validations:
required: true
- type: dropdown
id: worker
attributes:
label: Worker Mode
description: Does the problem happen only when using the worker mode?
options:
- "Yes"
- "No"
default: 0
validations:
required: true
- type: dropdown
id: os
attributes:
label: Operating System
description: What operating system are you executing FrankenPHP with?
options:
- GNU/Linux
- macOS
- Other (tell us more in the description)
default: 0
validations:
required: true
- type: dropdown
id: arch
attributes:
label: CPU Architecture
description: What CPU architecture are you using?
options:
- x86_64
- Apple Silicon
- x86
- aarch64
- Other (tell us more in the description)
default: 0
- type: textarea
id: php
attributes:
label: PHP configuration
description: |
Please copy and paste the output of the `phpinfo()` function -- remember to remove **sensitive information** like passwords, API keys, etc.
render: shell
validations:
required: true
- type: textarea
id: logs
attributes:
label: Relevant log output
description: |
Please copy and paste any relevant log output.
This will be automatically formatted into code,
so no need for backticks.
render: shell

View File

@@ -0,0 +1,20 @@
---
name: Feature Request
description: Suggest an idea for this project
labels: [enhancement]
body:
- type: textarea
id: description
attributes:
label: Describe your feature request
value: |
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is.
Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions
or features you've considered.

34
.github/actions/watcher/action.yaml vendored Normal file
View File

@@ -0,0 +1,34 @@
name: watcher
description: Install e-dant/watcher
runs:
using: composite
steps:
- name: Determine e-dant/watcher version
id: determine-watcher-version
run: echo version="$(gh release view --repo e-dant/watcher --json tagName --template '{{ .tagName }}')" >> "${GITHUB_OUTPUT}"
shell: bash
env:
GH_TOKEN: ${{ github.token }}
- name: Cache e-dant/watcher
id: cache-watcher
uses: actions/cache@v4
with:
path: watcher/target
key: watcher-${{ runner.os }}-${{ runner.arch }}-${{ steps.determine-watcher-version.outputs.version }}-${{ env.CC && env.CC || 'gcc' }}
- if: steps.cache-watcher.outputs.cache-hit != 'true'
name: Compile e-dant/watcher
run: |
mkdir watcher
gh release download --repo e-dant/watcher -A tar.gz -O - | tar -xz -C watcher --strip-components 1
cd watcher
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build
sudo cmake --install build --prefix target
shell: bash
env:
GH_TOKEN: ${{ github.token }}
- name: Update LD_LIBRARY_PATH
run: |
sudo sh -c "echo ${PWD}/watcher/target/lib > /etc/ld.so.conf.d/watcher.conf"
sudo ldconfig
shell: bash

View File

@@ -1,31 +1,39 @@
---
version: 2
updates:
-
package-ecosystem: gomod
- package-ecosystem: gomod
directory: /
schedule:
interval: weekly
commit-message:
prefix: chore
-
package-ecosystem: gomod
groups:
go-modules:
patterns:
- "*"
cooldown:
default-days: 7
- package-ecosystem: gomod
directory: /caddy
schedule:
interval: weekly
commit-message:
prefix: chore(caddy)
# These packages must be in sync with versions
# used by github.com/caddyserver/caddy/v2
ignore:
-
dependency-name: github.com/google/cel-go
-
dependency-name: github.com/quic-go/*
-
package-ecosystem: github-actions
groups:
go-modules:
patterns:
- "*"
cooldown:
default-days: 7
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
commit-message:
prefix: ci
groups:
github-actions:
patterns:
- "*"
cooldown:
default-days: 7

View File

@@ -1,72 +1,109 @@
---
name: Build Docker images
concurrency:
cancel-in-progress: true
group: ${{ github.workflow }}-${{ github.ref }}
on:
pull_request:
branches:
- main
paths:
- "docker-bake.hcl"
- ".github/workflows/docker.yaml"
- "**cgo.go"
- "**Dockerfile"
- "**.c"
- "**.h"
- "**.sh"
- "**.stub.php"
push:
branches:
- main
tags:
- v*.*.*
workflow_dispatch:
inputs: {}
inputs:
#checkov:skip=CKV_GHA_7
version:
description: "FrankenPHP version"
required: false
type: string
schedule:
- cron: '0 4 * * *'
- cron: "0 4 * * *"
permissions:
contents: read
env:
IMAGE_NAME: ${{ (github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && inputs.version) || startsWith(github.ref, 'refs/tags/')) && 'dunglas/frankenphp' || 'dunglas/frankenphp-dev' }}
jobs:
prepare:
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
outputs:
# Push if it's a scheduled job, a tag, or if we're committing to the main branch
push: ${{ toJson(github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/') || (github.ref == 'refs/heads/main' && github.event_name != 'pull_request')) }}
push: ${{ (github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && inputs.version) || startsWith(github.ref, 'refs/tags/') || (github.ref == 'refs/heads/main' && github.event_name != 'pull_request')) && true || false }}
variants: ${{ steps.matrix.outputs.variants }}
platforms: ${{ steps.matrix.outputs.platforms }}
metadata: ${{ steps.matrix.outputs.metadata }}
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 }}
ref: ${{ steps.check.outputs.ref || (github.event_name == 'workflow_dispatch' && inputs.version) || '' }}
steps:
-
name: Check PHP versions
- name: Check PHP versions
id: check
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
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="; "")')
echo php_version="${PHP_83_LATEST},${PHP_82_LATEST}" >> "${GITHUB_OUTPUT}"
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},${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
if [[ "${GITHUB_EVENT_NAME}" != "schedule" ]]; then
echo skip=false >> "${GITHUB_OUTPUT}"
exit 0
fi
FRANKENPHP_82_LATEST=$(skopeo inspect docker://docker.io/dunglas/frankenphp:latest-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:latest-php8.3 --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}" ]]; then
FRANKENPHP_LATEST_TAG=$(gh release view --repo php/frankenphp --json tagName --jq '.tagName')
FRANKENPHP_LATEST_TAG_NO_PREFIX="${FRANKENPHP_LATEST_TAG#v}"
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}" ]] && [[ "${FRANKENPHP_85_LATEST}" == "${PHP_85_LATEST}" ]]; then
echo skip=true >> "${GITHUB_OUTPUT}"
exit 0
fi
{
echo ref="$(gh release view --repo dunglas/frankenphp --json tagName --jq '.tagName')"
echo ref="${FRANKENPHP_LATEST_TAG}"
echo skip=false
} >> "${GITHUB_OUTPUT}"
-
uses: actions/checkout@v4
- uses: actions/checkout@v6
if: ${{ !fromJson(steps.check.outputs.skip) }}
with:
ref: ${{ steps.check.outputs.ref }}
-
name: Set up Docker Buildx
persist-credentials: false
- name: Set up Docker Buildx
if: ${{ !fromJson(steps.check.outputs.skip) }}
uses: docker/setup-buildx-action@v3
with:
version: latest
-
name: Create variants matrix
- name: Create variants matrix
if: ${{ !fromJson(steps.check.outputs.skip) }}
id: matrix
shell: bash
run: |
set -e
METADATA="$(docker buildx bake --print | jq -c)"
{
echo metadata="${METADATA}"
@@ -75,10 +112,10 @@ jobs:
} >> "${GITHUB_OUTPUT}"
env:
SHA: ${{ github.sha }}
VERSION: ${{ (github.ref_type == 'tag' && github.ref_name) || steps.check.outputs.ref || github.sha }}
VERSION: ${{ (github.ref_type == 'tag' && github.ref_name) || steps.check.outputs.ref || 'dev' }}
PHP_VERSION: ${{ steps.check.outputs.php_version }}
build:
runs-on: ubuntu-latest
runs-on: ${{ startsWith(matrix.platform, 'linux/arm') && 'ubuntu-24.04-arm' || 'ubuntu-24.04' }}
needs:
- prepare
if: ${{ !fromJson(needs.prepare.outputs.skip) }}
@@ -89,105 +126,117 @@ jobs:
platform: ${{ fromJson(needs.prepare.outputs.platforms) }}
include:
- race: ""
qemu: true
- platform: linux/amd64
qemu: false
race: "-race" # The Go race detector is only supported on amd64
- platform: linux/386
qemu: false
exclude:
# arm/v6 is only available for Alpine: https://github.com/docker-library/golang/issues/502
- variant: php-${{ needs.prepare.outputs.php82_version }}-trixie
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:
-
uses: actions/checkout@v4
- name: Prepare
id: prepare
run: echo "sanitized_platform=${PLATFORM//\//-}" >> "${GITHUB_OUTPUT}"
env:
PLATFORM: ${{ matrix.platform }}
- uses: actions/checkout@v6
with:
ref: ${{ needs.prepare.outputs.ref }}
-
name: Set up QEMU
if: matrix.qemu
uses: docker/setup-qemu-action@v3
with:
platforms: ${{ matrix.platform }}
-
name: Set up Docker Buildx
persist-credentials: false
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
platforms: ${{ matrix.platform }}
version: latest
-
name: Login to DockerHub
if: fromJson(needs.prepare.outputs.push)
- name: Login to DockerHub
uses: docker/login-action@v3
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
with:
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
-
name: Build
- name: Build
id: build
uses: docker/bake-action@v4
uses: docker/bake-action@v6
with:
pull: true
load: ${{ !fromJson(needs.prepare.outputs.push) }}
source: .
targets: |
builder-${{ matrix.variant }}
runner-${{ matrix.variant }}
# Remove tags to prevent "can't push tagged ref [...] by digest" error
set: |
${{ (github.event_name == 'pull_request') && '*.args.NO_COMPRESS=1' || '' }}
*.tags=
*.platform=${{ matrix.platform }}
*.cache-from=type=gha,scope=${{ github.ref }}-${{ matrix.platform }}
*.cache-from=type=gha,scope=refs/heads/main-${{ matrix.platform }}
*.cache-to=type=gha,scope=${{ github.ref }}-${{ matrix.platform }},ignore-error=true
${{ fromJson(needs.prepare.outputs.push) && '*.output=type=image,name=dunglas/frankenphp,push-by-digest=true,name-canonical=true,push=true' || '' }}
builder-${{ matrix.variant }}.cache-from=type=gha,scope=builder-${{ matrix.variant }}-${{ needs.prepare.outputs.ref || github.ref }}-${{ matrix.platform }}
builder-${{ matrix.variant }}.cache-from=type=gha,scope=refs/heads/main-builder-${{ matrix.variant }}-${{ matrix.platform }}
builder-${{ matrix.variant }}.cache-to=type=gha,scope=builder-${{ matrix.variant }}-${{ needs.prepare.outputs.ref || github.ref }}-${{ matrix.platform }},ignore-error=true
runner-${{ matrix.variant }}.cache-from=type=gha,scope=runner-${{ matrix.variant }}-${{ needs.prepare.outputs.ref || github.ref }}-${{ matrix.platform }}
runner-${{ matrix.variant }}.cache-from=type=gha,scope=refs/heads/main-runner-${{ matrix.variant }}-${{ matrix.platform }}
runner-${{ matrix.variant }}.cache-to=type=gha,scope=runner-${{ matrix.variant }}-${{ needs.prepare.outputs.ref || github.ref }}-${{ matrix.platform }},ignore-error=true
${{ 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 || github.sha }}
VERSION: ${{ (github.ref_type == 'tag' && github.ref_name) || needs.prepare.outputs.ref || 'dev' }}
PHP_VERSION: ${{ needs.prepare.outputs.php_version }}
-
# Workaround for https://github.com/actions/runner/pull/2477#issuecomment-1501003600
- # Workaround for https://github.com/actions/runner/pull/2477#issuecomment-1501003600
name: Export metadata
if: fromJson(needs.prepare.outputs.push)
run: |
mkdir -p /tmp/metadata/builder /tmp/metadata/runner
# shellcheck disable=SC2086
builderDigest=$(jq -r '."builder-${{ matrix.variant }}"."containerimage.digest"' <<< ${METADATA})
builderDigest=$(jq -r ".\"builder-${VARIANT}\".\"containerimage.digest\"" <<< "${METADATA}")
touch "/tmp/metadata/builder/${builderDigest#sha256:}"
# shellcheck disable=SC2086
runnerDigest=$(jq -r '."runner-${{ matrix.variant }}"."containerimage.digest"' <<< ${METADATA})
runnerDigest=$(jq -r ".\"runner-${VARIANT}\".\"containerimage.digest\"" <<< "${METADATA}")
touch "/tmp/metadata/runner/${runnerDigest#sha256:}"
env:
METADATA: ${{ steps.build.outputs.metadata }}
-
name: Upload builder metadata
VARIANT: ${{ matrix.variant }}
- name: Upload builder metadata
if: fromJson(needs.prepare.outputs.push)
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v5
with:
name: metadata-builder-${{ matrix.variant }}
name: metadata-builder-${{ matrix.variant }}-${{ steps.prepare.outputs.sanitized_platform }}
path: /tmp/metadata/builder/*
if-no-files-found: error
retention-days: 1
-
name: Upload runner metadata
- name: Upload runner metadata
if: fromJson(needs.prepare.outputs.push)
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v5
with:
name: metadata-runner-${{ matrix.variant }}
name: metadata-runner-${{ matrix.variant }}-${{ steps.prepare.outputs.sanitized_platform }}
path: /tmp/metadata/runner/*
if-no-files-found: error
retention-days: 1
-
name: Run tests
if: '!matrix.qemu'
continue-on-error: ${{ fromJson(needs.prepare.outputs.push) }}
- name: Run tests
if: ${{ !fromJson(needs.prepare.outputs.push) }}
run: |
docker run --platform=${{ matrix.platform }} --rm \
"$(jq -r '."builder-${{ matrix.variant }}"."containerimage.config.digest"' <<< "${METADATA}")" \
sh -c 'go test ${{ matrix.race }} -v ./... && cd caddy && go test ${{ matrix.race }} -v ./...'
docker run --platform="${PLATFORM}" --rm \
"$(jq -r ".\"builder-${VARIANT}\".\"containerimage.config.digest\"" <<< "${METADATA}")" \
sh -c "./go.sh test ${RACE} -v $(./go.sh list ./... | grep -v github.com/dunglas/frankenphp/internal/testext | grep -v github.com/dunglas/frankenphp/internal/extgen | tr '\n' ' ') && cd caddy && ../go.sh test ${RACE} -v ./..."
env:
METADATA: ${{ steps.build.outputs.metadata }}
PLATFORM: ${{ matrix.platform }}
VARIANT: ${{ matrix.variant }}
RACE: ${{ matrix.race }}
# Adapted from https://docs.docker.com/build/ci/github-actions/multi-platform/
push:
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
needs:
- prepare
- build
@@ -196,38 +245,38 @@ jobs:
fail-fast: false
matrix:
variant: ${{ fromJson(needs.prepare.outputs.variants) }}
target: ['builder', 'runner']
target: ["builder", "runner"]
steps:
-
name: Download metadata
uses: actions/download-artifact@v4
- name: Download metadata
uses: actions/download-artifact@v6
with:
name: metadata-${{ matrix.target }}-${{ matrix.variant }}
pattern: metadata-${{ matrix.target }}-${{ matrix.variant }}-*
path: /tmp/metadata
-
name: Set up Docker Buildx
merge-multiple: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
version: latest
-
name: Login to DockerHub
- name: Login to DockerHub
uses: docker/login-action@v3
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
with:
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
-
name: Create manifest list and push
- name: Create manifest list and push
working-directory: /tmp/metadata
run: |
set -x
# shellcheck disable=SC2046,SC2086
docker buildx imagetools create $(jq -cr '.target."${{ matrix.target }}-${{ matrix.variant }}".tags | map("-t " + .) | join(" ")' <<< ${METADATA}) \
$(printf 'dunglas/frankenphp@sha256:%s ' *)
docker buildx imagetools create $(jq -cr ".target.\"${TARGET}-${VARIANT}\".tags | map(\"-t \" + .) | join(\" \")" <<< ${METADATA}) \
$(printf "${IMAGE_NAME}@sha256:%s " *)
env:
METADATA: ${{ needs.prepare.outputs.metadata }}
-
name: Inspect image
TARGET: ${{ matrix.target }}
VARIANT: ${{ matrix.variant }}
- name: Inspect image
run: |
# shellcheck disable=SC2046,SC2086
docker buildx imagetools inspect $(jq -cr '.target."${{ matrix.target }}-${{ matrix.variant }}".tags | first' <<< ${METADATA})
docker buildx imagetools inspect $(jq -cr ".target.\"${TARGET}-${VARIANT}\".tags | first" <<< ${METADATA})
env:
METADATA: ${{ needs.prepare.outputs.metadata }}
TARGET: ${{ matrix.target }}
VARIANT: ${{ matrix.variant }}

View File

@@ -1,5 +1,8 @@
---
name: Lint Code Base
concurrency:
cancel-in-progress: true
group: ${{ github.workflow }}-${{ github.ref }}
on:
pull_request:
branches:
@@ -7,37 +10,42 @@ on:
push:
branches:
- main
permissions:
contents: read
packages: read
statuses: write
jobs:
build:
name: Lint Code Base
runs-on: ubuntu-latest
permissions:
contents: read
packages: read
statuses: write
steps:
-
name: Checkout Code
uses: actions/checkout@v4
- name: Checkout Code
uses: actions/checkout@v6
with:
fetch-depth: 0
-
name: Lint Code Base
uses: super-linter/super-linter@v5
persist-credentials: false
- name: Lint Code Base
uses: super-linter/super-linter/slim@v8
env:
VALIDATE_ALL_CODEBASE: true
DEFAULT_BRANCH: main
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
LINTER_RULES_PATH: /
FILTER_REGEX_EXCLUDE: '.*C-Thread-Pool/.*'
MARKDOWN_CONFIG_FILE: .markdown-lint.yaml
VALIDATE_CPP: false
VALIDATE_JSCPD: false
VALIDATE_GO: false
VALIDATE_GO_MODULES: false
VALIDATE_PHP_PHPCS: false
VALIDATE_PHP_PHPSTAN: false
VALIDATE_PHP_PSALM: false
VALIDATE_TERRAGRUNT: false
VALIDATE_DOCKERFILE_HADOLINT: false
VALIDATE_TRIVY: false
# 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

113
.github/workflows/sanitizers.yaml vendored Normal file
View File

@@ -0,0 +1,113 @@
---
name: Sanitizers
concurrency:
cancel-in-progress: true
group: ${{ github.workflow }}-${{ github.ref }}
on:
pull_request:
branches:
- main
paths-ignore:
- "docs/**"
push:
branches:
- main
paths-ignore:
- "docs/**"
permissions:
contents: read
env:
GOTOOLCHAIN: local
jobs:
# Adapted from https://github.com/beberlei/hdrhistogram-php
sanitizers:
name: ${{ matrix.sanitizer }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
sanitizer: ["asan", "msan"]
env:
CFLAGS: -g -O0 -fsanitize=${{ matrix.sanitizer == 'asan' && 'address' || 'memory' }} -DZEND_TRACK_ARENA_ALLOC
LDFLAGS: -fsanitize=${{ matrix.sanitizer == 'asan' && 'address' || 'memory' }}
CC: clang
CXX: clang++
USE_ZEND_ALLOC: 0
LIBRARY_PATH: ${{ github.workspace }}/php/target/lib:${{ github.workspace }}/watcher/target/lib
LD_LIBRARY_PATH: ${{ github.workspace }}/php/target/lib
# PHP doesn't free some memory on purpose, we have to disable leaks detection: https://go.dev/doc/go1.25#go-command
ASAN_OPTIONS: detect_leaks=0
steps:
- name: Remove local PHP
run: sudo apt-get remove --purge --autoremove 'php*' 'libmemcached*'
- 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
- name: Determine PHP version
id: determine-php-version
run: |
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
with:
path: php/target
key: php-sanitizers-${{ matrix.sanitizer }}-${{ runner.arch }}-${{ steps.determine-php-version.outputs.version }}
- if: steps.cache-php.outputs.cache-hit != 'true'
name: Compile PHP
run: |
mkdir php/
curl -fsSL "${URL}" | tar -Jx -C php --strip-components=1
cd php/
./configure \
CFLAGS="$CFLAGS" \
LDFLAGS="$LDFLAGS" \
--enable-debug \
--enable-embed \
--enable-zts \
--enable-option-checking=fatal \
--disable-zend-signals \
--without-sqlite3 \
--without-pdo-sqlite \
--without-libxml \
--disable-dom \
--disable-simplexml \
--disable-xml \
--disable-xmlreader \
--disable-xmlwriter \
--without-pcre-jit \
--disable-opcache-jit \
--disable-cli \
--disable-cgi \
--disable-phpdbg \
--without-pear \
--disable-mbregex \
--enable-werror \
${{ matrix.sanitizer == 'msan' && '--enable-memory-sanitizer' || '' }} \
--prefix="$(pwd)/target/"
make -j"$(getconf _NPROCESSORS_ONLN)"
make install
env:
URL: ${{ steps.determine-php-version.outputs.archive }}
- name: Add PHP to the PATH
run: echo "$(pwd)/php/target/bin" >> "$GITHUB_PATH"
- name: Install e-dant/watcher
uses: ./.github/actions/watcher
- name: Set Set CGO flags
run: |
{
echo "CGO_CFLAGS=$CFLAGS -I${PWD}/watcher/target/include $(php-config --includes)"
echo "CGO_LDFLAGS=$LDFLAGS $(php-config --ldflags) $(php-config --libs)"
} >> "$GITHUB_ENV"
- name: Compile tests
run: go test ${{ matrix.sanitizer == 'msan' && '-tags=nowatcher' || '' }} -${{ matrix.sanitizer }} -v -x -c
- name: Run tests
run: ./frankenphp.test -test.v

View File

@@ -1,154 +1,503 @@
---
name: Build binary releases
concurrency:
cancel-in-progress: true
group: ${{ github.workflow }}-${{ github.ref }}
on:
pull_request:
branches:
- main
paths:
- "docker-bake.hcl"
- ".github/workflows/static.yaml"
- "**cgo.go"
- "**Dockerfile"
- "**.c"
- "**.h"
- "**.sh"
- "**.stub.php"
push:
branches:
- main
tags:
- v*.*.*
workflow_dispatch:
inputs: {}
inputs:
#checkov:skip=CKV_GHA_7
version:
description: "FrankenPHP version"
required: false
type: string
schedule:
- cron: '0 0 * * *'
- cron: "0 0 * * *"
permissions:
contents: read
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:
release:
if: github.ref_type == 'tag'
name: Create GitHub Release
runs-on: ubuntu-latest
steps:
- name: Create release
uses: ncipollo/release-action@v1
with:
generateReleaseNotes: true
allowUpdates: true
omitBodyDuringUpdate: true
omitNameDuringUpdate: true
prepare:
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
outputs:
ref: ${{ steps.ref.outputs.ref }}
push: ${{ toJson((steps.check.outputs.ref || (github.event_name == 'workflow_dispatch' && inputs.version) || startsWith(github.ref, 'refs/tags/') || (github.ref == 'refs/heads/main' && github.event_name != 'pull_request')) && true || false) }}
platforms: ${{ steps.matrix.outputs.platforms }}
metadata: ${{ steps.matrix.outputs.metadata }}
gnu_metadata: ${{ steps.matrix.outputs.gnu_metadata }}
ref: ${{ steps.check.outputs.ref }}
steps:
-
name: Get latest release
id: ref
- name: Get version
id: check
if: github.event_name == 'schedule'
run: echo ref="$(gh release view --repo dunglas/frankenphp --json tagName --jq '.tagName')" >> "${GITHUB_OUTPUT}"
build-linux:
name: Build Linux x86_64 binary
runs-on: ubuntu-latest
needs: [ prepare ]
run: |
ref="${REF}"
if [[ -z "${ref}" ]]; then
ref="$(gh release view --repo dunglas/frankenphp --json tagName --jq '.tagName')"
fi
echo "ref=${ref}" >> "${GITHUB_OUTPUT}"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REF: ${{ (github.ref_type == 'tag' && github.ref_name) || (github.event_name == 'workflow_dispatch' && inputs.version) || '' }}
- uses: actions/checkout@v6
with:
ref: ${{ steps.check.outputs.ref }}
persist-credentials: false
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Create platforms matrix
id: matrix
run: |
METADATA="$(docker buildx bake --print static-builder-musl | jq -c)"
GNU_METADATA="$(docker buildx bake --print static-builder-gnu | jq -c)"
{
echo metadata="${METADATA}"
echo platforms="$(jq -c 'first(.target[]) | .platforms' <<< "${METADATA}")"
echo gnu_metadata="${GNU_METADATA}"
} >> "${GITHUB_OUTPUT}"
env:
SHA: ${{ github.sha }}
VERSION: ${{ steps.check.outputs.ref || 'dev' }}
build-linux-musl:
permissions:
contents: write
id-token: write
attestations: write
strategy:
fail-fast: false
matrix:
platform: ${{ fromJson(needs.prepare.outputs.platforms) }}
debug: [false]
mimalloc: [false]
include:
- platform: linux/amd64
- platform: linux/amd64
debug: true
- platform: linux/amd64
mimalloc: true
name: Build ${{ matrix.platform }} static musl binary${{ matrix.debug && ' (debug)' || '' }}${{ matrix.mimalloc && ' (mimalloc)' || '' }}
runs-on: ${{ startsWith(matrix.platform, 'linux/arm') && 'ubuntu-24.04-arm' || 'ubuntu-24.04' }}
needs: [prepare]
steps:
-
uses: actions/checkout@v4
- name: Prepare
id: prepare
run: echo "sanitized_platform=${PLATFORM//\//-}" >> "${GITHUB_OUTPUT}"
env:
PLATFORM: ${{ matrix.platform }}
- uses: actions/checkout@v6
with:
ref: ${{ needs.prepare.outputs.ref }}
-
name: Set up Docker Buildx
persist-credentials: false
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
version: latest
-
name: Login to DockerHub
if: github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/') || (github.ref == 'refs/heads/main' && github.event_name != 'pull_request')
platforms: ${{ matrix.platform }}
- name: Login to DockerHub
uses: docker/login-action@v3
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
with:
username: ${{secrets.REGISTRY_USERNAME}}
password: ${{secrets.REGISTRY_PASSWORD}}
-
name: Build
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@v4
uses: docker/bake-action@v6
with:
pull: true
load: ${{toJson(github.event_name != 'schedule' && !startsWith(github.ref, 'refs/tags/') && (github.ref != 'refs/heads/main' || github.event_name == 'pull_request'))}}
push: ${{toJson(github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/') || (github.ref == 'refs/heads/main' && github.event_name != 'pull_request'))}}
targets: static-builder
load: ${{ !fromJson(needs.prepare.outputs.push) || matrix.debug || matrix.mimalloc }}
source: .
targets: static-builder-musl
set: |
*.cache-from=type=gha,scope=${{github.ref}}-static-builder
*.cache-from=type=gha,scope=refs/heads/main-static-builder
*.cache-to=type=gha,scope=${{github.ref}}-static-builder,ignore-error=true
${{ matrix.debug && 'static-builder-musl.args.DEBUG_SYMBOLS=1' || '' }}
${{ matrix.mimalloc && 'static-builder-musl.args.MIMALLOC=1' || '' }}
${{ (github.event_name == 'pull_request' || matrix.platform == 'linux/arm64') && 'static-builder-musl.args.NO_COMPRESS=1' || '' }}
*.tags=
*.platform=${{ matrix.platform }}
*.cache-from=type=gha,scope=${{ needs.prepare.outputs.ref || github.ref }}-static-builder-musl${{ matrix.debug && '-debug' || '' }}${{ matrix.mimalloc && '-mimalloc' || '' }}
*.cache-from=type=gha,scope=refs/heads/main-static-builder-musl${{ matrix.debug && '-debug' || '' }}${{ matrix.mimalloc && '-mimalloc' || '' }}
*.cache-to=type=gha,scope=${{ needs.prepare.outputs.ref || github.ref }}-static-builder-musl${{ matrix.debug && '-debug' || '' }}${{ matrix.mimalloc && '-mimalloc' || '' }},ignore-error=true
${{ (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 || github.sha}}
SHA: ${{ github.sha }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-
name: Pull Docker image
if: github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/') || (github.ref == 'refs/heads/main' && github.event_name != 'pull_request')
run: docker pull dunglas/frankenphp:static-builder
-
name: Copy binary
run: docker cp "$(docker create --name static-builder dunglas/frankenphp:static-builder):/go/src/app/dist/frankenphp-linux-x86_64" frankenphp-linux-x86_64 ; docker rm static-builder
-
name: Upload asset
if: github.event_name == 'schedule' || github.ref_type == 'tag'
uses: ncipollo/release-action@v1
- # Workaround for https://github.com/actions/runner/pull/2477#issuecomment-1501003600
name: Export metadata
if: fromJson(needs.prepare.outputs.push) && !matrix.debug && !matrix.mimalloc
run: |
mkdir -p /tmp/metadata
# shellcheck disable=SC2086
digest=$(jq -r '."static-builder-musl"."containerimage.digest"' <<< ${METADATA})
touch "/tmp/metadata/${digest#sha256:}"
env:
METADATA: ${{ steps.build.outputs.metadata }}
- name: Upload metadata
if: fromJson(needs.prepare.outputs.push) && !matrix.debug && !matrix.mimalloc
uses: actions/upload-artifact@v5
with:
tag: ${{ (github.ref_type == 'tag' && github.ref_name) || needs.prepare.outputs.ref }}
generateReleaseNotes: true
allowUpdates: true
omitBodyDuringUpdate: true
omitNameDuringUpdate: true
artifacts: frankenphp-linux-x86_64
replacesArtifacts: true
-
name: Upload artifact
if: github.ref_type == 'branch'
uses: actions/upload-artifact@v3
name: metadata-static-builder-musl-${{ steps.prepare.outputs.sanitized_platform }}
path: /tmp/metadata/*
if-no-files-found: error
retention-days: 1
- name: Copy binary
run: |
# shellcheck disable=SC2034
digest=$(jq -r '."static-builder-musl"."${{ (fromJson(needs.prepare.outputs.push) && !matrix.debug && !matrix.mimalloc) && 'containerimage.digest' || 'containerimage.config.digest' }}"' <<< "${METADATA}")
docker create --platform="${PLATFORM}" --name static-builder-musl "${{ (fromJson(needs.prepare.outputs.push) && !matrix.debug && !matrix.mimalloc) && '${IMAGE_NAME}@${digest}' || '${digest}' }}"
docker cp "static-builder-musl:/go/src/app/dist/${BINARY}" "${BINARY}${{ matrix.debug && '-debug' || '' }}${{ matrix.mimalloc && '-mimalloc' || '' }}"
env:
METADATA: ${{ steps.build.outputs.metadata }}
BINARY: frankenphp-linux-${{ matrix.platform == 'linux/amd64' && 'x86_64' || 'aarch64' }}
PLATFORM: ${{ matrix.platform }}
- name: Upload artifact
if: ${{ !fromJson(needs.prepare.outputs.push) }}
uses: actions/upload-artifact@v5
with:
path: frankenphp-linux-x86_64
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' || '' }}
- name: Upload assets
if: fromJson(needs.prepare.outputs.push) && (needs.prepare.outputs.ref || github.ref_type == 'tag')
run: gh release upload "${REF}" frankenphp-linux-${{ matrix.platform == 'linux/amd64' && 'x86_64' || 'aarch64' }}${{ matrix.debug && '-debug' || '' }}${{ matrix.mimalloc && '-mimalloc' || '' }} --repo dunglas/frankenphp --clobber
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REF: ${{ (github.ref_type == 'tag' && github.ref_name) || needs.prepare.outputs.ref }}
- if: fromJson(needs.prepare.outputs.push) && (needs.prepare.outputs.ref || github.ref_type == 'tag')
uses: actions/attest-build-provenance@v3
with:
subject-path: ${{ github.workspace }}/frankenphp-linux-*
- name: Run sanity checks
run: |
"${BINARY}" version
"${BINARY}" build-info
"${BINARY}" list-modules | grep frankenphp
"${BINARY}" list-modules | grep http.encoders.br
"${BINARY}" list-modules | grep http.handlers.mercure
"${BINARY}" list-modules | grep http.handlers.mercure
"${BINARY}" list-modules | grep http.handlers.vulcain
"${BINARY}" php-cli -r "echo 'Sanity check passed';"
env:
BINARY: ./frankenphp-linux-${{ matrix.platform == 'linux/amd64' && 'x86_64' || 'aarch64' }}${{ matrix.debug && '-debug' || '' }}${{ matrix.mimalloc && '-mimalloc' || '' }}
build-linux-gnu:
permissions:
contents: write
id-token: write
attestations: write
strategy:
fail-fast: false
matrix:
platform: ${{ fromJson(needs.prepare.outputs.platforms) }}
name: Build ${{ matrix.platform }} static GNU binary
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@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:
platforms: ${{ matrix.platform }}
- name: Login to DockerHub
uses: docker/login-action@v3
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
with:
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Build
id: build
uses: docker/bake-action@v6
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' || '' }}
*.tags=
*.platform=${{ matrix.platform }}
*.cache-from=type=gha,scope=${{ needs.prepare.outputs.ref || github.ref }}-static-builder-gnu
*.cache-from=type=gha,scope=refs/heads/main-static-builder-gnu
*.cache-to=type=gha,scope=${{ needs.prepare.outputs.ref || github.ref }}-static-builder-gnu,ignore-error=true
${{ 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 }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- # Workaround for https://github.com/actions/runner/pull/2477#issuecomment-1501003600
name: Export metadata
if: fromJson(needs.prepare.outputs.push)
run: |
mkdir -p /tmp/metadata-gnu
# shellcheck disable=SC2086
digest=$(jq -r '."static-builder-gnu"."containerimage.digest"' <<< ${METADATA})
touch "/tmp/metadata-gnu/${digest#sha256:}"
env:
METADATA: ${{ steps.build.outputs.metadata }}
- name: Upload metadata
if: fromJson(needs.prepare.outputs.push)
uses: actions/upload-artifact@v5
with:
name: metadata-static-builder-gnu-${{ steps.prepare.outputs.sanitized_platform }}
path: /tmp/metadata-gnu/*
if-no-files-found: error
retention-days: 1
- name: Copy all frankenphp* files
run: |
# shellcheck disable=SC2034
digest=$(jq -r '."static-builder-gnu"."${{ fromJson(needs.prepare.outputs.push) && 'containerimage.digest' || 'containerimage.config.digest' }}"' <<< "${METADATA}")
container_id=$(docker create --platform="${PLATFORM}" "${{ fromJson(needs.prepare.outputs.push) && '${IMAGE_NAME}@${digest}' || '${digest}' }}")
mkdir -p gh-output
cd gh-output
for file in $(docker run --rm "${{ fromJson(needs.prepare.outputs.push) && '${IMAGE_NAME}@${digest}' || '${digest}' }}" sh -c "ls /go/src/app/dist | grep '^frankenphp'"); do
docker cp "${container_id}:/go/src/app/dist/${file}" "./${file}"
done
docker rm "${container_id}"
mv "${BINARY}" "${BINARY}-gnu"
env:
METADATA: ${{ steps.build.outputs.metadata }}
BINARY: frankenphp-linux-${{ matrix.platform == 'linux/amd64' && 'x86_64' || 'aarch64' }}
PLATFORM: ${{ matrix.platform }}
- name: Upload artifact
if: ${{ !fromJson(needs.prepare.outputs.push) }}
uses: actions/upload-artifact@v5
with:
name: frankenphp-linux-${{ matrix.platform == 'linux/amd64' && 'x86_64' || 'aarch64' }}-gnu-files
path: gh-output/*
- name: Upload assets
if: fromJson(needs.prepare.outputs.push) && (needs.prepare.outputs.ref || github.ref_type == 'tag')
run: gh release upload "${REF}" gh-output/* --repo dunglas/frankenphp --clobber
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REF: ${{ (github.ref_type == 'tag' && github.ref_name) || needs.prepare.outputs.ref }}
- if: fromJson(needs.prepare.outputs.push) && (needs.prepare.outputs.ref || github.ref_type == 'tag')
uses: actions/attest-build-provenance@v3
with:
subject-path: ${{ github.workspace }}/gh-output/frankenphp-linux-*-gnu
- name: Run sanity checks
run: |
"${BINARY}" version
"${BINARY}" list-modules | grep frankenphp
"${BINARY}" list-modules | grep http.encoders.br
"${BINARY}" list-modules | grep http.handlers.mercure
"${BINARY}" list-modules | grep http.handlers.mercure
"${BINARY}" list-modules | grep http.handlers.vulcain
"${BINARY}" php-cli -r "echo 'Sanity check passed';"
env:
BINARY: ./gh-output/frankenphp-linux-${{ matrix.platform == 'linux/amd64' && 'x86_64' || 'aarch64' }}-gnu
# Adapted from https://docs.docker.com/build/ci/github-actions/multi-platform/
push:
runs-on: ubuntu-24.04
needs:
- prepare
- build-linux-musl
- build-linux-gnu
if: fromJson(needs.prepare.outputs.push)
steps:
- name: Download metadata
uses: actions/download-artifact@v6
with:
pattern: metadata-static-builder-musl-*
path: /tmp/metadata
merge-multiple: true
- name: Download GNU metadata
uses: actions/download-artifact@v6
with:
pattern: metadata-static-builder-gnu-*
path: /tmp/metadata-gnu
merge-multiple: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to DockerHub
uses: docker/login-action@v3
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
with:
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Create manifest list and push
working-directory: /tmp/metadata
run: |
# shellcheck disable=SC2046,SC2086
docker buildx imagetools create $(jq -cr '.target."static-builder-musl".tags | map("-t " + .) | join(" ")' <<< "${METADATA}") \
$(printf "${IMAGE_NAME}@sha256:%s " *)
env:
METADATA: ${{ needs.prepare.outputs.metadata }}
- name: Create GNU manifest list and push
working-directory: /tmp/metadata-gnu
run: |
# shellcheck disable=SC2046,SC2086
docker buildx imagetools create $(jq -cr '.target."static-builder-gnu".tags | map("-t " + .) | join(" ")' <<< "${GNU_METADATA}") \
$(printf "${IMAGE_NAME}@sha256:%s " *)
env:
GNU_METADATA: ${{ needs.prepare.outputs.gnu_metadata }}
- name: Inspect image
run: |
# shellcheck disable=SC2046,SC2086
docker buildx imagetools inspect "$(jq -cr '.target."static-builder-musl".tags | first' <<< "${METADATA}")"
env:
METADATA: ${{ needs.prepare.outputs.metadata }}
- name: Inspect GNU image
run: |
# shellcheck disable=SC2046,SC2086
docker buildx imagetools inspect "$(jq -cr '.target."static-builder-gnu".tags | first' <<< "${GNU_METADATA}")-gnu"
env:
GNU_METADATA: ${{ needs.prepare.outputs.gnu_metadata }}
build-mac:
name: Build macOS x86_64 binaries
runs-on: macos-latest
needs: [ prepare ]
permissions:
contents: write
id-token: write
attestations: write
strategy:
fail-fast: false
matrix:
platform: ["arm64", "x86_64"]
name: Build macOS ${{ matrix.platform }} binaries
runs-on: ${{ matrix.platform == 'arm64' && 'macos-15' || 'macos-15-intel' }}
needs: [prepare]
env:
HOMEBREW_NO_AUTO_UPDATE: 1
steps:
-
uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
ref: ${{ needs.prepare.outputs.ref }}
-
uses: actions/setup-go@v5
persist-credentials: false
- uses: actions/setup-go@v6
with:
go-version: '1.21'
go-version: "1.25"
cache-dependency-path: |
go.sum
go.sum
caddy/go.sum
-
name: Set FRANKENPHP_VERSION
cache: false
- name: Set FRANKENPHP_VERSION
run: |
if [ "${GITHUB_REF_TYPE}" == "tag" ]; then
export FRANKENPHP_VERSION=${GITHUB_REF_NAME:1}
elif [ "${GITHUB_EVENT_NAME}" == "schedule" ]; then
export FRANKENPHP_VERSION="${{ needs.prepare.outputs.ref }}"
export FRANKENPHP_VERSION="${REF}"
else
export FRANKENPHP_VERSION=${GITHUB_SHA}
fi
echo "FRANKENPHP_VERSION=${FRANKENPHP_VERSION}" >> "${GITHUB_ENV}"
-
name: Build FrankenPHP
env:
REF: ${{ needs.prepare.outputs.ref }}
- name: Build FrankenPHP
run: ./build-static.sh
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-
name: Upload asset
if: github.event_name == 'schedule' || github.ref_type == 'tag'
uses: ncipollo/release-action@v1
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@v5
with:
tag: ${{ (github.ref_type == 'tag' && github.ref_name) || needs.prepare.outputs.ref }}
generateReleaseNotes: true
allowUpdates: true
omitBodyDuringUpdate: true
omitNameDuringUpdate: true
artifacts: dist/frankenphp-mac-x86_64
replacesArtifacts: true
-
name: Upload artifact
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@v3
uses: actions/upload-artifact@v5
with:
path: dist/frankenphp-mac-x86_64
name: frankenphp-mac-${{ matrix.platform }}
path: dist/frankenphp-mac-${{ matrix.platform }}
- name: Run sanity checks
run: |
"${BINARY}" version
"${BINARY}" build-info
"${BINARY}" list-modules | grep frankenphp
"${BINARY}" list-modules | grep http.encoders.br
"${BINARY}" list-modules | grep http.handlers.mercure
"${BINARY}" list-modules | grep http.handlers.mercure
"${BINARY}" list-modules | grep http.handlers.vulcain
"${BINARY}" php-cli -r "echo 'Sanity check passed';"
env:
BINARY: dist/frankenphp-mac-${{ matrix.platform }}

View File

@@ -1,31 +1,51 @@
---
name: Tests
concurrency:
cancel-in-progress: true
group: ${{ github.workflow }}-${{ github.ref }}
on:
pull_request:
branches:
- main
paths-ignore:
- "docs/**"
push:
branches:
- main
paths-ignore:
- "docs/**"
permissions:
contents: read
env:
GOTOOLCHAIN: local
GOEXPERIMENT: cgocheck2
jobs:
tests:
tests-linux:
name: Tests (Linux, PHP ${{ matrix.php-versions }})
runs-on: ubuntu-latest
continue-on-error: false
strategy:
fail-fast: false
matrix:
php-versions: ['8.2', '8.3']
include:
- php-versions: "8.2"
- php-versions: "8.3"
- php-versions: "8.4"
- php-versions: "8.5"
env:
GOMAXPROCS: 10
LIBRARY_PATH: ${{ github.workspace }}/watcher/target/lib
steps:
-
uses: actions/checkout@v4
-
uses: actions/setup-go@v5
- uses: actions/checkout@v6
with:
go-version: '1.21'
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
- uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-versions }}
ini-file: development
@@ -33,39 +53,123 @@ jobs:
tools: none
env:
phpts: ts
-
name: Set CGO flags
run: |
echo "CGO_CFLAGS=$(php-config --includes)" >> "$GITHUB_ENV"
-
name: Build
debug: true
- name: Install e-dant/watcher
uses: ./.github/actions/watcher
- name: Set CGO flags
run: echo "CGO_CFLAGS=-I${PWD}/watcher/target/include $(php-config --includes)" >> "${GITHUB_ENV}"
- name: Build
run: go build
env:
GOEXPERIMENT: cgocheck2
-
name: Build testcli binary
- name: Build testcli binary
working-directory: internal/testcli/
run: go build
-
name: Run library tests
run: go test -race -v ./...
-
name: Run Caddy module tests
- name: Compile library tests
run: go test -race -v -x -c
- name: Run library tests
run: ./frankenphp.test -test.v
- name: Run Caddy module tests
working-directory: caddy/
run: go test -race -v ./...
-
name: Build the server
run: go test -tags nobadger,nomysql,nopgx -race -v ./...
- name: Run Fuzzing Tests
working-directory: caddy/
run: go test -fuzz FuzzRequest -fuzztime 20s
- name: Build the server
working-directory: caddy/frankenphp/
run: go build
-
name: Start the server
- name: Start the server
working-directory: testdata/
run: sudo ../caddy/frankenphp/frankenphp start
-
name: Run integrations tests
- name: Run integrations tests
run: ./reload_test.sh
-
name: Lint Go code
uses: golangci/golangci-lint-action@v3
- name: Lint Go code
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.5'
run: go mod tidy -diff
- name: Ensure caddy/go.mod is tidy
if: matrix.php-versions == '8.5'
run: go mod tidy -diff
working-directory: caddy/
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@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: ${{ 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
env:
phpts: ts
debug: true
- name: Set Set CGO flags
run: |
{
echo "CGO_CFLAGS=-I/opt/homebrew/include/ $(php-config --includes)"
echo "CGO_LDFLAGS=-L/opt/homebrew/lib/ $(php-config --ldflags) $(php-config --libs)"
} >> "${GITHUB_ENV}"
- name: Build
run: go build -tags nowatcher
- name: Run library tests
run: go test -tags nowatcher -race -v ./...
- name: Run Caddy module tests
working-directory: caddy/
run: go test -tags nowatcher,nobadger,nomysql,nopgx -race -v ./...

4
.gitignore vendored
View File

@@ -2,7 +2,11 @@
/internal/testserver/testserver
/internal/testcli/testcli
/dist
.DS_Store
.idea/
.vscode/
__debug_bin
frankenphp.test
caddy/frankenphp/Build
package/etc/php.ini
*.log

1
.gitleaksignore Normal file
View File

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

7
.golangci.yaml Normal file
View File

@@ -0,0 +1,7 @@
---
version: "2"
run:
build-tags:
- nobadger
- nomysql
- nopgx

View File

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

View File

@@ -1,21 +0,0 @@
The MIT License (MIT)
Copyright (c) 2016 Johan Hanssen Seferidis
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,69 +0,0 @@
[![CircleCI](https://circleci.com/gh/Pithikos/C-Thread-Pool.svg?style=svg)](https://circleci.com/gh/Pithikos/C-Thread-Pool)
# C Thread Pool
This is a minimal but advanced threadpool implementation.
* ANCI C and POSIX compliant
* Pause/resume/wait as you like
* Simple easy-to-digest API
* Well tested
The threadpool is under MIT license. Notice that this project took a considerable amount of work and sacrifice of my free time and the reason I give it for free (even for commercial use) is so when you become rich and wealthy you don't forget about us open-source creatures of the night. Cheers!
If this project reduced your development time feel free to buy me a coffee.
[![Donate](https://www.paypal.com/en_US/i/btn/x-click-but21.gif)](https://www.paypal.me/seferidis)
## Run an example
The library is not precompiled so you have to compile it with your project. The thread pool
uses POSIX threads so if you compile with gcc on Linux you have to use the flag `-pthread` like this:
gcc example.c thpool.c -D THPOOL_DEBUG -pthread -o example
Then run the executable like this:
./example
## Basic usage
1. Include the header in your source file: `#include "thpool.h"`
2. Create a thread pool with number of threads you want: `threadpool thpool = thpool_init(4);`
3. Add work to the pool: `thpool_add_work(thpool, (void*)function_p, (void*)arg_p);`
The workers(threads) will start their work automatically as fast as there is new work
in the pool. If you want to wait for all added work to be finished before continuing
you can use `thpool_wait(thpool);`. If you want to destroy the pool you can use
`thpool_destroy(thpool);`.
## API
For a deeper look into the documentation check in the [thpool.h](https://github.com/Pithikos/C-Thread-Pool/blob/master/thpool.h) file. Below is a fast practical overview.
| Function example | Description |
|---------------------------------|---------------------------------------------------------------------|
| ***thpool_init(4)*** | Will return a new threadpool with `4` threads. |
| ***thpool_add_work(thpool, (void&#42;)function_p, (void&#42;)arg_p)*** | Will add new work to the pool. Work is simply a function. You can pass a single argument to the function if you wish. If not, `NULL` should be passed. |
| ***thpool_wait(thpool)*** | Will wait for all jobs (both in queue and currently running) to finish. |
| ***thpool_destroy(thpool)*** | This will destroy the threadpool. If jobs are currently being executed, then it will wait for them to finish. |
| ***thpool_pause(thpool)*** | All threads in the threadpool will pause no matter if they are idle or executing work. |
| ***thpool_resume(thpool)*** | If the threadpool is paused, then all threads will resume from where they were. |
| ***thpool_num_threads_working(thpool)*** | Will return the number of currently working threads. |
## Contribution
You are very welcome to contribute. If you have a new feature in mind, you can always open an issue on github describing it so you don't end up doing a lot of work that might not be eventually merged. Generally we are very open to contributions as long as they follow the below keypoints.
* Try to keep the API as minimal as possible. That means if a feature or fix can be implemented without affecting the existing API but requires more development time, then we will opt to sacrifice development time.
* Solutions need to be POSIX compliant. The thread-pool is advertised as such so it makes sense that it actually is.
* For coding style simply try to stick to the conventions you find in the existing codebase.
* Tests: A new fix or feature should be covered by tests. If the existing tests are not sufficient, we expect an according test to follow with the pull request.
* Documentation: for a new feature please add documentation. For an API change the documentation has to be thorough and super easy to understand.
If you wish to **get access as a collaborator** feel free to mention it in the issue https://github.com/Pithikos/C-Thread-Pool/issues/78

View File

@@ -1,563 +0,0 @@
/* ********************************
* Author: Johan Hanssen Seferidis
* License: MIT
* Description: Library providing a threading pool where you can add
* work. For usage, check the thpool.h file or README.md
*
*//** @file thpool.h *//*
*
********************************/
#if defined(__APPLE__)
#include <AvailabilityMacros.h>
#else
#ifndef _POSIX_C_SOURCE
#define _POSIX_C_SOURCE 200809L
#endif
#endif
#include <unistd.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <errno.h>
#include <time.h>
#if defined(__linux__)
#include <sys/prctl.h>
#endif
#include "thpool.h"
#ifdef THPOOL_DEBUG
#define THPOOL_DEBUG 1
#else
#define THPOOL_DEBUG 0
#endif
#if !defined(DISABLE_PRINT) || defined(THPOOL_DEBUG)
#define err(str) fprintf(stderr, str)
#else
#define err(str)
#endif
#ifndef THPOOL_THREAD_NAME
#define THPOOL_THREAD_NAME thpool
#endif
#define STRINGIFY(x) #x
#define TOSTRING(x) STRINGIFY(x)
static volatile int threads_keepalive;
static volatile int threads_on_hold;
/* ========================== STRUCTURES ============================ */
/* Binary semaphore */
typedef struct bsem {
pthread_mutex_t mutex;
pthread_cond_t cond;
int v;
} bsem;
/* Job */
typedef struct job{
struct job* prev; /* pointer to previous job */
void (*function)(void* arg); /* function pointer */
void* arg; /* function's argument */
} job;
/* Job queue */
typedef struct jobqueue{
pthread_mutex_t rwmutex; /* used for queue r/w access */
job *front; /* pointer to front of queue */
job *rear; /* pointer to rear of queue */
bsem *has_jobs; /* flag as binary semaphore */
int len; /* number of jobs in queue */
} jobqueue;
/* Thread */
typedef struct thread{
int id; /* friendly id */
pthread_t pthread; /* pointer to actual thread */
struct thpool_* thpool_p; /* access to thpool */
} thread;
/* Threadpool */
typedef struct thpool_{
thread** threads; /* pointer to threads */
volatile int num_threads_alive; /* threads currently alive */
volatile int num_threads_working; /* threads currently working */
pthread_mutex_t thcount_lock; /* used for thread count etc */
pthread_cond_t threads_all_idle; /* signal to thpool_wait */
jobqueue jobqueue; /* job queue */
} thpool_;
/* ========================== PROTOTYPES ============================ */
static int thread_init(thpool_* thpool_p, struct thread** thread_p, int id);
static void* thread_do(struct thread* thread_p);
static void thread_hold(int sig_id);
static void thread_destroy(struct thread* thread_p);
static int jobqueue_init(jobqueue* jobqueue_p);
static void jobqueue_clear(jobqueue* jobqueue_p);
static void jobqueue_push(jobqueue* jobqueue_p, struct job* newjob_p);
static struct job* jobqueue_pull(jobqueue* jobqueue_p);
static void jobqueue_destroy(jobqueue* jobqueue_p);
static void bsem_init(struct bsem *bsem_p, int value);
static void bsem_reset(struct bsem *bsem_p);
static void bsem_post(struct bsem *bsem_p);
static void bsem_post_all(struct bsem *bsem_p);
static void bsem_wait(struct bsem *bsem_p);
/* ========================== THREADPOOL ============================ */
/* Initialise thread pool */
struct thpool_* thpool_init(int num_threads){
threads_on_hold = 0;
threads_keepalive = 1;
if (num_threads < 0){
num_threads = 0;
}
/* Make new thread pool */
thpool_* thpool_p;
thpool_p = (struct thpool_*)malloc(sizeof(struct thpool_));
if (thpool_p == NULL){
err("thpool_init(): Could not allocate memory for thread pool\n");
return NULL;
}
thpool_p->num_threads_alive = 0;
thpool_p->num_threads_working = 0;
/* Initialise the job queue */
if (jobqueue_init(&thpool_p->jobqueue) == -1){
err("thpool_init(): Could not allocate memory for job queue\n");
free(thpool_p);
return NULL;
}
/* Make threads in pool */
thpool_p->threads = (struct thread**)malloc(num_threads * sizeof(struct thread *));
if (thpool_p->threads == NULL){
err("thpool_init(): Could not allocate memory for threads\n");
jobqueue_destroy(&thpool_p->jobqueue);
free(thpool_p);
return NULL;
}
pthread_mutex_init(&(thpool_p->thcount_lock), NULL);
pthread_cond_init(&thpool_p->threads_all_idle, NULL);
/* Thread init */
int n;
for (n=0; n<num_threads; n++){
thread_init(thpool_p, &thpool_p->threads[n], n);
#if THPOOL_DEBUG
printf("THPOOL_DEBUG: Created thread %d in pool \n", n);
#endif
}
/* Wait for threads to initialize */
while (thpool_p->num_threads_alive != num_threads) {}
return thpool_p;
}
/* Add work to the thread pool */
int thpool_add_work(thpool_* thpool_p, void (*function_p)(void*), void* arg_p){
job* newjob;
newjob=(struct job*)malloc(sizeof(struct job));
if (newjob==NULL){
err("thpool_add_work(): Could not allocate memory for new job\n");
return -1;
}
/* add function and argument */
newjob->function=function_p;
newjob->arg=arg_p;
/* add job to queue */
jobqueue_push(&thpool_p->jobqueue, newjob);
return 0;
}
/* Wait until all jobs have finished */
void thpool_wait(thpool_* thpool_p){
pthread_mutex_lock(&thpool_p->thcount_lock);
while (thpool_p->jobqueue.len || thpool_p->num_threads_working) {
pthread_cond_wait(&thpool_p->threads_all_idle, &thpool_p->thcount_lock);
}
pthread_mutex_unlock(&thpool_p->thcount_lock);
}
/* Destroy the threadpool */
void thpool_destroy(thpool_* thpool_p){
/* No need to destroy if it's NULL */
if (thpool_p == NULL) return ;
volatile int threads_total = thpool_p->num_threads_alive;
/* End each thread 's infinite loop */
threads_keepalive = 0;
/* Give one second to kill idle threads */
double TIMEOUT = 1.0;
time_t start, end;
double tpassed = 0.0;
time (&start);
while (tpassed < TIMEOUT && thpool_p->num_threads_alive){
bsem_post_all(thpool_p->jobqueue.has_jobs);
time (&end);
tpassed = difftime(end,start);
}
/* Poll remaining threads */
while (thpool_p->num_threads_alive){
bsem_post_all(thpool_p->jobqueue.has_jobs);
sleep(1);
}
/* Job queue cleanup */
jobqueue_destroy(&thpool_p->jobqueue);
/* Deallocs */
int n;
for (n=0; n < threads_total; n++){
thread_destroy(thpool_p->threads[n]);
}
free(thpool_p->threads);
free(thpool_p);
}
/* Pause all threads in threadpool */
void thpool_pause(thpool_* thpool_p) {
int n;
for (n=0; n < thpool_p->num_threads_alive; n++){
pthread_kill(thpool_p->threads[n]->pthread, SIGUSR1);
}
}
/* Resume all threads in threadpool */
void thpool_resume(thpool_* thpool_p) {
// resuming a single threadpool hasn't been
// implemented yet, meanwhile this suppresses
// the warnings
(void)thpool_p;
threads_on_hold = 0;
}
int thpool_num_threads_working(thpool_* thpool_p){
return thpool_p->num_threads_working;
}
/* ============================ THREAD ============================== */
/* Initialize a thread in the thread pool
*
* @param thread address to the pointer of the thread to be created
* @param id id to be given to the thread
* @return 0 on success, -1 otherwise.
*/
static int thread_init (thpool_* thpool_p, struct thread** thread_p, int id){
*thread_p = (struct thread*)malloc(sizeof(struct thread));
if (*thread_p == NULL){
err("thread_init(): Could not allocate memory for thread\n");
return -1;
}
(*thread_p)->thpool_p = thpool_p;
(*thread_p)->id = id;
pthread_create(&(*thread_p)->pthread, NULL, (void * (*)(void *)) thread_do, (*thread_p));
pthread_detach((*thread_p)->pthread);
return 0;
}
/* Sets the calling thread on hold */
static void thread_hold(int sig_id) {
(void)sig_id;
threads_on_hold = 1;
while (threads_on_hold){
sleep(1);
}
}
/* What each thread is doing
*
* In principle this is an endless loop. The only time this loop gets interuppted is once
* thpool_destroy() is invoked or the program exits.
*
* @param thread thread that will run this function
* @return nothing
*/
static void* thread_do(struct thread* thread_p){
/* Set thread name for profiling and debugging */
char thread_name[16] = {0};
snprintf(thread_name, 16, TOSTRING(THPOOL_THREAD_NAME) "-%d", thread_p->id);
#if defined(__linux__)
/* Use prctl instead to prevent using _GNU_SOURCE flag and implicit declaration */
prctl(PR_SET_NAME, thread_name);
#elif defined(__APPLE__) && defined(__MACH__)
pthread_setname_np(thread_name);
#else
err("thread_do(): pthread_setname_np is not supported on this system");
#endif
/* Assure all threads have been created before starting serving */
thpool_* thpool_p = thread_p->thpool_p;
/* Register signal handler */
struct sigaction act;
sigemptyset(&act.sa_mask);
act.sa_flags = SA_ONSTACK;
act.sa_handler = thread_hold;
if (sigaction(SIGUSR1, &act, NULL) == -1) {
err("thread_do(): cannot handle SIGUSR1");
}
/* Mark thread as alive (initialized) */
pthread_mutex_lock(&thpool_p->thcount_lock);
thpool_p->num_threads_alive += 1;
pthread_mutex_unlock(&thpool_p->thcount_lock);
while(threads_keepalive){
bsem_wait(thpool_p->jobqueue.has_jobs);
if (threads_keepalive){
pthread_mutex_lock(&thpool_p->thcount_lock);
thpool_p->num_threads_working++;
pthread_mutex_unlock(&thpool_p->thcount_lock);
/* Read job from queue and execute it */
void (*func_buff)(void*);
void* arg_buff;
job* job_p = jobqueue_pull(&thpool_p->jobqueue);
if (job_p) {
func_buff = job_p->function;
arg_buff = job_p->arg;
func_buff(arg_buff);
free(job_p);
}
pthread_mutex_lock(&thpool_p->thcount_lock);
thpool_p->num_threads_working--;
if (!thpool_p->num_threads_working) {
pthread_cond_signal(&thpool_p->threads_all_idle);
}
pthread_mutex_unlock(&thpool_p->thcount_lock);
}
}
pthread_mutex_lock(&thpool_p->thcount_lock);
thpool_p->num_threads_alive --;
pthread_mutex_unlock(&thpool_p->thcount_lock);
return NULL;
}
/* Frees a thread */
static void thread_destroy (thread* thread_p){
free(thread_p);
}
/* ============================ JOB QUEUE =========================== */
/* Initialize queue */
static int jobqueue_init(jobqueue* jobqueue_p){
jobqueue_p->len = 0;
jobqueue_p->front = NULL;
jobqueue_p->rear = NULL;
jobqueue_p->has_jobs = (struct bsem*)malloc(sizeof(struct bsem));
if (jobqueue_p->has_jobs == NULL){
return -1;
}
pthread_mutex_init(&(jobqueue_p->rwmutex), NULL);
bsem_init(jobqueue_p->has_jobs, 0);
return 0;
}
/* Clear the queue */
static void jobqueue_clear(jobqueue* jobqueue_p){
while(jobqueue_p->len){
free(jobqueue_pull(jobqueue_p));
}
jobqueue_p->front = NULL;
jobqueue_p->rear = NULL;
bsem_reset(jobqueue_p->has_jobs);
jobqueue_p->len = 0;
}
/* Add (allocated) job to queue
*/
static void jobqueue_push(jobqueue* jobqueue_p, struct job* newjob){
pthread_mutex_lock(&jobqueue_p->rwmutex);
newjob->prev = NULL;
switch(jobqueue_p->len){
case 0: /* if no jobs in queue */
jobqueue_p->front = newjob;
jobqueue_p->rear = newjob;
break;
default: /* if jobs in queue */
jobqueue_p->rear->prev = newjob;
jobqueue_p->rear = newjob;
}
jobqueue_p->len++;
bsem_post(jobqueue_p->has_jobs);
pthread_mutex_unlock(&jobqueue_p->rwmutex);
}
/* Get first job from queue(removes it from queue)
* Notice: Caller MUST hold a mutex
*/
static struct job* jobqueue_pull(jobqueue* jobqueue_p){
pthread_mutex_lock(&jobqueue_p->rwmutex);
job* job_p = jobqueue_p->front;
switch(jobqueue_p->len){
case 0: /* if no jobs in queue */
break;
case 1: /* if one job in queue */
jobqueue_p->front = NULL;
jobqueue_p->rear = NULL;
jobqueue_p->len = 0;
break;
default: /* if >1 jobs in queue */
jobqueue_p->front = job_p->prev;
jobqueue_p->len--;
/* more than one job in queue -> post it */
bsem_post(jobqueue_p->has_jobs);
}
pthread_mutex_unlock(&jobqueue_p->rwmutex);
return job_p;
}
/* Free all queue resources back to the system */
static void jobqueue_destroy(jobqueue* jobqueue_p){
jobqueue_clear(jobqueue_p);
free(jobqueue_p->has_jobs);
}
/* ======================== SYNCHRONISATION ========================= */
/* Init semaphore to 1 or 0 */
static void bsem_init(bsem *bsem_p, int value) {
if (value < 0 || value > 1) {
err("bsem_init(): Binary semaphore can take only values 1 or 0");
exit(1);
}
pthread_mutex_init(&(bsem_p->mutex), NULL);
pthread_cond_init(&(bsem_p->cond), NULL);
bsem_p->v = value;
}
/* Reset semaphore to 0 */
static void bsem_reset(bsem *bsem_p) {
pthread_mutex_destroy(&(bsem_p->mutex));
pthread_cond_destroy(&(bsem_p->cond));
bsem_init(bsem_p, 0);
}
/* Post to at least one thread */
static void bsem_post(bsem *bsem_p) {
pthread_mutex_lock(&bsem_p->mutex);
bsem_p->v = 1;
pthread_cond_signal(&bsem_p->cond);
pthread_mutex_unlock(&bsem_p->mutex);
}
/* Post to all threads */
static void bsem_post_all(bsem *bsem_p) {
pthread_mutex_lock(&bsem_p->mutex);
bsem_p->v = 1;
pthread_cond_broadcast(&bsem_p->cond);
pthread_mutex_unlock(&bsem_p->mutex);
}
/* Wait on semaphore until semaphore has value 0 */
static void bsem_wait(bsem* bsem_p) {
pthread_mutex_lock(&bsem_p->mutex);
while (bsem_p->v != 1) {
pthread_cond_wait(&bsem_p->cond, &bsem_p->mutex);
}
bsem_p->v = 0;
pthread_mutex_unlock(&bsem_p->mutex);
}

View File

@@ -1,187 +0,0 @@
/**********************************
* @author Johan Hanssen Seferidis
* License: MIT
*
**********************************/
#ifndef _THPOOL_
#define _THPOOL_
#ifdef __cplusplus
extern "C" {
#endif
/* =================================== API ======================================= */
typedef struct thpool_* threadpool;
/**
* @brief Initialize threadpool
*
* Initializes a threadpool. This function will not return until all
* threads have initialized successfully.
*
* @example
*
* ..
* threadpool thpool; //First we declare a threadpool
* thpool = thpool_init(4); //then we initialize it to 4 threads
* ..
*
* @param num_threads number of threads to be created in the threadpool
* @return threadpool created threadpool on success,
* NULL on error
*/
threadpool thpool_init(int num_threads);
/**
* @brief Add work to the job queue
*
* Takes an action and its argument and adds it to the threadpool's job queue.
* If you want to add to work a function with more than one arguments then
* a way to implement this is by passing a pointer to a structure.
*
* NOTICE: You have to cast both the function and argument to not get warnings.
*
* @example
*
* void print_num(int num){
* printf("%d\n", num);
* }
*
* int main() {
* ..
* int a = 10;
* thpool_add_work(thpool, (void*)print_num, (void*)a);
* ..
* }
*
* @param threadpool threadpool to which the work will be added
* @param function_p pointer to function to add as work
* @param arg_p pointer to an argument
* @return 0 on success, -1 otherwise.
*/
int thpool_add_work(threadpool, void (*function_p)(void*), void* arg_p);
/**
* @brief Wait for all queued jobs to finish
*
* Will wait for all jobs - both queued and currently running to finish.
* Once the queue is empty and all work has completed, the calling thread
* (probably the main program) will continue.
*
* Smart polling is used in wait. The polling is initially 0 - meaning that
* there is virtually no polling at all. If after 1 seconds the threads
* haven't finished, the polling interval starts growing exponentially
* until it reaches max_secs seconds. Then it jumps down to a maximum polling
* interval assuming that heavy processing is being used in the threadpool.
*
* @example
*
* ..
* threadpool thpool = thpool_init(4);
* ..
* // Add a bunch of work
* ..
* thpool_wait(thpool);
* puts("All added work has finished");
* ..
*
* @param threadpool the threadpool to wait for
* @return nothing
*/
void thpool_wait(threadpool);
/**
* @brief Pauses all threads immediately
*
* The threads will be paused no matter if they are idle or working.
* The threads return to their previous states once thpool_resume
* is called.
*
* While the thread is being paused, new work can be added.
*
* @example
*
* threadpool thpool = thpool_init(4);
* thpool_pause(thpool);
* ..
* // Add a bunch of work
* ..
* thpool_resume(thpool); // Let the threads start their magic
*
* @param threadpool the threadpool where the threads should be paused
* @return nothing
*/
void thpool_pause(threadpool);
/**
* @brief Unpauses all threads if they are paused
*
* @example
* ..
* thpool_pause(thpool);
* sleep(10); // Delay execution 10 seconds
* thpool_resume(thpool);
* ..
*
* @param threadpool the threadpool where the threads should be unpaused
* @return nothing
*/
void thpool_resume(threadpool);
/**
* @brief Destroy the threadpool
*
* This will wait for the currently active threads to finish and then 'kill'
* the whole threadpool to free up memory.
*
* @example
* int main() {
* threadpool thpool1 = thpool_init(2);
* threadpool thpool2 = thpool_init(2);
* ..
* thpool_destroy(thpool1);
* ..
* return 0;
* }
*
* @param threadpool the threadpool to destroy
* @return nothing
*/
void thpool_destroy(threadpool);
/**
* @brief Show currently working threads
*
* Working threads are the threads that are performing work (not idle).
*
* @example
* int main() {
* threadpool thpool1 = thpool_init(2);
* threadpool thpool2 = thpool_init(2);
* ..
* printf("Working threads: %d\n", thpool_num_threads_working(thpool1));
* ..
* return 0;
* }
*
* @param threadpool the threadpool of interest
* @return integer number of threads working
*/
int thpool_num_threads_working(threadpool);
#ifdef __cplusplus
}
#endif
#endif

View File

@@ -8,18 +8,21 @@ Build the dev Docker image:
```console
docker build -t frankenphp-dev -f dev.Dockerfile .
docker run --cap-add=SYS_PTRACE --security-opt seccomp=unconfined -p 8080:8080 -p 443:443 -v $PWD:/go/src/app -it frankenphp-dev
docker run --cap-add=SYS_PTRACE --security-opt seccomp=unconfined -p 8080:8080 -p 443:443 -p 443:443/udp -v $PWD:/go/src/app -it frankenphp-dev
```
The image contains the usual development tools (Go, GDB, Valgrind, Neovim...).
The image contains the usual development tools (Go, GDB, Valgrind, Neovim...) and uses the following php setting locations
If docker version is lower than 23.0, build is failed by dockerignore [pattern issue](https://github.com/moby/moby/pull/42676). Add directories to `.dockerignore`.
- php.ini: `/etc/frankenphp/php.ini` A php.ini file with development presets is provided by default.
- additional configuration files: `/etc/frankenphp/php.d/*.ini`
- php extensions: `/usr/lib/frankenphp/modules/`
If your Docker version is lower than 23.0, the build will fail due to dockerignore [pattern issue](https://github.com/moby/moby/pull/42676). Add directories to `.dockerignore`.
```patch
!testdata/*.php
!testdata/*.txt
+!caddy
+!C-Thread-Pool
+!internal
```
@@ -30,7 +33,7 @@ If docker version is lower than 23.0, build is failed by dockerignore [pattern i
## Running the test suite
```console
go test -race -v ./...
go test -tags watcher -race -v ./...
```
## Caddy module
@@ -39,7 +42,7 @@ Build Caddy with the FrankenPHP Caddy module:
```console
cd caddy/frankenphp/
go build
go build -tags watcher,brotli,nobadger,nomysql,nopgx
cd ../../
```
@@ -50,10 +53,13 @@ cd testdata/
../caddy/frankenphp/frankenphp run
```
The server is listening on `127.0.0.1:8080`:
The server is listening on `127.0.0.1:80`:
> [!NOTE]
> if you are using Docker, you will have to either bind container port 80 or execute from inside the container
```console
curl -vk https://localhost/phpinfo.php
curl -vk http://127.0.0.1/phpinfo.php
```
## Minimal test server
@@ -105,69 +111,91 @@ Build FrankenPHP images from scratch for arm64 & amd64 and push to Docker Hub:
docker buildx bake -f docker-bake.hcl --pull --no-cache --push
```
## Debugging Segmentation Faults With Static Builds
1. Download the debug version of the FrankenPHP binary from GitHub or create your custom static build including debug symbols:
```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. Replace your current version of `frankenphp` by the debug FrankenPHP executable
3. Start FrankenPHP as usual (alternatively, you can directly start FrankenPHP with GDB: `gdb --args frankenphp run`)
4. Attach to the process with GDB:
```console
gdb -p `pidof frankenphp`
```
5. If necessary, type `continue` in the GDB shell
6. Make FrankenPHP crash
7. Type `bt` in the GDB shell
8. Copy the output
## Debugging Segmentation Faults in GitHub Actions
1. Open `.github/workflows/tests.yml`
2. Enable PHP debug symbols
```patch
- uses: shivammathur/setup-php@v2
# ...
env:
phpts: ts
+ debug: true
```
```patch
- uses: shivammathur/setup-php@v2
# ...
env:
phpts: ts
+ debug: true
```
3. Enable `tmate` to connect to the container
```patch
-
name: Set CGO flags
run: echo "CGO_CFLAGS=$(php-config --includes)" >> "$GITHUB_ENV"
+ -
+ run: |
+ sudo apt install gdb
+ mkdir -p /home/runner/.config/gdb/
+ printf "set auto-load safe-path /\nhandle SIG34 nostop noprint pass" > /home/runner/.config/gdb/gdbinit
+ -
+ uses: mxschmitt/action-tmate@v3
```
```patch
- name: Set CGO flags
run: echo "CGO_CFLAGS=$(php-config --includes)" >> "$GITHUB_ENV"
+ - run: |
+ sudo apt install gdb
+ mkdir -p /home/runner/.config/gdb/
+ printf "set auto-load safe-path /\nhandle SIG34 nostop noprint pass" > /home/runner/.config/gdb/gdbinit
+ - uses: mxschmitt/action-tmate@v3
```
4. Connect to the container
5. Open `frankenphp.go`
6. Enable `cgosymbolizer`
```patch
- //_ "github.com/ianlancetaylor/cgosymbolizer"
+ _ "github.com/ianlancetaylor/cgosymbolizer"
```
```patch
- //_ "github.com/ianlancetaylor/cgosymbolizer"
+ _ "github.com/ianlancetaylor/cgosymbolizer"
```
7. Download the module: `go get`
8. In the container, you can use GDB and the like:
```console
go test -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. When the bug is fixed, revert all these changes
## Misc Dev Resources
* [PHP embedding in uWSGI](https://github.com/unbit/uwsgi/blob/master/plugins/php/php_plugin.c)
* [PHP embedding in NGINX Unit](https://github.com/nginx/unit/blob/master/src/nxt_php_sapi.c)
* [PHP embedding in Go (go-php)](https://github.com/deuill/go-php)
* [PHP embedding in Go (GoEmPHP)](https://github.com/mikespook/goemphp)
* [PHP embedding in C++](https://gist.github.com/paresy/3cbd4c6a469511ac7479aa0e7c42fea7)
* [Extending and Embedding PHP by Sara Golemon](https://books.google.fr/books?id=zMbGvK17_tYC&pg=PA254&lpg=PA254#v=onepage&q&f=false)
* [What the heck is TSRMLS_CC, anyway?](http://blog.golemon.com/2006/06/what-heck-is-tsrmlscc-anyway.html)
* [PHP embedding on Mac](https://gist.github.com/jonnywang/61427ffc0e8dde74fff40f479d147db4)
* [SDL bindings](https://pkg.go.dev/github.com/veandco/go-sdl2@v0.4.21/sdl#Main)
- [PHP embedding in uWSGI](https://github.com/unbit/uwsgi/blob/master/plugins/php/php_plugin.c)
- [PHP embedding in NGINX Unit](https://github.com/nginx/unit/blob/master/src/nxt_php_sapi.c)
- [PHP embedding in Go (go-php)](https://github.com/deuill/go-php)
- [PHP embedding in Go (GoEmPHP)](https://github.com/mikespook/goemphp)
- [PHP embedding in C++](https://gist.github.com/paresy/3cbd4c6a469511ac7479aa0e7c42fea7)
- [Extending and Embedding PHP by Sara Golemon](https://books.google.fr/books?id=zMbGvK17_tYC&pg=PA254&lpg=PA254#v=onepage&q&f=false)
- [What the heck is TSRMLS_CC, anyway?](http://blog.golemon.com/2006/06/what-heck-is-tsrmlscc-anyway.html)
- [SDL bindings](https://pkg.go.dev/github.com/veandco/go-sdl2@v0.4.21/sdl#Main)
## Docker-Related Resources
* [Bake file definition](https://docs.docker.com/build/customize/bake/file-definition/)
* [docker buildx build](https://docs.docker.com/engine/reference/commandline/buildx_build/)
- [Bake file definition](https://docs.docker.com/build/customize/bake/file-definition/)
- [`docker buildx build`](https://docs.docker.com/engine/reference/commandline/buildx_build/)
## Useful Command
@@ -175,3 +203,17 @@ docker buildx bake -f docker-bake.hcl --pull --no-cache --push
apk add strace util-linux gdb
strace -e 'trace=!futex,epoll_ctl,epoll_pwait,tgkill,rt_sigreturn' -p 1
```
## Translating the Documentation
To translate the documentation and the site in a new language,
follow these steps:
1. Create a new directory named with the language's 2-character ISO code in this repository's `docs/` directory
2. Copy all the `.md` files in the root of the `docs/` directory into the new directory (always use the English version as source for translation, as it's always up to date)
3. Copy the `README.md` and `CONTRIBUTING.md` files from the root directory to the new directory
4. Translate the content of the files, but don't change the filenames, also don't translate strings starting with `> [!` (it's special markup for GitHub)
5. Create a Pull Request with the translations
6. In the [site repository](https://github.com/dunglas/frankenphp-website/tree/main), copy and translate the translation files in the `content/`, `data/` and `i18n/` directories
7. Translate the values in the created YAML file
8. Open a Pull Request on the site repository

View File

@@ -1,34 +1,42 @@
# syntax=docker/dockerfile:1
#checkov:skip=CKV_DOCKER_2
#checkov:skip=CKV_DOCKER_3
#checkov:skip=CKV_DOCKER_7
FROM php-base AS common
WORKDIR /app
RUN apt-get update && \
apt-get -y --no-install-recommends install \
mailcap \
libcap2-bin \
&& \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
apt-get -y --no-install-recommends install \
mailcap \
libcap2-bin \
&& \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
RUN set -eux; \
mkdir -p \
/app/public \
/config/caddy \
/data/caddy \
/etc/caddy; \
/etc/caddy \
/etc/frankenphp; \
sed -i 's/php/frankenphp run/g' /usr/local/bin/docker-php-entrypoint; \
echo '<?php phpinfo();' > /app/public/index.php
COPY --link caddy/frankenphp/Caddyfile /etc/caddy/Caddyfile
COPY --from=mlocati/php-extension-installer /usr/bin/install-php-extensions /usr/local/bin/
RUN ln /etc/caddy/Caddyfile /etc/frankenphp/Caddyfile && \
curl -sSLf \
-o /usr/local/bin/install-php-extensions \
https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions && \
chmod +x /usr/local/bin/install-php-extensions
CMD ["--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"]
CMD ["--config", "/etc/frankenphp/Caddyfile", "--adapter", "caddyfile"]
HEALTHCHECK CMD curl -f http://localhost:2019/metrics || exit 1
# See https://caddyserver.com/docs/conventions#file-locations for details
ENV XDG_CONFIG_HOME /config
ENV XDG_DATA_HOME /data
ENV XDG_CONFIG_HOME=/config
ENV XDG_DATA_HOME=/data
EXPOSE 80
EXPOSE 443
@@ -38,7 +46,7 @@ EXPOSE 2019
LABEL org.opencontainers.image.title=FrankenPHP
LABEL org.opencontainers.image.description="The modern PHP app server"
LABEL org.opencontainers.image.url=https://frankenphp.dev
LABEL org.opencontainers.image.source=https://github.com/dunglas/frankenphp
LABEL org.opencontainers.image.source=https://github.com/php/frankenphp
LABEL org.opencontainers.image.licenses=MIT
LABEL org.opencontainers.image.vendor="Kévin Dunglas"
@@ -50,48 +58,70 @@ SHELL ["/bin/bash", "-o", "pipefail", "-c"]
COPY --from=golang-base /usr/local/go /usr/local/go
ENV PATH /usr/local/go/bin:$PATH
ENV PATH=/usr/local/go/bin:$PATH
ENV GOTOOLCHAIN=local
# This is required to link the FrankenPHP binary to the PHP binary
RUN apt-get update && \
apt-get -y --no-install-recommends install \
libargon2-dev \
libcurl4-openssl-dev \
libonig-dev \
libreadline-dev \
libsodium-dev \
libsqlite3-dev \
libssl-dev \
libxml2-dev \
zlib1g-dev \
&& \
apt-get clean
apt-get -y --no-install-recommends install \
cmake \
git \
libargon2-dev \
libbrotli-dev \
libcurl4-openssl-dev \
libonig-dev \
libreadline-dev \
libsodium-dev \
libsqlite3-dev \
libssl-dev \
libxml2-dev \
zlib1g-dev \
&& \
apt-get clean
# 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 && \
ldconfig
WORKDIR /go/src/app
COPY --link go.mod go.sum ./
RUN go mod graph | awk '{if ($1 !~ "@") print $2}' | xargs go get
RUN go mod download
WORKDIR /go/src/app/caddy
COPY --link caddy/go.mod caddy/go.sum ./
RUN go mod graph | awk '{if ($1 !~ "@") print $2}' | xargs go get
RUN go mod download
WORKDIR /go/src/app
COPY --link *.* ./
COPY --link caddy caddy
COPY --link C-Thread-Pool C-Thread-Pool
COPY --link internal internal
COPY --link testdata testdata
COPY --link . ./
# todo: automate this?
# see https://github.com/docker-library/php/blob/master/8.2/bookworm/zts/Dockerfile#L57-L59 for PHP values
ENV CGO_LDFLAGS="-lssl -lcrypto -lreadline -largon2 -lcurl -lonig -lz $PHP_LDFLAGS" CGO_CFLAGS="-DFRANKENPHP_VERSION=$FRANKENPHP_VERSION $PHP_CFLAGS" CGO_CPPFLAGS=$PHP_CPPFLAGS
# 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"
WORKDIR /go/src/app/caddy/frankenphp
RUN GOBIN=/usr/local/bin go install -ldflags "-w -s -X 'github.com/caddyserver/caddy/v2.CustomVersion=FrankenPHP $FRANKENPHP_VERSION PHP $PHP_VERSION Caddy'" && \
setcap cap_net_bind_service=+ep /usr/local/bin/frankenphp && \
cp Caddyfile /etc/caddy/Caddyfile && \
frankenphp version
RUN GOBIN=/usr/local/bin \
../../go.sh install -ldflags "-w -s -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 && \
cp Caddyfile /etc/frankenphp/Caddyfile && \
frankenphp version && \
frankenphp build-info
WORKDIR /go/src/app
@@ -100,6 +130,14 @@ FROM common AS runner
ENV GODEBUG=cgocheck=0
# copy watcher shared library
COPY --from=builder /usr/local/lib/libwatcher* /usr/local/lib/
# fix for the file watcher on arm
RUN apt-get install -y --no-install-recommends libstdc++6 && \
apt-get clean && \
ldconfig
COPY --from=builder /usr/local/bin/frankenphp /usr/local/bin/frankenphp
RUN setcap cap_net_bind_service=+ep /usr/local/bin/frankenphp && \
frankenphp version
frankenphp version && \
frankenphp build-info

170
README.md
View File

@@ -4,23 +4,114 @@
FrankenPHP is a modern application server for PHP built on top of the [Caddy](https://caddyserver.com/) web server.
FrankenPHP gives superpowers to your PHP apps thanks to its stunning features: [*Early Hints*](https://frankenphp.dev/docs/early-hints/), [worker mode](https://frankenphp.dev/docs/worker/), [real-time capabilities](https://frankenphp.dev/docs/mercure/), automatic HTTPS, HTTP/2, and HTTP/3 support...
FrankenPHP gives superpowers to your PHP apps thanks to its stunning features: [_Early Hints_](https://frankenphp.dev/docs/early-hints/), [worker mode](https://frankenphp.dev/docs/worker/), [real-time capabilities](https://frankenphp.dev/docs/mercure/), automatic HTTPS, HTTP/2, and HTTP/3 support...
FrankenPHP works with any PHP app and makes your Symfony projects faster than ever thanks to the provided integration with the worker mode (Laravel Octane support coming).
FrankenPHP works with any PHP app and makes your Laravel and Symfony projects faster than ever thanks to their official integrations with the worker mode.
FrankenPHP can also be used as a standalone Go library to embed PHP in any app using `net/http`.
[**Learn more** on *frankenphp.dev*](https://frankenphp.dev) and in this slide deck:
[**Learn more** on _frankenphp.dev_](https://frankenphp.dev) and in this slide deck:
<a href="https://dunglas.dev/2022/10/frankenphp-the-modern-php-app-server-written-in-go/"><img src="https://dunglas.dev/wp-content/uploads/2022/10/frankenphp.png" alt="Slides" width="600"></a>
## Getting Started
### Docker
On Windows, use [WSL](https://learn.microsoft.com/windows/wsl/) to run FrankenPHP.
### Install Script
You can copy this line into your terminal to automatically
install an appropriate version for your platform:
```console
docker run -v $PWD:/app/public \
-p 80:80 -p 443:443 \
curl https://frankenphp.dev/install.sh | sh
```
### 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
frankenphp php-server
```
You can also run command-line scripts with:
```console
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:
```console
docker run -v .:/app/public \
-p 80:80 -p 443:443 -p 443:443/udp \
dunglas/frankenphp
```
@@ -28,46 +119,39 @@ Go to `https://localhost`, and enjoy!
> [!TIP]
>
> Do not attempt to use `https://127.0.0.1`. Use `localhost` and accept the self-signed certificate. Caddy has an automatic TLS handling that auto-trusts some local-based hostnames like `localhost`, but it does not apply to IP addresses. More details [on Caddy's "automatic https" docs](https://caddyserver.com/docs/automatic-https#hostname-requirements).
### Standalone Binary
If you prefer not to use Docker, we provide standalone FrankenPHP binaries for Linux and macOS
containing [PHP 8.3](https://www.php.net/releases/8.3/en.php) and most popular PHP extensions: [Download FrankenPHP](https://github.com/dunglas/frankenphp/releases)
To serve the content of the current directory, run:
```console
./frankenphp php-server
```
You can also run command-line scripts with:
```console
./frankenphp php-cli /path/to/your/script.php
```
> 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.
## Docs
* [The worker mode](https://frankenphp.dev/docs/worker/)
* [Early Hints support (103 HTTP status code)](https://frankenphp.dev/docs/early-hints/)
* [Real-time](https://frankenphp.dev/docs/mercure/)
* [Configuration](https://frankenphp.dev/docs/config/)
* [Docker images](https://frankenphp.dev/docs/docker/)
* [Create **standalone**, self-executable PHP apps](https://frankenphp.dev/docs/embed/)
* [Create static binaries](https://frankenphp.dev/docs/static/)
* [Compile from sources](https://frankenphp.dev/docs/compile/)
* [Known issues](https://frankenphp.dev/docs/known-issues/)
* [Demo app (Symfony) and benchmarks](https://github.com/dunglas/frankenphp-demo)
* [Go library documentation](https://pkg.go.dev/github.com/dunglas/frankenphp)
* [Contributing and debugging](https://frankenphp.dev/docs/contributing/)
- [Classic mode](https://frankenphp.dev/docs/classic/)
- [Worker mode](https://frankenphp.dev/docs/worker/)
- [Early Hints support (103 HTTP status code)](https://frankenphp.dev/docs/early-hints/)
- [Real-time](https://frankenphp.dev/docs/mercure/)
- [Efficiently Serving Large Static Files](https://frankenphp.dev/docs/x-sendfile/)
- [Configuration](https://frankenphp.dev/docs/config/)
- [Writing PHP Extensions in Go](https://frankenphp.dev/docs/extensions/)
- [Docker images](https://frankenphp.dev/docs/docker/)
- [Deploy in production](https://frankenphp.dev/docs/production/)
- [Performance optimization](https://frankenphp.dev/docs/performance/)
- [Create **standalone**, self-executable PHP apps](https://frankenphp.dev/docs/embed/)
- [Create static binaries](https://frankenphp.dev/docs/static/)
- [Compile from sources](https://frankenphp.dev/docs/compile/)
- [Monitoring FrankenPHP](https://frankenphp.dev/docs/metrics/)
- [Laravel integration](https://frankenphp.dev/docs/laravel/)
- [Known issues](https://frankenphp.dev/docs/known-issues/)
- [Demo app (Symfony) and benchmarks](https://github.com/dunglas/frankenphp-demo)
- [Go library documentation](https://pkg.go.dev/github.com/dunglas/frankenphp)
- [Contributing and debugging](https://frankenphp.dev/docs/contributing/)
## Examples and Skeletons
* [Symfony](https://github.com/dunglas/symfony-docker)
* [API Platform](https://api-platform.com/docs/distribution/)
* [Laravel](https://frankenphp.dev/docs/laravel/)
* [Sulu](https://sulu.io/blog/running-sulu-with-frankenphp)
* [WordPress](https://github.com/dunglas/frankenphp-wordpress)
* [Drupal](https://github.com/dunglas/frankenphp-drupal)
* [Joomla](https://github.com/alexandreelise/frankenphp-joomla)
- [Symfony](https://github.com/dunglas/symfony-docker)
- [API Platform](https://api-platform.com/docs/symfony)
- [Laravel](https://frankenphp.dev/docs/laravel/)
- [Sulu](https://sulu.io/blog/running-sulu-with-frankenphp)
- [WordPress](https://github.com/StephenMiracle/frankenwp)
- [Drupal](https://github.com/dunglas/frankenphp-drupal)
- [Joomla](https://github.com/alexandreelise/frankenphp-joomla)
- [TYPO3](https://github.com/ochorocho/franken-typo3)
- [Magento2](https://github.com/ekino/frankenphp-magento2)

18
SECURITY.md Normal file
View File

@@ -0,0 +1,18 @@
# Security Policy
## Supported Versions
Only the latest version is supported.
Please ensure that you're always using the latest release.
Binaries and Docker images are rebuilt nightly using the latest versions of dependencies.
## Reporting a Vulnerability
If you believe you have discovered a security issue directly affecting FrankenPHP,
please do **NOT** report it publicly.
Please write a detailed vulnerability report and send it [through GitHub](https://github.com/php/frankenphp/security/advisories/new) or to [kevin+frankenphp-security@dunglas.dev](mailto:kevin+frankenphp-security@dunglas.dev?subject=Security%20issue%20affecting%20FrankenPHP).
Only vulnerabilities directly affecting FrankenPHP should be reported to this project.
Flaws affecting components used by FrankenPHP (PHP, Caddy, Go...) or using FrankenPHP (Laravel Octane, PHP Runtime...) should be reported to the relevant projects.

View File

@@ -1,6 +1,11 @@
# syntax=docker/dockerfile:1
#checkov:skip=CKV_DOCKER_2
#checkov:skip=CKV_DOCKER_3
#checkov:skip=CKV_DOCKER_7
FROM php-base AS common
ARG TARGETARCH
WORKDIR /app
RUN apk add --no-cache \
@@ -13,19 +18,25 @@ RUN set -eux; \
/app/public \
/config/caddy \
/data/caddy \
/etc/caddy; \
/etc/caddy \
/etc/frankenphp; \
sed -i 's/php/frankenphp run/g' /usr/local/bin/docker-php-entrypoint; \
echo '<?php phpinfo();' > /app/public/index.php
COPY --link caddy/frankenphp/Caddyfile /etc/caddy/Caddyfile
COPY --from=mlocati/php-extension-installer /usr/bin/install-php-extensions /usr/local/bin/
CMD ["--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"]
RUN ln /etc/caddy/Caddyfile /etc/frankenphp/Caddyfile && \
curl -sSLf \
-o /usr/local/bin/install-php-extensions \
https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions && \
chmod +x /usr/local/bin/install-php-extensions
CMD ["--config", "/etc/frankenphp/Caddyfile", "--adapter", "caddyfile"]
HEALTHCHECK CMD curl -f http://localhost:2019/metrics || exit 1
# See https://caddyserver.com/docs/conventions#file-locations for details
ENV XDG_CONFIG_HOME /config
ENV XDG_DATA_HOME /data
ENV XDG_CONFIG_HOME=/config
ENV XDG_DATA_HOME=/data
EXPOSE 80
EXPOSE 443
@@ -35,7 +46,7 @@ EXPOSE 2019
LABEL org.opencontainers.image.title=FrankenPHP
LABEL org.opencontainers.image.description="The modern PHP app server"
LABEL org.opencontainers.image.url=https://frankenphp.dev
LABEL org.opencontainers.image.source=https://github.com/dunglas/frankenphp
LABEL org.opencontainers.image.source=https://github.com/php/frankenphp
LABEL org.opencontainers.image.licenses=MIT
LABEL org.opencontainers.image.vendor="Kévin Dunglas"
@@ -43,51 +54,80 @@ LABEL org.opencontainers.image.vendor="Kévin Dunglas"
FROM common AS builder
ARG FRANKENPHP_VERSION='dev'
ARG NO_COMPRESS=''
SHELL ["/bin/ash", "-eo", "pipefail", "-c"]
COPY --link --from=golang-base /usr/local/go /usr/local/go
ENV PATH /usr/local/go/bin:$PATH
ENV PATH=/usr/local/go/bin:$PATH
ENV GOTOOLCHAIN=local
# hadolint ignore=SC2086
RUN apk add --no-cache --virtual .build-deps \
$PHPIZE_DEPS \
argon2-dev \
# Needed for the custom Go build \
bash \
brotli-dev \
coreutils \
curl-dev \
# Needed for the custom Go build \
git \
gnu-libiconv-dev \
libsodium-dev \
# Needed for the file watcher \
cmake \
libstdc++ \
libxml2-dev \
linux-headers \
oniguruma-dev \
openssl-dev \
readline-dev \
sqlite-dev
sqlite-dev \
upx
# 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
WORKDIR /go/src/app
COPY --link go.mod go.sum ./
RUN go mod graph | awk '{if ($1 !~ "@") print $2}' | xargs go get
RUN go mod download
WORKDIR /go/src/app/caddy
COPY caddy/go.mod caddy/go.sum ./
RUN go mod graph | awk '{if ($1 !~ "@") print $2}' | xargs go get
RUN go mod download
WORKDIR /go/src/app
COPY --link *.* ./
COPY --link caddy caddy
COPY --link C-Thread-Pool C-Thread-Pool
COPY --link internal internal
COPY --link testdata testdata
COPY --link . ./
# todo: automate this?
# see https://github.com/docker-library/php/blob/master/8.2/bookworm/zts/Dockerfile#L57-L59 for php values
ENV CGO_LDFLAGS="-lssl -lcrypto -lreadline -largon2 -lcurl -lonig -lz $PHP_LDFLAGS" CGO_CFLAGS="-DFRANKENPHP_VERSION=$FRANKENPHP_VERSION $PHP_CFLAGS" CGO_CPPFLAGS=$PHP_CPPFLAGS
# See https://github.com/docker-library/php/blob/master/8.3/alpine3.20/zts/Dockerfile#L53-L55
ENV CGO_CFLAGS="-DFRANKENPHP_VERSION=$FRANKENPHP_VERSION $PHP_CFLAGS"
ENV CGO_CPPFLAGS=$PHP_CPPFLAGS
ENV CGO_LDFLAGS="-lssl -lcrypto -lreadline -largon2 -lcurl -lonig -lz $PHP_LDFLAGS"
WORKDIR /go/src/app/caddy/frankenphp
RUN GOBIN=/usr/local/bin go install -ldflags "-w -s -extldflags '-Wl,-z,stack-size=0x80000' -X 'github.com/caddyserver/caddy/v2.CustomVersion=FrankenPHP $FRANKENPHP_VERSION PHP $PHP_VERSION Caddy'" && \
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 && \
setcap cap_net_bind_service=+ep /usr/local/bin/frankenphp && \
frankenphp version
([ -z "${NO_COMPRESS}" ] && upx --best /usr/local/bin/frankenphp || true) && \
frankenphp version && \
frankenphp build-info
WORKDIR /go/src/app
@@ -96,6 +136,12 @@ FROM common AS runner
ENV GODEBUG=cgocheck=0
# copy watcher shared library (libgcc and libstdc++ are needed for the watcher)
COPY --from=builder /usr/local/lib/libwatcher* /usr/local/lib/
RUN apk add --no-cache libstdc++ && \
ldconfig /usr/local/lib
COPY --from=builder /usr/local/bin/frankenphp /usr/local/bin/frankenphp
RUN setcap cap_net_bind_service=+ep /usr/local/bin/frankenphp && \
frankenphp version
frankenphp version && \
frankenphp build-info

0
app_checksum.txt Normal file
View File

View File

@@ -1,147 +1,228 @@
#!/bin/sh
#!/bin/bash
set -o errexit
set -x
if ! type "git" > /dev/null; then
echo "The \"git\" command must be installed."
exit 1
if ! type "git" >/dev/null 2>&1; then
echo "The \"git\" command must be installed."
exit 1
fi
CURRENT_DIR=$(pwd)
arch="$(uname -m)"
os="$(uname -s | tr '[:upper:]' '[:lower:]')"
if [ "${os}" = "darwin" ]; then
os="mac"
fi
[ "$os" = "darwin" ] && os="mac"
if [ -z "${PHP_EXTENSIONS}" ]; then
if [ "${os}" = "mac" ] && [ "${arch}" = "x86_64" ]; then
# Temporary fix for https://github.com/crazywhalecc/static-php-cli/issues/278 (remove ldap)
export PHP_EXTENSIONS="apcu,bcmath,bz2,calendar,ctype,curl,dba,dom,exif,fileinfo,filter,gd,iconv,intl,mbregex,mbstring,mysqli,mysqlnd,opcache,openssl,pcntl,pdo,pdo_mysql,pdo_pgsql,pdo_sqlite,pgsql,phar,posix,readline,redis,session,simplexml,sockets,sqlite3,sysvsem,tokenizer,xml,xmlreader,xmlwriter,zip,zlib"
else
export PHP_EXTENSIONS="apcu,bcmath,bz2,calendar,ctype,curl,dba,dom,exif,fileinfo,filter,gd,iconv,intl,ldap,mbregex,mbstring,mysqli,mysqlnd,opcache,openssl,pcntl,pdo,pdo_mysql,pdo_pgsql,pdo_sqlite,pgsql,phar,posix,readline,redis,session,simplexml,sockets,sqlite3,sysvsem,tokenizer,xml,xmlreader,xmlwriter,zip,zlib"
fi
fi
# Supported variables:
# - PHP_VERSION: PHP version to build (default: "8.4")
# - PHP_EXTENSIONS: PHP extensions to build (default: ${defaultExtensions} set below)
# - PHP_EXTENSION_LIBS: PHP extension libraries to build (default: ${defaultExtensionLibs} set below)
# - FRANKENPHP_VERSION: FrankenPHP version (default: current Git commit)
# - EMBED: Path to the PHP app to embed (default: none)
# - DEBUG_SYMBOLS: Enable debug symbols if set to 1 (default: none)
# - MIMALLOC: Use mimalloc as the allocator if set to 1 (default: none)
# - XCADDY_ARGS: Additional arguments to pass to xcaddy
# - RELEASE: [maintainer only] Create a GitHub release if set to 1 (default: none)
if [ -z "${PHP_EXTENSIONS_LIB}" ]; then
export PHP_EXTENSION_LIBS="freetype,libjpeg,libavif,libwebp,libzip,bzip2"
fi
# - SPC_REL_TYPE: Release type to download (accept "source" and "binary", default: "source")
# - SPC_OPT_BUILD_ARGS: Additional arguments to pass to spc build
# - SPC_OPT_DOWNLOAD_ARGS: Additional arguments to pass to spc download
# - SPC_LIBC: Set to glibc to build with GNU toolchain (default: musl)
# init spc command, if we use spc binary, just use it instead of fetching source
if [ -z "${SPC_REL_TYPE}" ]; then
SPC_REL_TYPE="source"
fi
# init spc libc
if [ -z "${SPC_LIBC}" ]; then
if [ "${os}" = "linux" ]; then
SPC_LIBC="musl"
fi
fi
# init spc build additional args
if [ -z "${SPC_OPT_BUILD_ARGS}" ]; then
SPC_OPT_BUILD_ARGS=""
fi
if [ "${SPC_LIBC}" = "musl" ] && [[ "${SPC_OPT_BUILD_ARGS}" != *"--disable-opcache-jit"* ]]; then
SPC_OPT_BUILD_ARGS="${SPC_OPT_BUILD_ARGS} --disable-opcache-jit"
fi
# init spc download additional args
if [ -z "${SPC_OPT_DOWNLOAD_ARGS}" ]; then
SPC_OPT_DOWNLOAD_ARGS="--ignore-cache-sources=php-src --retry 5"
if [ "${SPC_LIBC}" = "musl" ]; then
SPC_OPT_DOWNLOAD_ARGS="${SPC_OPT_DOWNLOAD_ARGS} --prefer-pre-built"
fi
fi
# if we need debug symbols, disable strip
if [ -n "${DEBUG_SYMBOLS}" ]; then
SPC_OPT_BUILD_ARGS="${SPC_OPT_BUILD_ARGS} --no-strip"
fi
# php version to build
if [ -z "${PHP_VERSION}" ]; then
export PHP_VERSION="8.3"
get_latest_php_version() {
input="$1"
json=$(curl -s "https://www.php.net/releases/index.php?json&version=$input")
latest=$(echo "$json" | jq -r '.version')
if [[ "$latest" == "$input"* ]]; then
echo "$latest"
else
echo "$input"
fi
}
PHP_VERSION="$(get_latest_php_version "8.4")"
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,xsl,xz,zip,zlib,yaml,zstd"
defaultExtensionLibs="libavif,nghttp2,nghttp3,ngtcp2,watcher"
if [ -z "${FRANKENPHP_VERSION}" ]; then
FRANKENPHP_VERSION="$(git rev-parse --verify HEAD)"
export FRANKENPHP_VERSION
FRANKENPHP_VERSION="$(git rev-parse --verify HEAD)"
export FRANKENPHP_VERSION
elif [ -d ".git/" ]; then
CURRENT_REF="$(git rev-parse --abbrev-ref HEAD)"
export CURRENT_REF
CURRENT_REF="$(git rev-parse --abbrev-ref HEAD)"
export CURRENT_REF
if echo "${FRANKENPHP_VERSION}" | grep -F -q "."; then
# Tag
git checkout "v${FRANKENPHP_VERSION}"
else
git checkout "${FRANKENPHP_VERSION}"
fi
if echo "${FRANKENPHP_VERSION}" | grep -F -q "."; then
# Tag
# Trim "v" prefix if any
FRANKENPHP_VERSION=${FRANKENPHP_VERSION#v}
export FRANKENPHP_VERSION
git checkout "v${FRANKENPHP_VERSION}"
else
git checkout "${FRANKENPHP_VERSION}"
fi
fi
bin="frankenphp-${os}-${arch}"
if [ -n "${CLEAN}" ]; then
rm -Rf dist/
go clean -cache
rm -Rf dist/
go clean -cache
fi
# Build libphp if ncessary
if [ -f "dist/static-php-cli/buildroot/lib/libphp.a" ]; then
cd dist/static-php-cli
mkdir -p dist/
cd dist/
if type "brew" >/dev/null 2>&1; then
if ! type "composer" >/dev/null; then
packages="composer"
fi
if ! type "go" >/dev/null 2>&1; then
packages="${packages} go"
fi
if [ -n "${RELEASE}" ] && ! type "gh" >/dev/null 2>&1; then
packages="${packages} gh"
fi
if [ -n "${packages}" ]; then
# shellcheck disable=SC2086
brew install --formula --quiet ${packages}
fi
fi
if [ "${SPC_REL_TYPE}" = "binary" ]; then
mkdir -p static-php-cli/
cd static-php-cli/
if [[ "${arch}" =~ "arm" ]]; then
dl_arch="aarch64"
else
dl_arch="${arch}"
fi
curl -o spc -fsSL "https://dl.static-php.dev/static-php-cli/spc-bin/nightly/spc-linux-${dl_arch}"
chmod +x spc
spcCommand="./spc"
elif [ -d "static-php-cli/src" ]; then
cd static-php-cli/
git pull
composer install --no-dev -a --no-interaction
spcCommand="./bin/spc"
else
mkdir -p dist/
cd dist/
if [ -d "static-php-cli/" ]; then
cd static-php-cli/
git pull
else
git clone --depth 1 https://github.com/crazywhalecc/static-php-cli
cd static-php-cli/
fi
if type "brew" > /dev/null; then
packages="composer"
if ! type "go" > /dev/null; then
packages="${packages} go"
fi
if [ -n "${RELEASE}" ]; then
packages="${packages} gh"
fi
# shellcheck disable=SC2086
brew install --formula --quiet ${packages}
fi
composer install --no-dev -a
if [ "${os}" = "linux" ]; then
extraOpts="--disable-opcache-jit"
fi
if [ -n "${DEBUG_SYMBOLS}" ]; then
extraOpts="${extraOpts} --no-strip"
fi
./bin/spc doctor
./bin/spc fetch --with-php="${PHP_VERSION}" --for-extensions="${PHP_EXTENSIONS}"
# shellcheck disable=SC2086
./bin/spc build --debug --enable-zts --build-embed ${extraOpts} "${PHP_EXTENSIONS}" --with-libs="${PHP_EXTENSION_LIBS}"
git clone --depth 1 https://github.com/crazywhalecc/static-php-cli --branch main
cd static-php-cli/
composer install --no-dev -a --no-interaction
spcCommand="./bin/spc"
fi
CGO_CFLAGS="-DFRANKENPHP_VERSION=${FRANKENPHP_VERSION} $(./buildroot/bin/php-config --includes | sed s#-I/#-I"${PWD}"/buildroot/#g)"
if [ -n "${DEBUG_SYMBOLS}" ]; then
CGO_CFLAGS="-g ${CGO_CFLAGS}"
fi
export CGO_CFLAGS
if [ "${os}" = "mac" ]; then
export CGO_LDFLAGS="-framework CoreFoundation -framework SystemConfiguration"
# Extensions to build
if [ -z "${PHP_EXTENSIONS}" ]; then
# enable EMBED mode, first check if project has dumped extensions
if [ -n "${EMBED}" ] && [ -f "${EMBED}/composer.json" ] && [ -f "${EMBED}/composer.lock" ] && [ -f "${EMBED}/vendor/installed.json" ]; then
cd "${EMBED}"
# read the extensions using spc dump-extensions
PHP_EXTENSIONS=$(${spcCommand} dump-extensions "${EMBED}" --format=text --no-dev --no-ext-output="${defaultExtensions}")
else
PHP_EXTENSIONS="${defaultExtensions}"
fi
fi
CGO_LDFLAGS="${CGO_LDFLAGS} $(./buildroot/bin/php-config --ldflags) $(./buildroot/bin/php-config --libs)"
export CGO_LDFLAGS
# Additional libraries to build
if [ -z "${PHP_EXTENSION_LIBS}" ]; then
PHP_EXTENSION_LIBS="${defaultExtensionLibs}"
fi
LIBPHP_VERSION="$(./buildroot/bin/php-config --version)"
export LIBPHP_VERSION
# The Brotli library must always be built as it is required by http://github.com/dunglas/caddy-cbrotli
if ! echo "${PHP_EXTENSION_LIBS}" | grep -q "\bbrotli\b"; then
PHP_EXTENSION_LIBS="${PHP_EXTENSION_LIBS},brotli"
fi
cd ../..
# The mimalloc library must be built if MIMALLOC is true
if [ -n "${MIMALLOC}" ]; then
if ! echo "${PHP_EXTENSION_LIBS}" | grep -q "\bmimalloc\b"; then
PHP_EXTENSION_LIBS="${PHP_EXTENSION_LIBS},mimalloc"
fi
fi
# Embed PHP app, if any
if [ -n "${EMBED}" ] && [ -d "${EMBED}" ]; then
tar -cf app.tar -C "${EMBED}" .
SPC_OPT_BUILD_ARGS="${SPC_OPT_BUILD_ARGS} --with-frankenphp-app=${EMBED}"
fi
if [ "${os}" = "linux" ]; then
extraExtldflags="-Wl,-z,stack-size=0x80000"
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
if [ -z "${DEBUG_SYMBOLS}" ]; then
extraLdflags="-w -s"
export SPC_DEFAULT_C_FLAGS="-fPIC -O2"
if [ -n "${DEBUG_SYMBOLS}" ]; then
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
${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 [ -n "$CI" ]; then
rm -rf ./downloads
rm -rf ./source
fi
cd caddy/frankenphp/
go env
go build -buildmode=pie -tags "cgo netgo osusergo static_build" -ldflags "-linkmode=external -extldflags '-static-pie ${extraExtldflags}' ${extraLdflags} -X 'github.com/caddyserver/caddy/v2.CustomVersion=FrankenPHP ${FRANKENPHP_VERSION} PHP ${LIBPHP_VERSION} Caddy'" -o "../../dist/${bin}"
cd ../..
if [ -d "${EMBED}" ]; then
truncate -s 0 app.tar
fi
"dist/${bin}" version
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
git checkout "${CURRENT_REF}"
git checkout "${CURRENT_REF}"
fi

65
caddy/admin.go Normal file
View File

@@ -0,0 +1,65 @@
package caddy
import (
"encoding/json"
"fmt"
"github.com/caddyserver/caddy/v2"
"github.com/dunglas/frankenphp"
"net/http"
)
type FrankenPHPAdmin struct{}
// if the id starts with "admin.api" the module will register AdminRoutes via module.Routes()
func (FrankenPHPAdmin) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: "admin.api.frankenphp",
New: func() caddy.Module { return new(FrankenPHPAdmin) },
}
}
// EXPERIMENTAL: These routes are not yet stable and may change in the future.
func (admin FrankenPHPAdmin) Routes() []caddy.AdminRoute {
return []caddy.AdminRoute{
{
Pattern: "/frankenphp/workers/restart",
Handler: caddy.AdminHandlerFunc(admin.restartWorkers),
},
{
Pattern: "/frankenphp/threads",
Handler: caddy.AdminHandlerFunc(admin.threads),
},
}
}
func (admin *FrankenPHPAdmin) restartWorkers(w http.ResponseWriter, r *http.Request) error {
if r.Method != http.MethodPost {
return admin.error(http.StatusMethodNotAllowed, fmt.Errorf("method not allowed"))
}
frankenphp.RestartWorkers()
caddy.Log().Info("workers restarted from admin api")
admin.success(w, "workers restarted successfully\n")
return nil
}
func (admin *FrankenPHPAdmin) threads(w http.ResponseWriter, _ *http.Request) error {
debugState := frankenphp.DebugState()
prettyJson, err := json.MarshalIndent(debugState, "", " ")
if err != nil {
return admin.error(http.StatusInternalServerError, err)
}
return admin.success(w, string(prettyJson))
}
func (admin *FrankenPHPAdmin) success(w http.ResponseWriter, message string) error {
w.WriteHeader(http.StatusOK)
_, err := w.Write([]byte(message))
return err
}
func (admin *FrankenPHPAdmin) error(statusCode int, err error) error {
return caddy.APIError{HTTPStatus: statusCode, Err: err}
}

303
caddy/admin_test.go Normal file
View File

@@ -0,0 +1,303 @@
package caddy_test
import (
"bytes"
"encoding/json"
"fmt"
"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"
)
func TestRestartWorkerViaAdminApi(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
skip_install_trust
admin localhost:2999
http_port `+testPort+`
frankenphp {
worker ../testdata/worker-with-counter.php 1
}
}
localhost:`+testPort+` {
route {
root ../testdata
rewrite worker-with-counter.php
php
}
}
`, "caddyfile")
tester.AssertGetResponse("http://localhost:"+testPort+"/", http.StatusOK, "requests:1")
tester.AssertGetResponse("http://localhost:"+testPort+"/", http.StatusOK, "requests:2")
assertAdminResponse(t, tester, "POST", "workers/restart", http.StatusOK, "workers restarted successfully\n")
tester.AssertGetResponse("http://localhost:"+testPort+"/", http.StatusOK, "requests:1")
}
func TestShowTheCorrectThreadDebugStatus(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
skip_install_trust
admin localhost:2999
http_port `+testPort+`
frankenphp {
num_threads 3
max_threads 6
worker ../testdata/worker-with-counter.php 1
worker ../testdata/index.php 1
}
}
localhost:`+testPort+` {
route {
root ../testdata
rewrite worker-with-counter.php
php
}
}
`, "caddyfile")
debugState := getDebugState(t, tester)
// assert that the correct threads are present in the thread info
assert.Equal(t, debugState.ThreadDebugStates[0].State, "ready")
assert.Contains(t, debugState.ThreadDebugStates[1].Name, "worker-with-counter.php")
assert.Contains(t, debugState.ThreadDebugStates[2].Name, "index.php")
assert.Equal(t, debugState.ReservedThreadCount, 3)
assert.Len(t, debugState.ThreadDebugStates, 3)
}
func TestAutoScaleWorkerThreads(t *testing.T) {
wg := sync.WaitGroup{}
maxTries := 10
requestsPerTry := 200
tester := caddytest.NewTester(t)
tester.InitServer(`
{
skip_install_trust
admin localhost:2999
http_port `+testPort+`
frankenphp {
max_threads 10
num_threads 2
worker ../testdata/sleep.php {
num 1
max_threads 3
}
}
}
localhost:`+testPort+` {
route {
root ../testdata
rewrite sleep.php
php
}
}
`, "caddyfile")
// spam an endpoint that simulates IO
endpoint := "http://localhost:" + testPort + "/?sleep=2&work=1000"
amountOfThreads := getNumThreads(t, tester)
// try to spawn the additional threads by spamming the server
for range maxTries {
wg.Add(requestsPerTry)
for range requestsPerTry {
go func() {
tester.AssertGetResponse(endpoint, http.StatusOK, "slept for 2 ms and worked for 1000 iterations")
wg.Done()
}()
}
wg.Wait()
amountOfThreads = getNumThreads(t, tester)
if amountOfThreads > 2 {
break
}
}
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
func TestAutoScaleRegularThreadsOnAutomaticThreadLimit(t *testing.T) {
wg := sync.WaitGroup{}
maxTries := 10
requestsPerTry := 200
tester := caddytest.NewTester(t)
tester.InitServer(`
{
skip_install_trust
admin localhost:2999
http_port `+testPort+`
frankenphp {
max_threads auto
num_threads 1
php_ini memory_limit 40M # a reasonable limit for the test
}
}
localhost:`+testPort+` {
route {
root ../testdata
php
}
}
`, "caddyfile")
// spam an endpoint that simulates IO
endpoint := "http://localhost:" + testPort + "/sleep.php?sleep=2&work=1000"
amountOfThreads := getNumThreads(t, tester)
// try to spawn the additional threads by spamming the server
for range maxTries {
wg.Add(requestsPerTry)
for range requestsPerTry {
go func() {
tester.AssertGetResponse(endpoint, http.StatusOK, "slept for 2 ms and worked for 1000 iterations")
wg.Done()
}()
}
wg.Wait()
amountOfThreads = getNumThreads(t, tester)
if amountOfThreads > 1 {
break
}
}
// assert that there are now more threads present
assert.NotEqual(t, amountOfThreads, 1)
}
func assertAdminResponse(t *testing.T, tester *caddytest.Tester, method string, path string, expectedStatus int, expectedBody string) {
adminUrl := "http://localhost:2999/frankenphp/"
r, err := http.NewRequest(method, adminUrl+path, nil)
assert.NoError(t, err)
if expectedBody == "" {
_ = tester.AssertResponseCode(r, expectedStatus)
return
}
_, _ = tester.AssertResponse(r, expectedStatus, expectedBody)
}
func getAdminResponseBody(t *testing.T, tester *caddytest.Tester, method string, path string) string {
adminUrl := "http://localhost:2999/frankenphp/"
r, err := http.NewRequest(method, adminUrl+path, nil)
assert.NoError(t, err)
resp := tester.AssertResponseCode(r, http.StatusOK)
defer resp.Body.Close()
bytes, err := io.ReadAll(resp.Body)
assert.NoError(t, err)
return string(bytes)
}
func getDebugState(t *testing.T, tester *caddytest.Tester) frankenphp.FrankenPHPDebugState {
t.Helper()
threadStates := getAdminResponseBody(t, tester, "GET", "threads")
var debugStates frankenphp.FrankenPHPDebugState
err := json.Unmarshal([]byte(threadStates), &debugStates)
assert.NoError(t, err)
return debugStates
}
func getNumThreads(t *testing.T, tester *caddytest.Tester) int {
t.Helper()
return len(getDebugState(t, tester).ThreadDebugStates)
}
func TestAddModuleWorkerViaAdminApi(t *testing.T) {
// Initialize a server with admin API enabled
tester := caddytest.NewTester(t)
tester.InitServer(`
{
skip_install_trust
admin localhost:2999
http_port `+testPort+`
}
localhost:`+testPort+` {
route {
root ../testdata
php
}
}
`, "caddyfile")
// Get initial debug state to check number of workers
initialDebugState := getDebugState(t, tester)
initialWorkerCount := 0
for _, thread := range initialDebugState.ThreadDebugStates {
if thread.Name != "" && thread.Name != "ready" {
initialWorkerCount++
}
}
// Create a Caddyfile configuration with a module worker
workerConfig := `
{
skip_install_trust
admin localhost:2999
http_port ` + testPort + `
}
localhost:` + testPort + ` {
route {
root ../testdata
php {
worker ../testdata/worker-with-counter.php 1
}
}
}
`
// Send the configuration to the admin API
adminUrl := "http://localhost:2999/load"
r, err := http.NewRequest("POST", adminUrl, bytes.NewBufferString(workerConfig))
assert.NoError(t, err)
r.Header.Set("Content-Type", "text/caddyfile")
resp := tester.AssertResponseCode(r, http.StatusOK)
defer resp.Body.Close()
// Get the updated debug state to check if the worker was added
updatedDebugState := getDebugState(t, tester)
updatedWorkerCount := 0
workerFound := false
filename, _ := fastabs.FastAbs("../testdata/worker-with-counter.php")
for _, thread := range updatedDebugState.ThreadDebugStates {
if thread.Name != "" && thread.Name != "ready" {
updatedWorkerCount++
if thread.Name == "Worker PHP Thread - "+filename {
workerFound = true
}
}
}
// Assert that the worker was added
assert.Greater(t, updatedWorkerCount, initialWorkerCount, "Worker count should have increased")
assert.True(t, workerFound, fmt.Sprintf("Worker with name %q should be found", "Worker PHP Thread - "+filename))
// Make a request to the worker to verify it's working
tester.AssertGetResponse("http://localhost:"+testPort+"/worker-with-counter.php", http.StatusOK, "requests:1")
}

329
caddy/app.go Normal file
View File

@@ -0,0 +1,329 @@
package caddy
import (
"context"
"errors"
"fmt"
"log/slog"
"path/filepath"
"strconv"
"strings"
"sync"
"time"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/dunglas/frankenphp"
"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
//
// {
// frankenphp {
// num_threads 20
// }
// }
type FrankenPHPApp struct {
// NumThreads sets the number of PHP threads to start. Default: 2x the number of available CPUs.
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 []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>"`)
// CaddyModule returns the Caddy module information.
func (f FrankenPHPApp) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: "frankenphp",
New: func() caddy.Module { return &f },
}
}
// 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())
}
} else {
// if the http module is not configured (this should never happen) then collect the metrics by default
if errors.Is(err, caddy.ErrNotConfigured) {
f.metrics = frankenphp.NewPrometheusMetrics(ctx.GetMetricsRegistry())
} else {
// the http module failed to provision due to invalid configuration
return fmt.Errorf("failed to provision caddy http: %w", err)
}
}
return nil
}
func (f *FrankenPHPApp) generateUniqueModuleWorkerName(filepath string) string {
var i uint
filepath, _ = fastabs.FastAbs(filepath)
name := "m#" + filepath
retry:
for _, wc := range f.Workers {
if wc.Name == name {
name = fmt.Sprintf("m#%s_%d", filepath, i)
i++
goto retry
}
}
return name
}
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()
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.WithMetrics(f.metrics),
frankenphp.WithPhpIni(f.PhpIni),
frankenphp.WithMaxWaitTime(f.MaxWaitTime),
)
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...),
)
f.opts = append(f.opts, frankenphp.WithWorkers(w.Name, repl.ReplaceKnown(w.FileName, ""), w.Num, w.options...))
}
frankenphp.Shutdown()
if err := frankenphp.Init(f.opts...); err != nil {
return err
}
return nil
}
func (f *FrankenPHPApp) Stop() error {
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'
// https://github.com/caddyserver/caddy/blob/e76405d55058b0a3e5ba222b44b5ef00516116aa/caddy.go#L810
if caddy.Exiting() {
frankenphp.DrainWorkers()
}
// reset the configuration so it doesn't bleed into later tests
f.Workers = nil
f.NumThreads = 0
f.MaxWaitTime = 0
optionsMU.Lock()
options = nil
optionsMU.Unlock()
return nil
}
// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
func (f *FrankenPHPApp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
for d.Next() {
for d.NextBlock(0) {
// when adding a new directive, also update the allowedDirectives error message
switch d.Val() {
case "num_threads":
if !d.NextArg() {
return d.ArgErr()
}
v, err := strconv.ParseUint(d.Val(), 10, 32)
if err != nil {
return err
}
f.NumThreads = int(v)
case "max_threads":
if !d.NextArg() {
return d.ArgErr()
}
if d.Val() == "auto" {
f.MaxThreads = -1
continue
}
v, err := strconv.ParseUint(d.Val(), 10, 32)
if err != nil {
return err
}
f.MaxThreads = int(v)
case "max_wait_time":
if !d.NextArg() {
return d.ArgErr()
}
v, err := time.ParseDuration(d.Val())
if err != nil {
return d.Err("max_wait_time must be a valid duration (example: 10s)")
}
f.MaxWaitTime = v
case "php_ini":
parseIniLine := func(d *caddyfile.Dispenser) error {
key := d.Val()
if !d.NextArg() {
return d.WrapErr(iniError)
}
if f.PhpIni == nil {
f.PhpIni = make(map[string]string)
}
f.PhpIni[key] = d.Val()
if d.NextArg() {
return d.WrapErr(iniError)
}
return nil
}
isBlock := false
for d.NextBlock(1) {
isBlock = true
err := parseIniLine(d)
if err != nil {
return err
}
}
if !isBlock {
if !d.NextArg() {
return d.WrapErr(iniError)
}
err := parseIniLine(d)
if err != nil {
return err
}
}
case "worker":
wc, err := unmarshalWorker(d)
if err != nil {
return err
}
if frankenphp.EmbeddedAppPath != "" && filepath.IsLocal(wc.FileName) {
wc.FileName = filepath.Join(frankenphp.EmbeddedAppPath, wc.FileName)
}
if strings.HasPrefix(wc.Name, "m#") {
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 d.Errf("global workers must not have duplicate filenames: %q", wc.FileName)
}
}
f.Workers = append(f.Workers, wc)
default:
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 d.Err(`"max_threads"" must be greater than or equal to "num_threads"`)
}
return nil
}
func parseGlobalOption(d *caddyfile.Dispenser, _ any) (any, error) {
app := &FrankenPHPApp{}
if err := app.UnmarshalCaddyfile(d); err != nil {
return nil, err
}
// tell Caddyfile adapter that this is the JSON for an app
return httpcaddyfile.App{
Name: "frankenphp",
Value: caddyconfig.JSON(app, nil),
}, nil
}
var (
_ caddy.App = (*FrankenPHPApp)(nil)
_ caddy.Provisioner = (*FrankenPHPApp)(nil)
)

5
caddy/br-skip.go Normal file
View File

@@ -0,0 +1,5 @@
//go:build nobrotli
package caddy
var brotli = false

5
caddy/br.go Normal file
View File

@@ -0,0 +1,5 @@
//go:build !nobrotli
package caddy
var brotli = true

View File

@@ -4,575 +4,32 @@
package caddy
import (
"encoding/json"
"errors"
"net/http"
"path/filepath"
"strconv"
"fmt"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/caddy/v2/modules/caddyhttp/fileserver"
"github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite"
"github.com/dunglas/frankenphp"
"go.uber.org/zap"
)
const defaultDocumentRoot = "public"
const (
defaultDocumentRoot = "public"
defaultWatchPattern = "./**/*.{env,php,twig,yaml,yml}"
)
func init() {
caddy.RegisterModule(FrankenPHPApp{})
caddy.RegisterModule(FrankenPHPModule{})
caddy.RegisterModule(FrankenPHPAdmin{})
httpcaddyfile.RegisterGlobalOption("frankenphp", parseGlobalOption)
httpcaddyfile.RegisterHandlerDirective("php", parseCaddyfile)
httpcaddyfile.RegisterDirectiveOrder("php", "before", "file_server")
httpcaddyfile.RegisterDirective("php_server", parsePhpServer)
httpcaddyfile.RegisterDirectiveOrder("php_server", "before", "file_server")
}
type mainPHPinterpreterKeyType int
var mainPHPInterpreterKey mainPHPinterpreterKeyType
var phpInterpreter = caddy.NewUsagePool()
type phpInterpreterDestructor struct{}
func (phpInterpreterDestructor) Destruct() error {
frankenphp.Shutdown()
return nil
// 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)
}
type workerConfig struct {
// 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"`
// 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"`
}
type FrankenPHPApp struct {
// NumThreads sets the number of PHP threads to start. Default: 2x the number of available CPUs.
NumThreads int `json:"num_threads,omitempty"`
// Workers configures the worker scripts to start.
Workers []workerConfig `json:"workers,omitempty"`
}
// CaddyModule returns the Caddy module information.
func (a FrankenPHPApp) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: "frankenphp",
New: func() caddy.Module { return &a },
}
}
func (f *FrankenPHPApp) Start() error {
repl := caddy.NewReplacer()
logger := caddy.Log()
opts := []frankenphp.Option{frankenphp.WithNumThreads(f.NumThreads), frankenphp.WithLogger(logger)}
for _, w := range f.Workers {
opts = append(opts, frankenphp.WithWorkers(repl.ReplaceKnown(w.FileName, ""), w.Num, w.Env))
}
_, loaded, err := phpInterpreter.LoadOrNew(mainPHPInterpreterKey, func() (caddy.Destructor, error) {
if err := frankenphp.Init(opts...); err != nil {
return nil, err
}
return phpInterpreterDestructor{}, nil
})
if err != nil {
return err
}
if loaded {
frankenphp.Shutdown()
if err := frankenphp.Init(opts...); err != nil {
return err
}
}
return nil
}
func (*FrankenPHPApp) Stop() error {
caddy.Log().Info("FrankenPHP stopped 🐘")
return nil
}
// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
func (f *FrankenPHPApp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
for d.Next() {
for d.NextBlock(0) {
switch d.Val() {
case "num_threads":
if !d.NextArg() {
return d.ArgErr()
}
v, err := strconv.Atoi(d.Val())
if err != nil {
return err
}
f.NumThreads = v
case "worker":
wc := workerConfig{}
if d.NextArg() {
wc.FileName = d.Val()
}
if d.NextArg() {
v, err := strconv.Atoi(d.Val())
if err != nil {
return err
}
wc.Num = v
}
for d.NextBlock(1) {
v := d.Val()
switch v {
case "file":
if !d.NextArg() {
return d.ArgErr()
}
wc.FileName = d.Val()
case "num":
if !d.NextArg() {
return d.ArgErr()
}
v, err := strconv.Atoi(d.Val())
if err != nil {
return err
}
wc.Num = v
case "env":
args := d.RemainingArgs()
if len(args) != 2 {
return d.ArgErr()
}
if wc.Env == nil {
wc.Env = make(map[string]string)
}
wc.Env[args[0]] = args[1]
}
if wc.FileName == "" {
return errors.New(`The "file" argument must be specified`)
}
if frankenphp.EmbeddedAppPath != "" && filepath.IsLocal(wc.FileName) {
wc.FileName = filepath.Join(frankenphp.EmbeddedAppPath, wc.FileName)
}
}
f.Workers = append(f.Workers, wc)
}
}
}
return nil
}
func parseGlobalOption(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
app := &FrankenPHPApp{}
if err := app.UnmarshalCaddyfile(d); err != nil {
return nil, err
}
// tell Caddyfile adapter that this is the JSON for an app
return httpcaddyfile.App{
Name: "frankenphp",
Value: caddyconfig.JSON(app, nil),
}, nil
}
type FrankenPHPModule struct {
// 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`.
SplitPath []string `json:"split_path,omitempty"`
// ResolveRootSymlink enables resolving the `root` directory to its actual value by evaluating a symbolic link, if one exists.
ResolveRootSymlink bool `json:"resolve_root_symlink,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"`
logger *zap.Logger
}
// CaddyModule returns the Caddy module information.
func (FrankenPHPModule) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: "http.handlers.php",
New: func() caddy.Module { return new(FrankenPHPModule) },
}
}
// Provision sets up the module.
func (f *FrankenPHPModule) Provision(ctx caddy.Context) error {
f.logger = ctx.Logger(f)
if f.Root == "" {
if frankenphp.EmbeddedAppPath == "" {
f.Root = "{http.vars.root}"
} else {
f.Root = filepath.Join(frankenphp.EmbeddedAppPath, defaultDocumentRoot)
f.ResolveRootSymlink = false
}
} else {
if frankenphp.EmbeddedAppPath != "" && filepath.IsLocal(f.Root) {
f.Root = filepath.Join(frankenphp.EmbeddedAppPath, f.Root)
}
}
if len(f.SplitPath) == 0 {
f.SplitPath = []string{".php"}
}
return nil
}
// ServeHTTP implements caddyhttp.MiddlewareHandler.
// TODO: Expose TLS versions as env vars, as Apache's mod_ssl: https://github.com/caddyserver/caddy/blob/master/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go#L298
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)
documentRoot := repl.ReplaceKnown(f.Root, "")
env := make(map[string]string, len(f.Env)+1)
env["REQUEST_URI"] = origReq.URL.RequestURI()
for k, v := range f.Env {
env[k] = repl.ReplaceKnown(v, "")
}
fr, err := frankenphp.NewRequestWithContext(
r,
frankenphp.WithRequestDocumentRoot(documentRoot, f.ResolveRootSymlink),
frankenphp.WithRequestSplitPath(f.SplitPath),
frankenphp.WithRequestEnv(env),
)
if err != nil {
return err
}
return frankenphp.ServeHTTP(w, fr)
}
// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
func (f *FrankenPHPModule) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
for d.Next() {
for d.NextBlock(0) {
switch d.Val() {
case "root":
if !d.NextArg() {
return d.ArgErr()
}
f.Root = d.Val()
case "split":
f.SplitPath = d.RemainingArgs()
if len(f.SplitPath) == 0 {
return d.ArgErr()
}
case "env":
args := d.RemainingArgs()
if len(args) != 2 {
return d.ArgErr()
}
if f.Env == nil {
f.Env = make(map[string]string)
}
f.Env[args[0]] = args[1]
case "resolve_root_symlink":
if d.NextArg() {
return d.ArgErr()
}
f.ResolveRootSymlink = true
}
}
}
return nil
}
// parseCaddyfile unmarshals tokens from h into a new Middleware.
func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
m := FrankenPHPModule{}
err := m.UnmarshalCaddyfile(h.Dispenser)
return m, err
}
// parsePhpServer parses the php_server directive, which has a similar syntax
// to the php_fastcgi directive. A line such as this:
//
// php_server
//
// is equivalent to a route consisting of:
//
// # Add trailing slash for directory requests
// @canonicalPath {
// file {path}/index.php
// not path */
// }
// redir @canonicalPath {path}/ 308
//
// # If the requested file does not exist, try index files
// @indexFiles file {
// try_files {path} {path}/index.php index.php
// split_path .php
// }
// rewrite @indexFiles {http.matchers.file.relative}
//
// # FrankenPHP!
// @phpFiles path *.php
// php @phpFiles
// file_server
//
// parsePhpServer is freely inspired from the php_fastgci directive of the Caddy server (Apache License 2.0, Matthew Holt and The Caddy Authors)
func parsePhpServer(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error) {
if !h.Next() {
return nil, h.ArgErr()
}
// set up FrankenPHP
phpsrv := FrankenPHPModule{}
// set up file server
fsrv := fileserver.FileServer{}
disableFsrv := false
// set up the set of file extensions allowed to execute PHP code
extensions := []string{".php"}
// set the default index file for the try_files rewrites
indexFile := "index.php"
// set up for explicitly overriding try_files
tryFiles := []string{}
// if the user specified a matcher token, use that
// matcher in a route that wraps both of our routes;
// either way, strip the matcher token and pass
// the remaining tokens to the unmarshaler so that
// we can gain the rest of the directive syntax
userMatcherSet, err := h.ExtractMatcherSet()
if err != nil {
return nil, err
}
// make a new dispenser from the remaining tokens so that we
// can reset the dispenser back to this point for the
// php unmarshaler to read from it as well
dispenser := h.NewFromNextSegment()
// read the subdirectives that we allow as overrides to
// the php_server shortcut
// NOTE: we delete the tokens as we go so that the php
// unmarshal doesn't see these subdirectives which it cannot handle
for dispenser.Next() {
for dispenser.NextBlock(0) {
// ignore any sub-subdirectives that might
// have the same name somewhere within
// the php passthrough tokens
if dispenser.Nesting() != 1 {
continue
}
// parse the php_server subdirectives
switch dispenser.Val() {
case "root":
if !dispenser.NextArg() {
return nil, dispenser.ArgErr()
}
phpsrv.Root = dispenser.Val()
fsrv.Root = phpsrv.Root
dispenser.DeleteN(2)
case "split":
extensions = dispenser.RemainingArgs()
dispenser.DeleteN(len(extensions) + 1)
if len(extensions) == 0 {
return nil, dispenser.ArgErr()
}
case "index":
args := dispenser.RemainingArgs()
dispenser.DeleteN(len(args) + 1)
if len(args) != 1 {
return nil, dispenser.ArgErr()
}
indexFile = args[0]
case "try_files":
args := dispenser.RemainingArgs()
dispenser.DeleteN(len(args) + 1)
if len(args) < 1 {
return nil, dispenser.ArgErr()
}
tryFiles = args
case "file_server":
args := dispenser.RemainingArgs()
dispenser.DeleteN(len(args) + 1)
if len(args) < 1 || args[0] != "off" {
return nil, dispenser.ArgErr()
}
disableFsrv = true
}
}
}
// reset the dispenser after we're done so that the frankenphp
// unmarshaler can read it from the start
dispenser.Reset()
if frankenphp.EmbeddedAppPath != "" {
if phpsrv.Root == "" {
phpsrv.Root = filepath.Join(frankenphp.EmbeddedAppPath, defaultDocumentRoot)
fsrv.Root = phpsrv.Root
phpsrv.ResolveRootSymlink = false
} else if filepath.IsLocal(fsrv.Root) {
phpsrv.Root = filepath.Join(frankenphp.EmbeddedAppPath, phpsrv.Root)
fsrv.Root = phpsrv.Root
}
}
// set up a route list that we'll append to
routes := caddyhttp.RouteList{}
// set the list of allowed path segments on which to split
phpsrv.SplitPath = extensions
// if the index is turned off, we skip the redirect and try_files
if indexFile != "off" {
// route to redirect to canonical path if index PHP file
redirMatcherSet := caddy.ModuleMap{
"file": h.JSON(fileserver.MatchFile{
TryFiles: []string{"{http.request.uri.path}/" + indexFile},
}),
"not": h.JSON(caddyhttp.MatchNot{
MatcherSetsRaw: []caddy.ModuleMap{
{
"path": h.JSON(caddyhttp.MatchPath{"*/"}),
},
},
}),
}
redirHandler := caddyhttp.StaticResponse{
StatusCode: caddyhttp.WeakString(strconv.Itoa(http.StatusPermanentRedirect)),
Headers: http.Header{"Location": []string{"{http.request.orig_uri.path}/"}},
}
redirRoute := caddyhttp.Route{
MatcherSetsRaw: []caddy.ModuleMap{redirMatcherSet},
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(redirHandler, "handler", "static_response", nil)},
}
// if tryFiles wasn't overridden, use a reasonable default
if len(tryFiles) == 0 {
tryFiles = []string{"{http.request.uri.path}", "{http.request.uri.path}/" + indexFile, indexFile}
}
// route to rewrite to PHP index file
rewriteMatcherSet := caddy.ModuleMap{
"file": h.JSON(fileserver.MatchFile{
TryFiles: tryFiles,
SplitPath: extensions,
}),
}
rewriteHandler := rewrite.Rewrite{
URI: "{http.matchers.file.relative}",
}
rewriteRoute := caddyhttp.Route{
MatcherSetsRaw: []caddy.ModuleMap{rewriteMatcherSet},
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(rewriteHandler, "handler", "rewrite", nil)},
}
routes = append(routes, redirRoute, rewriteRoute)
}
// route to actually pass requests to PHP files;
// match only requests that are for PHP files
pathList := []string{}
for _, ext := range extensions {
pathList = append(pathList, "*"+ext)
}
phpMatcherSet := caddy.ModuleMap{
"path": h.JSON(pathList),
}
// the rest of the config is specified by the user
// using the php directive syntax
dispenser.Next() // consume the directive name
err = phpsrv.UnmarshalCaddyfile(dispenser)
if err != nil {
return nil, err
}
// create the PHP route which is
// conditional on matching PHP files
phpRoute := caddyhttp.Route{
MatcherSetsRaw: []caddy.ModuleMap{phpMatcherSet},
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(phpsrv, "handler", "php", nil)},
}
routes = append(routes, phpRoute)
// create the file server route
if !disableFsrv {
fileRoute := caddyhttp.Route{
MatcherSetsRaw: []caddy.ModuleMap{},
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(fsrv, "handler", "file_server", nil)},
}
routes = append(routes, fileRoute)
}
subroute := caddyhttp.Subroute{
Routes: routes,
}
// the user's matcher is a prerequisite for ours, so
// wrap ours in a subroute and return that
if userMatcherSet != nil {
return []httpcaddyfile.ConfigValue{
{
Class: "route",
Value: caddyhttp.Route{
MatcherSetsRaw: []caddy.ModuleMap{userMatcherSet},
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(subroute, "handler", "subroute", nil)},
},
},
}, nil
}
// otherwise, return the literal subroute instead of
// individual routes, to ensure they stay together and
// are treated as a single unit, without necessarily
// creating an actual subroute in the output
return []httpcaddyfile.ConfigValue{
{
Class: "route",
Value: subroute,
},
}, nil
}
// Interface guards
var (
_ caddy.App = (*FrankenPHPApp)(nil)
_ caddy.Provisioner = (*FrankenPHPModule)(nil)
_ caddyhttp.MiddlewareHandler = (*FrankenPHPModule)(nil)
_ caddyfile.Unmarshaler = (*FrankenPHPModule)(nil)
)

File diff suppressed because it is too large Load Diff

222
caddy/config_test.go Normal file
View File

@@ -0,0 +1,222 @@
package caddy
import (
"testing"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/stretchr/testify/require"
)
func TestModuleWorkerDuplicateFilenamesFail(t *testing.T) {
// Create a test configuration with duplicate worker filenames
configWithDuplicateFilenames := `
{
php {
worker {
file worker-with-env.php
num 1
}
worker {
file worker-with-env.php
num 2
}
}
}`
// Parse the configuration
d := caddyfile.NewTestDispenser(configWithDuplicateFilenames)
module := &FrankenPHPModule{}
// Unmarshal the configuration
err := module.UnmarshalCaddyfile(d)
// Verify that an error was returned
require.Error(t, err, "Expected an error when two workers in the same module have the same filename")
require.Contains(t, err.Error(), "must not have duplicate filenames", "Error message should mention duplicate filenames")
}
func TestModuleWorkersWithDifferentFilenames(t *testing.T) {
// Create a test configuration with different worker filenames
configWithDifferentFilenames := `
{
php {
worker ../testdata/worker-with-env.php
worker ../testdata/worker-with-counter.php
}
}`
// Parse the configuration
d := caddyfile.NewTestDispenser(configWithDifferentFilenames)
module := &FrankenPHPModule{}
// Unmarshal the configuration
err := module.UnmarshalCaddyfile(d)
// Verify that no error was returned
require.NoError(t, err, "Expected no error when two workers in the same module have different filenames")
// Verify that both workers were added to the module
require.Len(t, module.Workers, 2, "Expected two workers to be added to the module")
require.Equal(t, "../testdata/worker-with-env.php", module.Workers[0].FileName, "First worker should have the correct filename")
require.Equal(t, "../testdata/worker-with-counter.php", module.Workers[1].FileName, "Second worker should have the correct filename")
}
func TestModuleWorkersDifferentNamesSucceed(t *testing.T) {
// Create a test configuration with a worker name
configWithWorkerName1 := `
{
php_server {
worker {
name test-worker-1
file ../testdata/worker-with-env.php
num 1
}
}
}`
// Parse the first configuration
d1 := caddyfile.NewTestDispenser(configWithWorkerName1)
app := &FrankenPHPApp{}
module1 := &FrankenPHPModule{}
// Unmarshal the first configuration
err := module1.UnmarshalCaddyfile(d1)
require.NoError(t, err, "First module should be configured without errors")
// Create a second test configuration with a different worker name
configWithWorkerName2 := `
{
php_server {
worker {
name test-worker-2
file ../testdata/worker-with-env.php
num 1
}
}
}`
// Parse the second configuration
d2 := caddyfile.NewTestDispenser(configWithWorkerName2)
module2 := &FrankenPHPModule{}
// Unmarshal the second configuration
err = module2.UnmarshalCaddyfile(d2)
// Verify that no error was returned
require.NoError(t, err, "Expected no error when two workers have different names")
_, err = app.addModuleWorkers(module1.Workers...)
require.NoError(t, err, "Expected no error when adding the first module workers")
_, err = app.addModuleWorkers(module2.Workers...)
require.NoError(t, err, "Expected no error when adding the second module workers")
// Verify that both workers were added
require.Len(t, app.Workers, 2, "Expected two workers in the app")
require.Equal(t, "m#test-worker-1", app.Workers[0].Name, "First worker should have the correct name")
require.Equal(t, "m#test-worker-2", app.Workers[1].Name, "Second worker should have the correct name")
}
func TestModuleWorkerWithEnvironmentVariables(t *testing.T) {
// Create a test configuration with environment variables
configWithEnv := `
{
php {
worker {
file ../testdata/worker-with-env.php
num 1
env APP_ENV production
env DEBUG true
}
}
}`
// Parse the configuration
d := caddyfile.NewTestDispenser(configWithEnv)
module := &FrankenPHPModule{}
// Unmarshal the configuration
err := module.UnmarshalCaddyfile(d)
// Verify that no error was returned
require.NoError(t, err, "Expected no error when configuring a worker with environment variables")
// Verify that the worker was added to the module
require.Len(t, module.Workers, 1, "Expected one worker to be added to the module")
require.Equal(t, "../testdata/worker-with-env.php", module.Workers[0].FileName, "Worker should have the correct filename")
// Verify that the environment variables were set correctly
require.Len(t, module.Workers[0].Env, 2, "Expected two environment variables")
require.Equal(t, "production", module.Workers[0].Env["APP_ENV"], "APP_ENV should be set to production")
require.Equal(t, "true", module.Workers[0].Env["DEBUG"], "DEBUG should be set to true")
}
func TestModuleWorkerWithWatchConfiguration(t *testing.T) {
// Create a test configuration with watch directories
configWithWatch := `
{
php {
worker {
file ../testdata/worker-with-env.php
num 1
watch
watch ./src/**/*.php
watch ./config/**/*.yaml
}
}
}`
// Parse the configuration
d := caddyfile.NewTestDispenser(configWithWatch)
module := &FrankenPHPModule{}
// Unmarshal the configuration
err := module.UnmarshalCaddyfile(d)
// Verify that no error was returned
require.NoError(t, err, "Expected no error when configuring a worker with watch directories")
// Verify that the worker was added to the module
require.Len(t, module.Workers, 1, "Expected one worker to be added to the module")
require.Equal(t, "../testdata/worker-with-env.php", module.Workers[0].FileName, "Worker should have the correct filename")
// Verify that the watch directories were set correctly
require.Len(t, module.Workers[0].Watch, 3, "Expected three watch patterns")
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")
}
func TestModuleWorkerWithCustomName(t *testing.T) {
// Create a test configuration with a custom worker name
configWithCustomName := `
{
php {
worker {
file ../testdata/worker-with-env.php
num 1
name custom-worker-name
}
}
}`
// Parse the configuration
d := caddyfile.NewTestDispenser(configWithCustomName)
module := &FrankenPHPModule{}
app := &FrankenPHPApp{}
// Unmarshal the configuration
err := module.UnmarshalCaddyfile(d)
// Verify that no error was returned
require.NoError(t, err, "Expected no error when configuring a worker with a custom name")
// Verify that the worker was added to the module
require.Len(t, module.Workers, 1, "Expected one worker to be added to the module")
require.Equal(t, "../testdata/worker-with-env.php", module.Workers[0].FileName, "Worker should have the correct filename")
// Verify that the worker was added to app.Workers with the m# prefix
module.Workers, err = app.addModuleWorkers(module.Workers...)
require.NoError(t, err, "Expected no error when adding the worker to the app")
require.Equal(t, "m#custom-worker-name", module.Workers[0].Name, "Worker should have the custom name, prefixed with m#")
require.Equal(t, "m#custom-worker-name", app.Workers[0].Name, "Worker should have the custom name, prefixed with m#")
}

48
caddy/extinit.go Normal file
View File

@@ -0,0 +1,48 @@
package caddy
import (
"errors"
"log"
"os"
"path/filepath"
"strings"
"github.com/dunglas/frankenphp/internal/extgen"
caddycmd "github.com/caddyserver/caddy/v2/cmd"
"github.com/spf13/cobra"
)
func init() {
caddycmd.RegisterCommand(caddycmd.Command{
Name: "extension-init",
Usage: "go_extension.go [--verbose]",
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) {
cmd.Flags().BoolP("debug", "v", false, "Enable verbose debug logs")
cmd.RunE = caddycmd.WrapCommandFuncForCobra(cmdInitExtension)
},
})
}
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"))
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 directory %q", baseName, generator.BuildDir)
return 0, nil
}

View File

@@ -1,40 +1,35 @@
# The Caddyfile is an easy way to configure FrankenPHP and the Caddy web server.
#
# https://frankenphp.dev/docs/config
# https://caddyserver.com/docs/caddyfile
{
skip_install_trust
{$CADDY_GLOBAL_OPTIONS}
frankenphp {
{$FRANKENPHP_CONFIG}
#worker /path/to/your/worker.php
}
}
# https://caddyserver.com/docs/caddyfile/directives#sorting-algorithm
order mercure after encode
order vulcain after reverse_proxy
order php_server before file_server
order php before file_server
{$CADDY_EXTRA_CONFIG}
{$SERVER_NAME:localhost} {
#log {
log {
# Redact the authorization query parameter that can be set by Mercure
format filter {
wrap console
fields {
uri query {
replace authorization REDACTED
}
}
}
}
# # Redact the authorization query parameter that can be set by Mercure
# format filter {
# request>uri query {
# replace authorization REDACTED
# }
# }
#}
root {$SERVER_ROOT:public/}
root * public/
encode zstd gzip
encode zstd br gzip
# Uncomment the following lines to enable Mercure and Vulcain modules
#mercure {
# # Publisher JWT key
# # Transport to use (default to Bolt)
# transport_url {$MERCURE_TRANSPORT_URL:bolt:///data/mercure.db}
# 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}
@@ -50,5 +45,13 @@
{$CADDY_SERVER_EXTRA_DIRECTIVES}
php_server {
php_server
#worker /path/to/your/worker.php
}
}
# As an alternative to editing the above site block, you can add your own site
# block files in the Caddyfile.d directory, and they will be included as long
# as they use the .caddyfile extension.
import Caddyfile.d/*.caddyfile
import Caddyfile.d/*.caddyfile

View File

@@ -1,24 +1,16 @@
package main
import (
"github.com/caddyserver/caddy/v2"
caddycmd "github.com/caddyserver/caddy/v2/cmd"
"go.uber.org/automaxprocs/maxprocs"
"go.uber.org/zap"
// plug in Caddy modules here.
_ "github.com/caddyserver/caddy/v2/modules/standard"
_ "github.com/dunglas/caddy-cbrotli"
_ "github.com/dunglas/frankenphp/caddy"
_ "github.com/dunglas/mercure/caddy"
_ "github.com/dunglas/vulcain/caddy"
)
func main() {
undo, err := maxprocs.Set()
defer undo()
if err != nil {
caddy.Log().Warn("failed to set GOMAXPROCS", zap.Error(err))
}
caddycmd.Main()
}

View File

@@ -1,193 +1,212 @@
module github.com/dunglas/frankenphp/caddy
go 1.21
go 1.25.4
replace github.com/dunglas/frankenphp => ../
retract v1.0.0-rc.1 // Human error
require (
github.com/caddyserver/caddy/v2 v2.7.6
github.com/caddyserver/certmagic v0.20.0
github.com/dunglas/frankenphp v1.0.0
github.com/dunglas/mercure/caddy v0.15.6
github.com/dunglas/vulcain/caddy v1.0.0
github.com/spf13/cobra v1.8.0
go.uber.org/automaxprocs v1.5.3
go.uber.org/zap v1.26.0
github.com/caddyserver/caddy/v2 v2.10.2
github.com/caddyserver/certmagic v0.25.0
github.com/dunglas/caddy-cbrotli v1.0.1
github.com/dunglas/frankenphp v1.11.0
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.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.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.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.3.2 // indirect
github.com/BurntSushi/toml v1.5.0 // indirect
github.com/KimMachineGun/automemlimit v0.7.5 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.2.1 // indirect
github.com/Masterminds/sprig/v3 v3.2.3 // 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/Microsoft/go-winio v0.6.1 // indirect
github.com/RoaringBitmap/roaring v1.6.0 // indirect
github.com/alecthomas/chroma/v2 v2.12.0 // indirect
github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df // 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.11.0 // indirect
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
github.com/bits-and-blooms/bitset v1.24.4 // indirect
github.com/caddyserver/zerossl v0.1.3 // 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.2.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/chzyer/readline v1.5.1 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect
github.com/cloudflare/circl v1.6.1 // 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
github.com/dgraph-io/badger/v2 v2.2007.4 // indirect
github.com/dgraph-io/ristretto v0.1.1 // indirect
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect
github.com/dlclark/regexp2 v1.10.0 // indirect
github.com/dunglas/httpsfv v1.0.2 // indirect
github.com/dunglas/mercure v0.15.6 // indirect
github.com/dunglas/vulcain v1.0.0 // indirect
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/dunglas/httpsfv v1.1.0 // 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/fsnotify/fsnotify v1.7.0 // indirect
github.com/fxamacker/cbor/v2 v2.5.0 // indirect
github.com/getkin/kin-openapi v0.120.0 // indirect
github.com/go-chi/chi/v5 v5.0.10 // indirect
github.com/go-kit/kit v0.13.0 // indirect
github.com/go-kit/log v0.2.1 // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect
github.com/go-logr/logr v1.3.0 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/fxamacker/cbor/v2 v2.9.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.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.20.0 // indirect
github.com/go-openapi/swag v0.22.4 // indirect
github.com/go-sql-driver/mysql v1.7.1 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // 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/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/golang/glog v1.2.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/cel-go v0.15.1 // indirect
github.com/google/certificate-transparency-go v1.1.7 // indirect
github.com/google/go-tpm v0.9.0 // 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.7 // indirect
github.com/google/go-tspi v0.3.0 // indirect
github.com/google/pprof v0.0.0-20231205033806-a5a03c77bf08 // indirect
github.com/google/uuid v1.4.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.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.18.1 // indirect
github.com/hashicorp/golang-lru v1.0.2 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/huandu/xstrings v1.4.0 // indirect
github.com/imdario/mergo v0.3.16 // 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/invopop/yaml v0.2.0 // indirect
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
github.com/jackc/pgconn v1.14.1 // indirect
github.com/jackc/pgio v1.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgproto3/v2 v2.3.2 // indirect
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect
github.com/jackc/pgtype v1.14.0 // indirect
github.com/jackc/pgx/v4 v4.18.1 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // 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.17.4 // indirect
github.com/klauspost/cpuid/v2 v2.2.6 // indirect
github.com/libdns/libdns v0.2.1 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mailru/easyjson v0.7.7 // 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.1 // indirect
github.com/mailru/easyjson v0.9.1 // indirect
github.com/manifoldco/promptui v0.9.0 // indirect
github.com/mastercactapus/proxyprotocol v0.0.4 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
github.com/maypok86/otter/v2 v2.2.1 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/mholt/acmez v1.2.0 // indirect
github.com/miekg/dns v1.1.57 // 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/mapstructure v1.5.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/mschoch/smat v0.2.0 // indirect
github.com/onsi/ginkgo/v2 v2.13.2 // indirect
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/perimeterx/marshmallow v1.1.5 // indirect
github.com/pires/go-proxyproto v0.8.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/prometheus/client_golang v1.17.0 // indirect
github.com/prometheus/client_model v0.5.0 // indirect
github.com/prometheus/common v0.45.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect
github.com/quic-go/qpack v0.4.0 // indirect
github.com/quic-go/qtls-go1-20 v0.4.1 // indirect
github.com/quic-go/quic-go v0.40.0 // indirect
github.com/rs/xid v1.5.0 // 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.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.3.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/shopspring/decimal v1.3.1 // 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.8.0 // indirect
github.com/smallstep/certificates v0.25.2 // indirect
github.com/smallstep/go-attestation v0.4.4-0.20230627102604-cf579e53cbd2 // indirect
github.com/smallstep/nosql v0.6.0 // indirect
github.com/smallstep/pkcs7 v0.0.0-20231107075624-be1870d87d13 // indirect
github.com/smallstep/scep v0.0.0-20231024192529-aee96d7ad34d // 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.0 // indirect
github.com/spf13/afero v1.10.0 // indirect
github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/viper v1.17.0 // indirect
github.com/stoewer/go-strcase v1.3.0 // 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-20230806124524-28a91b69a046 // indirect
github.com/tidwall/gjson v1.17.0 // indirect
github.com/tidwall/match v1.1.1 // 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.2.0 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
github.com/unrolled/secure v1.13.0 // indirect
github.com/urfave/cli v1.22.14 // indirect
github.com/unrolled/secure v1.17.0 // indirect
github.com/urfave/cli v1.22.17 // indirect
github.com/woodsbury/decimal128 v1.4.0 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
github.com/yuin/goldmark v1.6.0 // indirect
github.com/yuin/goldmark v1.7.13 // indirect
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc // indirect
github.com/zeebo/blake3 v0.2.3 // indirect
go.etcd.io/bbolt v1.3.8 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0 // indirect
go.opentelemetry.io/contrib/propagators/autoprop v0.45.0 // indirect
go.opentelemetry.io/contrib/propagators/aws v1.20.0 // indirect
go.opentelemetry.io/contrib/propagators/b3 v1.20.0 // indirect
go.opentelemetry.io/contrib/propagators/jaeger v1.20.0 // indirect
go.opentelemetry.io/contrib/propagators/ot v1.20.0 // indirect
go.opentelemetry.io/otel v1.21.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0 // indirect
go.opentelemetry.io/otel/metric v1.21.0 // indirect
go.opentelemetry.io/otel/sdk v1.21.0 // indirect
go.opentelemetry.io/otel/trace v1.21.0 // indirect
go.opentelemetry.io/proto/otlp v1.0.0 // indirect
go.step.sm/cli-utils v0.8.0 // indirect
go.step.sm/crypto v0.39.0 // indirect
go.step.sm/linkedca v0.20.1 // indirect
go.uber.org/mock v0.3.0 // indirect
github.com/zeebo/blake3 v0.2.4 // indirect
go.etcd.io/bbolt v1.4.3 // 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/multierr v1.11.0 // indirect
golang.org/x/crypto v0.16.0 // indirect
golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb // indirect
golang.org/x/mod v0.14.0 // indirect
golang.org/x/net v0.19.0 // indirect
golang.org/x/sync v0.5.0 // indirect
golang.org/x/sys v0.15.0 // indirect
golang.org/x/term v0.15.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/tools v0.16.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20231127180814-3a041ad873d4 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20231127180814-3a041ad873d4 // indirect
google.golang.org/grpc v1.59.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
go.uber.org/zap v1.27.1 // indirect
go.uber.org/zap/exp v0.3.0 // 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/square/go-jose.v2 v2.6.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
howett.net/plist v1.0.1 // indirect
)

File diff suppressed because it is too large Load Diff

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
}
}

665
caddy/module.go Normal file
View File

@@ -0,0 +1,665 @@
package caddy
import (
"encoding/json"
"errors"
"fmt"
"log/slog"
"net/http"
"path/filepath"
"slices"
"strconv"
"strings"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/caddy/v2/modules/caddyhttp/fileserver"
"github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite"
"github.com/dunglas/frankenphp"
"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"
//
// example.com {
// php_server {
// root /var/www/html
// }
// }
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`.
SplitPath []string `json:"split_path,omitempty"`
// ResolveRootSymlink enables resolving the `root` directory to its actual value by evaluating a symbolic link, if one exists.
ResolveRootSymlink *bool `json:"resolve_root_symlink,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"`
// Workers configures the worker scripts to start.
Workers []workerConfig `json:"workers,omitempty"`
resolvedDocumentRoot string
preparedEnv frankenphp.PreparedEnv
preparedEnvNeedsReplacement bool
logger *slog.Logger
mercureHubRequestOption *frankenphp.RequestOption
}
// CaddyModule returns the Caddy module information.
func (FrankenPHPModule) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: "http.handlers.php",
New: func() caddy.Module { return new(FrankenPHPModule) },
}
}
// Provision sets up the module.
func (f *FrankenPHPModule) Provision(ctx caddy.Context) error {
f.logger = ctx.Slogger()
app, err := ctx.App("frankenphp")
if err != nil {
return err
}
fapp, ok := app.(*FrankenPHPApp)
if !ok {
return fmt.Errorf(`expected ctx.App("frankenphp") to return *FrankenPHPApp, got %T`, app)
}
if fapp == nil {
return fmt.Errorf(`expected ctx.App("frankenphp") to return *FrankenPHPApp, got nil`)
}
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 defined inside php_server
if !filepath.IsAbs(wc.FileName) && f.Root != "" {
wc.FileName = filepath.Join(f.Root, wc.FileName)
}
// Inherit environment variables from the parent php_server directive
if f.Env != nil {
wc.inheritEnv(f.Env)
}
wc.requestOptions = append(wc.requestOptions, loggerOpt)
f.Workers[i] = wc
}
workers, err := fapp.addModuleWorkers(f.Workers...)
if err != nil {
return err
}
f.Workers = workers
if f.Root == "" {
if frankenphp.EmbeddedAppPath == "" {
f.Root = "{http.vars.root}"
} else {
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)
}
if len(f.SplitPath) == 0 {
f.SplitPath = []string{".php"}
}
if f.ResolveRootSymlink == nil {
rrs := true
f.ResolveRootSymlink = &rrs
}
if !needReplacement(f.Root) {
root, err := fastabs.FastAbs(f.Root)
if err != nil {
return fmt.Errorf("unable to make the root path absolute: %w", err)
}
f.resolvedDocumentRoot = root
if *f.ResolveRootSymlink {
root, err := filepath.EvalSymlinks(root)
if err != nil {
return fmt.Errorf("unable to resolve root symlink: %w", err)
}
f.resolvedDocumentRoot = root
}
}
if f.preparedEnv == nil {
f.preparedEnv = frankenphp.PrepareEnv(f.Env)
for _, e := range f.preparedEnv {
if needReplacement(e) {
f.preparedEnvNeedsReplacement = true
break
}
}
}
if err := f.configureHotReload(fapp); err != nil {
return err
}
return nil
}
// needReplacement checks if a string contains placeholders.
func needReplacement(s string) bool {
return strings.ContainsAny(s, "{}")
}
// ServeHTTP implements caddyhttp.MiddlewareHandler.
func (f *FrankenPHPModule) ServeHTTP(w http.ResponseWriter, r *http.Request, _ caddyhttp.Handler) error {
ctx := r.Context()
origReq := ctx.Value(caddyhttp.OriginalRequestCtxKey).(http.Request)
repl := ctx.Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
var (
documentRootOption frankenphp.RequestOption
documentRoot string
)
if f.resolvedDocumentRoot == "" {
documentRoot = repl.ReplaceKnown(f.Root, "")
if documentRoot == "" && frankenphp.EmbeddedAppPath != "" {
documentRoot = frankenphp.EmbeddedAppPath
}
documentRootOption = frankenphp.WithRequestDocumentRoot(documentRoot, *f.ResolveRootSymlink)
} else {
documentRoot = f.resolvedDocumentRoot
documentRootOption = frankenphp.WithRequestResolvedDocumentRoot(documentRoot)
}
env := f.preparedEnv
if f.preparedEnvNeedsReplacement {
env = make(frankenphp.PreparedEnv, len(f.Env))
for k, v := range f.preparedEnv {
env[k] = repl.ReplaceKnown(v, "")
}
}
workerName := ""
for _, w := range f.Workers {
if w.matchesPath(r, documentRoot) {
workerName = w.Name
break
}
}
var (
err error
fr *http.Request
)
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)
}
return nil
}
// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
func (f *FrankenPHPModule) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
for d.Next() {
for d.NextBlock(0) {
switch d.Val() {
case "root":
if !d.NextArg() {
return d.ArgErr()
}
f.Root = d.Val()
case "split":
f.SplitPath = d.RemainingArgs()
if len(f.SplitPath) == 0 {
return d.ArgErr()
}
case "env":
args := d.RemainingArgs()
if len(args) != 2 {
return d.ArgErr()
}
if f.Env == nil {
f.Env = make(map[string]string)
f.preparedEnv = make(frankenphp.PreparedEnv)
}
f.Env[args[0]] = args[1]
f.preparedEnv[args[0]+"\x00"] = args[1]
case "resolve_root_symlink":
if !d.NextArg() {
continue
}
v, err := strconv.ParseBool(d.Val())
if err != nil {
return err
}
if d.NextArg() {
return d.ArgErr()
}
f.ResolveRootSymlink = &v
case "worker":
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:
return wrongSubDirectiveError("php or php_server", "hot_reload, name, root, split, env, resolve_root_symlink, worker", d.Val())
}
}
}
// Check if a worker with this filename already exists in this module
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" or "php_server" block must not have duplicate filenames: %q`, w.FileName)
}
if len(w.MatchPath) == 0 {
fileNames[w.FileName] = struct{}{}
}
}
return nil
}
// parseCaddyfile unmarshals tokens from h into a new Middleware.
func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
m := &FrankenPHPModule{}
err := m.UnmarshalCaddyfile(h.Dispenser)
return m, err
}
// parsePhpServer parses the php_server directive, which has a similar syntax
// to the php_fastcgi directive. A line such as this:
//
// php_server
//
// is equivalent to a route consisting of:
//
// # Add trailing slash for directory requests
// @canonicalPath {
// file {path}/index.php
// not path */
// }
// redir @canonicalPath {path}/ 308
//
// # If the requested file does not exist, try index files
// @indexFiles file {
// try_files {path} {path}/index.php index.php
// split_path .php
// }
// rewrite @indexFiles {http.matchers.file.relative}
//
// # FrankenPHP!
// @phpFiles path *.php
// php @phpFiles
// file_server
//
// parsePhpServer is freely inspired from the php_fastgci directive of the Caddy server (Apache License 2.0, Matthew Holt and The Caddy Authors)
func parsePhpServer(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error) {
if !h.Next() {
return nil, h.ArgErr()
}
// set up FrankenPHP
phpsrv := FrankenPHPModule{}
// set up file server
fsrv := fileserver.FileServer{}
disableFsrv := false
// set up the set of file extensions allowed to execute PHP code
extensions := []string{".php"}
// set the default index file for the try_files rewrites
indexFile := "index.php"
// set up for explicitly overriding try_files
var tryFiles []string
// if the user specified a matcher token, use that
// matcher in a route that wraps both of our routes;
// either way, strip the matcher token and pass
// the remaining tokens to the unmarshaler so that
// we can gain the rest of the directive syntax
userMatcherSet, err := h.ExtractMatcherSet()
if err != nil {
return nil, err
}
// make a new dispenser from the remaining tokens so that we
// can reset the dispenser back to this point for the
// php unmarshaler to read from it as well
dispenser := h.NewFromNextSegment()
// read the subdirectives that we allow as overrides to
// the php_server shortcut
// NOTE: we delete the tokens as we go so that the php
// unmarshal doesn't see these subdirectives which it cannot handle
for dispenser.Next() {
for dispenser.NextBlock(0) {
// ignore any sub-subdirectives that might
// have the same name somewhere within
// the php passthrough tokens
if dispenser.Nesting() != 1 {
continue
}
// parse the php_server subdirectives
switch dispenser.Val() {
case "root":
if !dispenser.NextArg() {
return nil, dispenser.ArgErr()
}
phpsrv.Root = dispenser.Val()
fsrv.Root = phpsrv.Root
dispenser.DeleteN(2)
case "split":
extensions = dispenser.RemainingArgs()
dispenser.DeleteN(len(extensions) + 1)
if len(extensions) == 0 {
return nil, dispenser.ArgErr()
}
case "index":
args := dispenser.RemainingArgs()
dispenser.DeleteN(len(args) + 1)
if len(args) != 1 {
return nil, dispenser.ArgErr()
}
indexFile = args[0]
case "try_files":
args := dispenser.RemainingArgs()
dispenser.DeleteN(len(args) + 1)
if len(args) < 1 {
return nil, dispenser.ArgErr()
}
tryFiles = args
case "file_server":
args := dispenser.RemainingArgs()
dispenser.DeleteN(len(args) + 1)
if len(args) < 1 || args[0] != "off" {
return nil, dispenser.ArgErr()
}
disableFsrv = true
}
}
}
// reset the dispenser after we're done so that the frankenphp
// unmarshaler can read it from the start
dispenser.Reset()
// the rest of the config is specified by the user
// using the php directive syntax
dispenser.Next() // consume the directive name
if err := phpsrv.UnmarshalCaddyfile(dispenser); err != nil {
return nil, err
}
if frankenphp.EmbeddedAppPath != "" {
if phpsrv.Root == "" {
phpsrv.Root = filepath.Join(frankenphp.EmbeddedAppPath, defaultDocumentRoot)
fsrv.Root = phpsrv.Root
rrs := false
phpsrv.ResolveRootSymlink = &rrs
} else if filepath.IsLocal(fsrv.Root) {
phpsrv.Root = filepath.Join(frankenphp.EmbeddedAppPath, phpsrv.Root)
fsrv.Root = phpsrv.Root
}
}
// set up a route list that we'll append to
routes := caddyhttp.RouteList{}
// prepend routes from the 'worker match *' directives
routes = prependWorkerRoutes(routes, h, phpsrv, fsrv, disableFsrv)
// set the list of allowed path segments on which to split
phpsrv.SplitPath = extensions
// if the index is turned off, we skip the redirect and try_files
if indexFile != "off" {
dirRedir := false
dirIndex := "{http.request.uri.path}/" + indexFile
tryPolicy := "first_exist_fallback"
// if tryFiles wasn't overridden, use a reasonable default
if len(tryFiles) == 0 {
if disableFsrv {
tryFiles = []string{dirIndex, indexFile}
} else {
tryFiles = []string{"{http.request.uri.path}", dirIndex, indexFile}
}
dirRedir = true
} else {
if !strings.HasSuffix(tryFiles[len(tryFiles)-1], ".php") {
// use first_exist strategy if the last file is not a PHP file
tryPolicy = ""
}
if slices.Contains(tryFiles, dirIndex) {
dirRedir = true
}
}
// route to redirect to canonical path if index PHP file
if dirRedir {
redirMatcherSet := caddy.ModuleMap{
"file": h.JSON(fileserver.MatchFile{
TryFiles: []string{dirIndex},
Root: phpsrv.Root,
}),
"not": h.JSON(caddyhttp.MatchNot{
MatcherSetsRaw: []caddy.ModuleMap{
{
"path": h.JSON(caddyhttp.MatchPath{"*/"}),
},
},
}),
}
redirHandler := caddyhttp.StaticResponse{
StatusCode: caddyhttp.WeakString(strconv.Itoa(http.StatusPermanentRedirect)),
Headers: http.Header{"Location": []string{"{http.request.orig_uri.path}/"}},
}
redirRoute := caddyhttp.Route{
MatcherSetsRaw: []caddy.ModuleMap{redirMatcherSet},
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(redirHandler, "handler", "static_response", nil)},
}
routes = append(routes, redirRoute)
}
// route to rewrite to PHP index file
rewriteMatcherSet := caddy.ModuleMap{
"file": h.JSON(fileserver.MatchFile{
TryFiles: tryFiles,
TryPolicy: tryPolicy,
SplitPath: extensions,
Root: phpsrv.Root,
}),
}
rewriteHandler := rewrite.Rewrite{
URI: "{http.matchers.file.relative}",
}
rewriteRoute := caddyhttp.Route{
MatcherSetsRaw: []caddy.ModuleMap{rewriteMatcherSet},
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(rewriteHandler, "handler", "rewrite", nil)},
}
routes = append(routes, rewriteRoute)
}
// route to actually pass requests to PHP files;
// match only requests that are for PHP files
var pathList []string
for _, ext := range extensions {
pathList = append(pathList, "*"+ext)
}
phpMatcherSet := caddy.ModuleMap{
"path": h.JSON(pathList),
}
// create the PHP route which is
// conditional on matching PHP files
phpRoute := caddyhttp.Route{
MatcherSetsRaw: []caddy.ModuleMap{phpMatcherSet},
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(phpsrv, "handler", "php", nil)},
}
routes = append(routes, phpRoute)
// create the file server route
if !disableFsrv {
fileRoute := caddyhttp.Route{
MatcherSetsRaw: []caddy.ModuleMap{},
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(fsrv, "handler", "file_server", nil)},
}
routes = append(routes, fileRoute)
}
subroute := caddyhttp.Subroute{
Routes: routes,
}
// the user's matcher is a prerequisite for ours, so
// wrap ours in a subroute and return that
if userMatcherSet != nil {
return []httpcaddyfile.ConfigValue{
{
Class: "route",
Value: caddyhttp.Route{
MatcherSetsRaw: []caddy.ModuleMap{userMatcherSet},
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(subroute, "handler", "subroute", nil)},
},
},
}, nil
}
// otherwise, return the literal subroute instead of
// individual routes, to ensure they stay together and
// are treated as a single unit, without necessarily
// creating an actual subroute in the output
return []httpcaddyfile.ConfigValue{
{
Class: "route",
Value: subroute,
},
}, nil
}
// 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 {
var allWorkerMatches caddyhttp.MatchPath
for _, w := range f.Workers {
for _, path := range w.MatchPath {
allWorkerMatches = append(allWorkerMatches, path)
}
}
if len(allWorkerMatches) == 0 {
return routes
}
// if there are match patterns, we need to check for files beforehand
if !disableFsrv {
routes = append(routes, caddyhttp.Route{
MatcherSetsRaw: []caddy.ModuleMap{
{
"file": h.JSON(fileserver.MatchFile{
TryFiles: []string{"{http.request.uri.path}"},
Root: f.Root,
}),
"not": h.JSON(caddyhttp.MatchNot{
MatcherSetsRaw: []caddy.ModuleMap{
{"path": h.JSON(caddyhttp.MatchPath{"*.php"})},
},
}),
},
},
HandlersRaw: []json.RawMessage{
caddyconfig.JSONModuleObject(fsrv, "handler", "file_server", nil),
},
})
}
// forward matching routes to the PHP handler
routes = append(routes, caddyhttp.Route{
MatcherSetsRaw: []caddy.ModuleMap{
{"path": h.JSON(allWorkerMatches)},
},
HandlersRaw: []json.RawMessage{
caddyconfig.JSONModuleObject(f, "handler", "php", nil),
},
})
return routes
}
// Interface guards
var (
_ caddy.Provisioner = (*FrankenPHPModule)(nil)
_ caddyhttp.MiddlewareHandler = (*FrankenPHPModule)(nil)
_ caddyfile.Unmarshaler = (*FrankenPHPModule)(nil)
)

52
caddy/module_test.go Normal file
View File

@@ -0,0 +1,52 @@
package caddy_test
import (
"net/http"
"os"
"strconv"
"testing"
"github.com/caddyserver/caddy/v2/caddytest"
)
func TestRootBehavesTheSameOutsideAndInsidePhpServer(t *testing.T) {
tester := caddytest.NewTester(t)
testPortNum, _ := strconv.Atoi(testPort)
testPortTwo := strconv.Itoa(testPortNum + 1)
expectedFileResponse, _ := os.ReadFile("../testdata/files/static.txt")
hostWithRootOutside := "http://localhost:" + testPort
hostWithRootInside := "http://localhost:" + testPortTwo
tester.InitServer(`
{
skip_install_trust
admin localhost:2999
}
`+hostWithRootOutside+` {
root ../testdata
php_server
}
`+hostWithRootInside+` {
php_server {
root ../testdata
}
}
`, "caddyfile")
// serve a static file
tester.AssertGetResponse(hostWithRootOutside+"/files/static.txt", http.StatusOK, string(expectedFileResponse))
tester.AssertGetResponse(hostWithRootInside+"/files/static.txt", http.StatusOK, string(expectedFileResponse))
// serve a php file
tester.AssertGetResponse(hostWithRootOutside+"/hello.php", http.StatusOK, "Hello from PHP")
tester.AssertGetResponse(hostWithRootInside+"/hello.php", http.StatusOK, "Hello from PHP")
// fallback to index.php
tester.AssertGetResponse(hostWithRootOutside+"/some-path", http.StatusOK, "I am by birth a Genevese (i not set)")
tester.AssertGetResponse(hostWithRootInside+"/some-path", http.StatusOK, "I am by birth a Genevese (i not set)")
// fallback to directory index ('dirIndex' in module.go)
tester.AssertGetResponse(hostWithRootOutside+"/dirindex/", http.StatusOK, "Hello from directory index.php")
tester.AssertGetResponse(hostWithRootInside+"/dirindex/", http.StatusOK, "Hello from directory index.php")
}

View File

@@ -37,7 +37,13 @@ func cmdPHPCLI(fs caddycmd.Flags) (int, error) {
}
}
status := frankenphp.ExecuteScriptCLI(args[0], args)
var status int
if len(args) >= 2 && args[0] == "-r" {
status = frankenphp.ExecutePHPCode(args[1])
} else {
status = frankenphp.ExecuteScriptCLI(args[0], args)
}
os.Exit(status)
return status, nil

View File

@@ -3,12 +3,15 @@ package caddy
import (
"encoding/json"
"log"
"log/slog"
"net/http"
"path/filepath"
"os"
"strconv"
"strings"
"time"
mercureModule "github.com/dunglas/mercure/caddy"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
caddycmd "github.com/caddyserver/caddy/v2/cmd"
@@ -18,7 +21,6 @@ import (
"github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite"
"github.com/caddyserver/certmagic"
"github.com/dunglas/frankenphp"
"go.uber.org/zap"
"github.com/spf13/cobra"
)
@@ -26,7 +28,7 @@ import (
func init() {
caddycmd.RegisterCommand(caddycmd.Command{
Name: "php-server",
Usage: "[--domain <example.com>] [--root <path>] [--listen <addr>] [--worker /path/to/worker.php<,nb-workers>] [--access-log] [--debug] [--no-compress]",
Usage: "[--domain=<example.com>] [--root=<path>] [--listen=<addr>] [--worker=/path/to/worker.php<,nb-workers>] [--watch[=<glob-pattern>]]... [--access-log] [--debug] [--no-compress] [--mercure]",
Short: "Spins up a production-ready PHP server",
Long: `
A simple but production-ready PHP server. Useful for quick deployments,
@@ -39,15 +41,20 @@ will be changed to the HTTPS port and the server will use HTTPS. If using
a public domain, ensure A/AAAA records are properly configured before
using this option.
For more advanced use cases, see https://github.com/dunglas/frankenphp/blob/main/docs/config.md`,
For more advanced use cases, see https://github.com/php/frankenphp/blob/main/docs/config.md`,
CobraFunc: func(cmd *cobra.Command) {
cmd.Flags().StringP("domain", "d", "", "Domain name at which to serve the files")
cmd.Flags().StringP("root", "r", "", "The path to the root of the site")
cmd.Flags().StringP("listen", "l", "", "The address to which to bind the listener")
cmd.Flags().StringArrayP("worker", "w", []string{}, "Worker script")
cmd.Flags().StringArray("watch", []string{}, "Glob pattern of directories and files to watch for changes")
cmd.Flags().BoolP("access-log", "a", false, "Enable the access log")
cmd.Flags().BoolP("debug", "v", false, "Enable verbose debug logs")
cmd.Flags().BoolP("no-compress", "", false, "Disable Zstandard and Gzip compression")
cmd.Flags().BoolP("mercure", "m", false, "Enable the built-in Mercure.rocks hub")
cmd.Flags().Bool("no-compress", false, "Disable Zstandard, Brotli and Gzip compression")
cmd.Flags().Lookup("watch").NoOptDefVal = defaultWatchPattern
cmd.RunE = caddycmd.WrapCommandFuncForCobra(cmdPHPServer)
},
})
@@ -63,45 +70,75 @@ func cmdPHPServer(fs caddycmd.Flags) (int, error) {
accessLog := fs.Bool("access-log")
debug := fs.Bool("debug")
compress := !fs.Bool("no-compress")
mercure := fs.Bool("mercure")
workers, err := fs.GetStringArray("worker")
if err != nil {
panic(err)
}
watch, err := fs.GetStringArray("watch")
if err != nil {
panic(err)
}
if frankenphp.EmbeddedAppPath != "" {
if err := os.Chdir(frankenphp.EmbeddedAppPath); err != nil {
return caddy.ExitCodeFailedStartup, err
}
}
var workersOption []workerConfig
if len(workers) != 0 {
workersOption = make([]workerConfig, 0, len(workers))
for _, worker := range workers {
parts := strings.SplitN(worker, ",", 2)
if frankenphp.EmbeddedAppPath != "" && filepath.IsLocal(parts[0]) {
parts[0] = filepath.Join(frankenphp.EmbeddedAppPath, parts[0])
}
var num int
var num uint64
if len(parts) > 1 {
num, _ = strconv.Atoi(parts[1])
num, _ = strconv.ParseUint(parts[1], 10, 32)
}
workersOption = append(workersOption, workerConfig{FileName: parts[0], Num: num})
workersOption = append(workersOption, workerConfig{FileName: parts[0], Num: int(num)})
}
workersOption[0].Watch = watch
}
if frankenphp.EmbeddedAppPath != "" {
if _, err := os.Stat("php.ini"); err == nil {
iniScanDir := os.Getenv("PHP_INI_SCAN_DIR")
if err := os.Setenv("PHP_INI_SCAN_DIR", iniScanDir+":"+frankenphp.EmbeddedAppPath); err != nil {
return caddy.ExitCodeFailedStartup, err
}
}
if _, err := os.Stat("Caddyfile"); err == nil {
config, _, err := caddycmd.LoadConfig("Caddyfile", "caddyfile")
if err != nil {
return caddy.ExitCodeFailedStartup, err
}
if err = caddy.Load(config, true); err != nil {
return caddy.ExitCodeFailedStartup, err
}
select {}
}
if root == "" {
root = filepath.Join(frankenphp.EmbeddedAppPath, defaultDocumentRoot)
} else if filepath.IsLocal(root) {
root = filepath.Join(frankenphp.EmbeddedAppPath, root)
root = defaultDocumentRoot
}
}
const indexFile = "index.php"
extensions := []string{"php"}
extensions := []string{".php"}
tryFiles := []string{"{http.request.uri.path}", "{http.request.uri.path}/" + indexFile, indexFile}
rrs := true
phpHandler := FrankenPHPModule{
Root: root,
SplitPath: extensions,
Root: root,
SplitPath: extensions,
ResolveRootSymlink: &rrs,
}
// route to redirect to canonical path if index PHP file
@@ -145,7 +182,7 @@ func cmdPHPServer(fs caddycmd.Flags) (int, error) {
// route to actually pass requests to PHP files;
// match only requests that are for PHP files
pathList := []string{}
var pathList []string
for _, ext := range extensions {
pathList = append(pathList, "*"+ext)
}
@@ -175,25 +212,79 @@ func cmdPHPServer(fs caddycmd.Flags) (int, error) {
return caddy.ExitCodeFailedStartup, err
}
br, err := caddy.GetModule("http.encoders.br")
if err != nil && brotli {
return caddy.ExitCodeFailedStartup, err
}
zstd, err := caddy.GetModule("http.encoders.zstd")
if err != nil {
return caddy.ExitCodeFailedStartup, err
}
var (
encodings caddy.ModuleMap
prefer []string
)
if brotli {
encodings = caddy.ModuleMap{
"zstd": caddyconfig.JSON(zstd.New(), nil),
"br": caddyconfig.JSON(br.New(), nil),
"gzip": caddyconfig.JSON(gzip.New(), nil),
}
prefer = []string{"zstd", "br", "gzip"}
} else {
encodings = caddy.ModuleMap{
"zstd": caddyconfig.JSON(zstd.New(), nil),
"gzip": caddyconfig.JSON(gzip.New(), nil),
}
prefer = []string{"zstd", "gzip"}
}
encodeRoute := caddyhttp.Route{
MatcherSetsRaw: []caddy.ModuleMap{},
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(encode.Encode{
EncodingsRaw: caddy.ModuleMap{
"zstd": caddyconfig.JSON(zstd.New(), nil),
"gzip": caddyconfig.JSON(gzip.New(), nil),
},
Prefer: []string{"zstd", "gzip"},
EncodingsRaw: encodings,
Prefer: prefer,
}, "handler", "encode", nil)},
}
subroute.Routes = append(caddyhttp.RouteList{encodeRoute}, subroute.Routes...)
}
if mercure {
mercurePublisherJwtKey := os.Getenv("MERCURE_PUBLISHER_JWT_KEY")
if mercurePublisherJwtKey == "" {
panic(`The "MERCURE_PUBLISHER_JWT_KEY" environment variable must be set to use the Mercure.rocks hub`)
}
mercureSubscriberJwtKey := os.Getenv("MERCURE_SUBSCRIBER_JWT_KEY")
if mercureSubscriberJwtKey == "" {
panic(`The "MERCURE_SUBSCRIBER_JWT_KEY" environment variable must be set to use the Mercure.rocks hub`)
}
mercureRoute := caddyhttp.Route{
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(
mercureModule.Mercure{
PublisherJWT: mercureModule.JWTConfig{
Alg: os.Getenv("MERCURE_PUBLISHER_JWT_ALG"),
Key: mercurePublisherJwtKey,
},
SubscriberJWT: mercureModule.JWTConfig{
Alg: os.Getenv("MERCURE_SUBSCRIBER_JWT_ALG"),
Key: mercureSubscriberJwtKey,
},
},
"handler",
"mercure",
nil,
),
},
}
subroute.Routes = append(caddyhttp.RouteList{mercureRoute}, subroute.Routes...)
}
route := caddyhttp.Route{
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(subroute, "handler", "subroute", nil)},
}
@@ -228,12 +319,12 @@ func cmdPHPServer(fs caddycmd.Flags) (int, error) {
Servers: map[string]*caddyhttp.Server{"php": server},
}
var false bool
var f bool
cfg := &caddy.Config{
Admin: &caddy.AdminConfig{
Disabled: true,
Config: &caddy.ConfigSettings{
Persist: &false,
Persist: &f,
},
},
AppsRaw: caddy.ModuleMap{
@@ -246,7 +337,7 @@ func cmdPHPServer(fs caddycmd.Flags) (int, error) {
cfg.Logging = &caddy.Logging{
Logs: map[string]*caddy.CustomLog{
"default": {
BaseLog: caddy.BaseLog{Level: zap.DebugLevel.CapitalString()},
BaseLog: caddy.BaseLog{Level: slog.LevelDebug.String()},
},
},
}

38
caddy/watcher_test.go Normal file
View File

@@ -0,0 +1,38 @@
//go:build !nowatcher
package caddy_test
import (
"net/http"
"testing"
"github.com/caddyserver/caddy/v2/caddytest"
)
func TestWorkerWithInactiveWatcher(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
skip_install_trust
admin localhost:2999
http_port `+testPort+`
frankenphp {
worker {
file ../testdata/worker-with-counter.php
num 1
watch ./**/*.php
}
}
}
localhost:`+testPort+` {
root ../testdata
rewrite worker-with-counter.php
php
}
`, "caddyfile")
tester.AssertGetResponse("http://localhost:"+testPort, http.StatusOK, "requests:1")
tester.AssertGetResponse("http://localhost:"+testPort, http.StatusOK, "requests:2")
}

185
caddy/workerconfig.go Normal file
View File

@@ -0,0 +1,185 @@
package caddy
import (
"net/http"
"path/filepath"
"strconv"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/dunglas/frankenphp"
"github.com/dunglas/frankenphp/internal/fastabs"
)
// workerConfig represents the "worker" directive in the Caddyfile
// it can appear in the "frankenphp", "php_server" and "php" directives
//
// frankenphp {
// worker {
// name "my-worker"
// file "my-worker.php"
// }
// }
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
Watch []string `json:"watch,omitempty"`
// The path to match against the worker
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 unmarshalWorker(d *caddyfile.Dispenser) (workerConfig, error) {
wc := workerConfig{}
if d.NextArg() {
wc.FileName = d.Val()
}
if d.NextArg() {
if d.Val() == "watch" {
wc.Watch = append(wc.Watch, defaultWatchPattern)
} else {
v, err := strconv.ParseUint(d.Val(), 10, 32)
if err != nil {
return wc, err
}
wc.Num = int(v)
}
}
if d.NextArg() {
return wc, d.Errf(`FrankenPHP: too many "worker" arguments: %s`, d.Val())
}
for d.NextBlock(1) {
switch v := d.Val(); v {
case "name":
if !d.NextArg() {
return wc, d.ArgErr()
}
wc.Name = d.Val()
case "file":
if !d.NextArg() {
return wc, d.ArgErr()
}
wc.FileName = d.Val()
case "num":
if !d.NextArg() {
return wc, d.ArgErr()
}
v, err := strconv.ParseUint(d.Val(), 10, 32)
if err != nil {
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 {
return wc, d.ArgErr()
}
if wc.Env == nil {
wc.Env = make(map[string]string)
}
wc.Env[args[0]] = args[1]
case "watch":
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, 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())
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()
}
v, err := strconv.Atoi(d.Val())
if err != nil {
return wc, d.WrapErr(err)
}
if v < -1 {
return wc, d.Errf("max_consecutive_failures must be >= -1")
}
wc.MaxConsecutiveFailures = v
default:
return wc, wrongSubDirectiveError("worker", "name, file, num, env, watch, match, max_consecutive_failures, max_threads", v)
}
}
if wc.FileName == "" {
return wc, d.Err(`the "file" argument must be specified`)
}
if frankenphp.EmbeddedAppPath != "" && filepath.IsLocal(wc.FileName) {
wc.FileName = filepath.Join(frankenphp.EmbeddedAppPath, wc.FileName)
}
return wc, nil
}
func (wc *workerConfig) inheritEnv(env map[string]string) {
if wc.Env == nil {
wc.Env = make(map[string]string, len(env))
}
for k, v := range env {
// do not overwrite existing environment variables
if _, exists := wc.Env[k]; !exists {
wc.Env[k] = v
}
}
}
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 {
return (caddyhttp.MatchPath)(wc.MatchPath).Match(r)
}
// if there is no pattern, try to match against the actual path (in the public directory)
fullScriptPath, _ := fastabs.FastAbs(documentRoot + "/" + r.URL.Path)
absFileName, _ := fastabs.FastAbs(wc.FileName)
return fullScriptPath == absFileName
}

340
cgi.go
View File

@@ -1,59 +1,76 @@
package frankenphp
// #cgo nocallback frankenphp_register_bulk
// #cgo nocallback frankenphp_register_variables_from_request_info
// #cgo nocallback frankenphp_register_variable_safe
// #cgo nocallback frankenphp_register_single
// #cgo noescape frankenphp_register_bulk
// #cgo noescape frankenphp_register_variables_from_request_info
// #cgo noescape frankenphp_register_variable_safe
// #cgo noescape frankenphp_register_single
// #include <php_variables.h>
// #include "frankenphp.h"
import "C"
import (
"context"
"crypto/tls"
"net"
"net/http"
"path/filepath"
"strings"
"unsafe"
"github.com/dunglas/frankenphp/internal/phpheaders"
)
type serverKey int
// Protocol versions, in Apache mod_ssl format: https://httpd.apache.org/docs/current/mod/mod_ssl.html
// Note that these are slightly different from SupportedProtocols in caddytls/config.go
var tlsProtocolStrings = map[uint16]string{
tls.VersionTLS10: "TLSv1",
tls.VersionTLS11: "TLSv1.1",
tls.VersionTLS12: "TLSv1.2",
tls.VersionTLS13: "TLSv1.3",
}
const (
contentLength serverKey = iota
documentRoot
documentUri
gatewayInterface
httpHost
https
pathInfo
phpSelf
remoteAddr
remoteHost
remotePort
requestScheme
scriptFilename
scriptName
serverName
serverPort
serverProtocol
serverSoftware
sslProtocol
)
func allocServerVariable(cArr *[27]*C.char, env map[string]string, serverKey serverKey, envKey string, val string) {
if val, ok := env[envKey]; ok {
cArr[serverKey] = C.CString(val)
delete(env, envKey)
return
}
cArr[serverKey] = C.CString(val)
// Known $_SERVER keys
var knownServerKeys = []string{
"CONTENT_LENGTH",
"DOCUMENT_ROOT",
"DOCUMENT_URI",
"GATEWAY_INTERFACE",
"HTTP_HOST",
"HTTPS",
"PATH_INFO",
"PHP_SELF",
"REMOTE_ADDR",
"REMOTE_HOST",
"REMOTE_PORT",
"REQUEST_SCHEME",
"SCRIPT_FILENAME",
"SCRIPT_NAME",
"SERVER_NAME",
"SERVER_PORT",
"SERVER_PROTOCOL",
"SERVER_SOFTWARE",
"SSL_PROTOCOL",
"SSL_CIPHER",
"AUTH_TYPE",
"REMOTE_IDENT",
"CONTENT_TYPE",
"PATH_TRANSLATED",
"QUERY_STRING",
"REMOTE_USER",
"REQUEST_METHOD",
"REQUEST_URI",
}
// computeKnownVariables returns a set of CGI environment variables for the request.
//
// 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 computeKnownVariables(request *http.Request) (cArr [27]*C.char) {
fc, fcOK := FromContext(request.Context())
if !fcOK {
panic("not a FrankenPHP request")
}
func addKnownVariablesToServer(fc *frankenPHPContext, trackVarsArray *C.zval) {
request := fc.request
keys := mainThread.knownServerKeys
// Separate remote IP and port; more lenient than net.SplitHostPort
var ip, port string
if idx := strings.LastIndex(request.RemoteAddr, ":"); idx > -1 {
@@ -67,58 +84,29 @@ func computeKnownVariables(request *http.Request) (cArr [27]*C.char) {
ip = strings.Replace(ip, "[", "", 1)
ip = strings.Replace(ip, "]", "", 1)
ra, raOK := fc.env["REMOTE_ADDR"]
if raOK {
cArr[remoteAddr] = C.CString(ra)
delete(fc.env, "REMOTE_ADDR")
} else {
cArr[remoteAddr] = C.CString(ip)
}
var https, sslProtocol, sslCipher, rs string
if rh, ok := fc.env["REMOTE_HOST"]; ok {
cArr[remoteHost] = C.CString(rh) // For speed, remote host lookups disabled
delete(fc.env, "REMOTE_HOST")
} else {
if raOK {
cArr[remoteHost] = C.CString(ip)
} else {
cArr[remoteHost] = cArr[remoteAddr]
}
}
allocServerVariable(&cArr, fc.env, remotePort, "REMOTE_PORT", port)
allocServerVariable(&cArr, fc.env, documentRoot, "DOCUMENT_ROOT", fc.documentRoot)
allocServerVariable(&cArr, fc.env, pathInfo, "PATH_INFO", fc.pathInfo)
allocServerVariable(&cArr, fc.env, phpSelf, "PHP_SELF", request.URL.Path)
allocServerVariable(&cArr, fc.env, documentUri, "DOCUMENT_URI", fc.docURI)
allocServerVariable(&cArr, fc.env, scriptFilename, "SCRIPT_FILENAME", fc.scriptFilename)
allocServerVariable(&cArr, fc.env, scriptName, "SCRIPT_NAME", fc.scriptName)
var rs string
if request.TLS == nil {
rs = "http"
https = ""
sslProtocol = ""
sslCipher = ""
} else {
rs = "https"
if h, ok := fc.env["HTTPS"]; ok {
cArr[https] = C.CString(h)
delete(fc.env, "HTTPS")
} else {
cArr[https] = C.CString("on")
}
https = "on"
// and pass the protocol details in a manner compatible with apache's mod_ssl
// (which is why these have a SSL_ prefix and not TLS_).
if p, ok := fc.env["SSL_PROTOCOL"]; ok {
cArr[sslProtocol] = C.CString(p)
delete(fc.env, "SSL_PROTOCOL")
// (which is why these have an SSL_ prefix and not TLS_).
if v, ok := tlsProtocolStrings[request.TLS.Version]; ok {
sslProtocol = v
} else {
if v, ok := tlsProtocolStrings[request.TLS.Version]; ok {
cArr[sslProtocol] = C.CString(v)
}
sslProtocol = ""
}
if request.TLS.CipherSuite != 0 {
sslCipher = tls.CipherSuiteName(request.TLS.CipherSuite)
}
}
allocServerVariable(&cArr, fc.env, requestScheme, "REQUEST_SCHEME", rs)
reqHost, reqPort, _ := net.SplitHostPort(request.Host)
@@ -140,38 +128,144 @@ func computeKnownVariables(request *http.Request) (cArr [27]*C.char) {
}
}
allocServerVariable(&cArr, fc.env, serverName, "SERVER_NAME", reqHost)
if reqPort != "" {
allocServerVariable(&cArr, fc.env, serverPort, "SERVER_PORT", reqPort)
serverPort := reqPort
contentLength := request.Header.Get("Content-Length")
var requestURI string
if fc.originalRequest != nil {
requestURI = fc.originalRequest.URL.RequestURI()
} else {
requestURI = request.URL.RequestURI()
}
// Variables defined in CGI 1.1 spec
// Some variables are unused but cleared explicitly to prevent
// the parent environment from interfering.
C.frankenphp_register_bulk(
trackVarsArray,
packCgiVariable(keys["REMOTE_ADDR"], ip),
packCgiVariable(keys["REMOTE_HOST"], ip),
packCgiVariable(keys["REMOTE_PORT"], port),
packCgiVariable(keys["DOCUMENT_ROOT"], fc.documentRoot),
packCgiVariable(keys["PATH_INFO"], fc.pathInfo),
packCgiVariable(keys["PHP_SELF"], request.URL.Path),
packCgiVariable(keys["DOCUMENT_URI"], fc.docURI),
packCgiVariable(keys["SCRIPT_FILENAME"], fc.scriptFilename),
packCgiVariable(keys["SCRIPT_NAME"], fc.scriptName),
packCgiVariable(keys["HTTPS"], https),
packCgiVariable(keys["SSL_PROTOCOL"], sslProtocol),
packCgiVariable(keys["REQUEST_SCHEME"], rs),
packCgiVariable(keys["SERVER_NAME"], reqHost),
packCgiVariable(keys["SERVER_PORT"], serverPort),
// Variables defined in CGI 1.1 spec
// Some variables are unused but cleared explicitly to prevent
// the parent environment from interfering.
// These values can not be overridden
packCgiVariable(keys["CONTENT_LENGTH"], contentLength),
packCgiVariable(keys["GATEWAY_INTERFACE"], "CGI/1.1"),
packCgiVariable(keys["SERVER_PROTOCOL"], request.Proto),
packCgiVariable(keys["SERVER_SOFTWARE"], "FrankenPHP"),
packCgiVariable(keys["HTTP_HOST"], request.Host),
// These values are always empty but must be defined:
packCgiVariable(keys["AUTH_TYPE"], ""),
packCgiVariable(keys["REMOTE_IDENT"], ""),
// Request uri of the original request
packCgiVariable(keys["REQUEST_URI"], requestURI),
packCgiVariable(keys["SSL_CIPHER"], sslCipher),
)
// These values can not be override
cArr[contentLength] = C.CString(request.Header.Get("Content-Length"))
// These values are already present in the SG(request_info), so we'll register them from there
C.frankenphp_register_variables_from_request_info(
trackVarsArray,
keys["CONTENT_TYPE"],
keys["PATH_TRANSLATED"],
keys["QUERY_STRING"],
keys["REMOTE_USER"],
keys["REQUEST_METHOD"],
)
}
allocServerVariable(&cArr, fc.env, gatewayInterface, "GATEWAY_INTERFACE", "CGI/1.1")
allocServerVariable(&cArr, fc.env, serverProtocol, "SERVER_PROTOCOL", request.Proto)
allocServerVariable(&cArr, fc.env, serverSoftware, "SERVER_SOFTWARE", "FrankenPHP")
allocServerVariable(&cArr, fc.env, httpHost, "HTTP_HOST", request.Host) // added here, since not always part of headers
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))}
}
return
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)
continue
}
// 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(ctx, field)
v := strings.Join(val, ", ")
C.frankenphp_register_variable_safe(toUnsafeChar(k), toUnsafeChar(v), C.size_t(len(v)), trackVarsArray)
}
}
func addPreparedEnvToServer(fc *frankenPHPContext, trackVarsArray *C.zval) {
for k, v := range fc.env {
C.frankenphp_register_variable_safe(toUnsafeChar(k), toUnsafeChar(v), C.size_t(len(v)), trackVarsArray)
}
fc.env = nil
}
//export go_register_variables
func go_register_variables(threadIndex C.uintptr_t, trackVarsArray *C.zval) {
thread := phpThreads[threadIndex]
fc := thread.frankenPHPContext()
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)
}
// splitCgiPath splits the request path into SCRIPT_NAME, SCRIPT_FILENAME, PATH_INFO, DOCUMENT_URI
func splitCgiPath(fc *frankenPHPContext) {
path := fc.request.URL.Path
splitPath := fc.splitPath
if splitPath == nil {
splitPath = []string{".php"}
}
if splitPos := splitPos(path, splitPath); splitPos > -1 {
fc.docURI = path[:splitPos]
fc.pathInfo = path[splitPos:]
// Strip PATH_INFO from SCRIPT_NAME
fc.scriptName = strings.TrimSuffix(path, fc.pathInfo)
// Ensure the SCRIPT_NAME has a leading slash for compliance with RFC3875
// Info: https://tools.ietf.org/html/rfc3875#section-4.1.13
if fc.scriptName != "" && !strings.HasPrefix(fc.scriptName, "/") {
fc.scriptName = "/" + fc.scriptName
}
}
// TODO: is it possible to delay this and avoid saving everything in the context?
// SCRIPT_FILENAME is the absolute path of SCRIPT_NAME
fc.scriptFilename = sanitizedPathJoin(fc.documentRoot, fc.scriptName)
fc.worker = getWorkerByPath(fc.scriptFilename)
}
// splitPos returns the index where path should
// be split based on SplitPath.
// example: if splitPath is [".php"]
// "/path/to/script.php/some/path": ("/path/to/script.php", "/some/path")
//
// Adapted from https://github.com/caddyserver/caddy/blob/master/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go
// Copyright 2015 Matthew Holt and The Caddy Authors
func splitPos(fc *FrankenPHPContext, path string) int {
if len(fc.splitPath) == 0 {
func splitPos(path string, splitPath []string) int {
if len(splitPath) == 0 {
return 0
}
lowerPath := strings.ToLower(path)
for _, split := range fc.splitPath {
for _, split := range splitPath {
if idx := strings.Index(lowerPath, strings.ToLower(split)); idx > -1 {
return idx + len(split)
}
@@ -179,16 +273,45 @@ func splitPos(fc *FrankenPHPContext, path string) int {
return -1
}
// Map of supported protocols to Apache ssl_mod format
// Note that these are slightly different from SupportedProtocols in caddytls/config.go
var tlsProtocolStrings = map[uint16]string{
tls.VersionTLS10: "TLSv1",
tls.VersionTLS11: "TLSv1.1",
tls.VersionTLS12: "TLSv1.2",
tls.VersionTLS13: "TLSv1.3",
}
// go_update_request_info updates the sapi_request_info struct
// 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) {
thread := phpThreads[threadIndex]
fc := thread.frankenPHPContext()
request := fc.request
var headerNameReplacer = strings.NewReplacer(" ", "_", "-", "_")
if request == nil {
return
}
authUser, authPassword, ok := request.BasicAuth()
if ok {
if authPassword != "" {
info.auth_password = thread.pinCString(authPassword)
}
if authUser != "" {
info.auth_user = thread.pinCString(authUser)
}
}
info.request_method = thread.pinCString(request.Method)
info.query_string = thread.pinCString(request.URL.RawQuery)
info.content_length = C.zend_long(request.ContentLength)
if contentType := request.Header.Get("Content-Type"); contentType != "" {
info.content_type = thread.pinCString(contentType)
}
if fc.pathInfo != "" {
info.path_translated = thread.pinCString(sanitizedPathJoin(fc.documentRoot, fc.pathInfo)) // See: http://www.oreilly.com/openbook/cgi/ch02_04.html
}
info.request_uri = thread.pinCString(request.URL.RequestURI())
info.proto_num = C.int(request.ProtoMajor*1000 + request.ProtoMinor)
}
// SanitizedPathJoin performs filepath.Join(root, reqPath) that
// is safe against directory traversal attacks. It uses logic
@@ -208,7 +331,7 @@ func sanitizedPathJoin(root, reqPath string) string {
path := filepath.Join(root, filepath.Clean("/"+reqPath))
// filepath.Join also cleans the path, and cleaning strips
// the trailing slash, so we need to re-add it afterwards.
// the trailing slash, so we need to re-add it afterward.
// if the length is 1, then it's a path to the root,
// and that should return ".", so we don't append the separator.
if strings.HasSuffix(reqPath, "/") && len(reqPath) > 1 {
@@ -219,3 +342,8 @@ func sanitizedPathJoin(root, reqPath string) string {
}
const separator = string(filepath.Separator)
func toUnsafeChar(s string) *C.char {
sData := unsafe.StringData(s)
return (*C.char)(unsafe.Pointer(sData))
}

9
cgo.go Normal file
View File

@@ -0,0 +1,9 @@
package frankenphp
// #cgo darwin pkg-config: libxml-2.0
// #cgo CFLAGS: -Wall -Werror
// #cgo linux CFLAGS: -D_GNU_SOURCE
// #cgo LDFLAGS: -lphp -lm -lutil
// #cgo linux LDFLAGS: -ldl -lresolv
// #cgo darwin LDFLAGS: -Wl,-rpath,/usr/local/lib -liconv -ldl
import "C"

186
context.go Normal file
View File

@@ -0,0 +1,186 @@
package frankenphp
import (
"context"
"errors"
"fmt"
"log/slog"
"net/http"
"os"
"strconv"
"strings"
"time"
)
// frankenPHPContext provides contextual information about the Request to handle.
type frankenPHPContext struct {
mercureContext
documentRoot string
splitPath []string
env PreparedEnv
logger *slog.Logger
request *http.Request
originalRequest *http.Request
worker *worker
docURI string
pathInfo string
scriptName string
scriptFilename string
// Whether the request is already closed by us
isDone bool
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
}
func newFrankenPHPContext() *frankenPHPContext {
return &frankenPHPContext{
done: make(chan any),
startedAt: time.Now(),
}
}
// 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
}
}
if fc.logger == nil {
fc.logger = globalLogger
}
if fc.documentRoot == "" {
if EmbeddedAppPath != "" {
fc.documentRoot = EmbeddedAppPath
} else {
var err error
if fc.documentRoot, err = os.Getwd(); err != nil {
return nil, err
}
}
}
// If a worker is already assigned explicitly, use its filename and skip parsing path variables
if fc.worker != nil {
fc.scriptFilename = fc.worker.fileName
} else {
// If no worker was assigned, split the path into the "traditional" CGI path variables.
// This needs to already happen here in case a worker script still matches the path.
splitCgiPath(fc)
}
c := context.WithValue(r.Context(), contextKey, fc)
return r.WithContext(c), nil
}
// newDummyContext creates a fake context from a request path
func newDummyContext(requestPath string, opts ...RequestOption) (*frankenPHPContext, error) {
r, err := http.NewRequestWithContext(globalCtx, http.MethodGet, requestPath, nil)
if err != nil {
return nil, err
}
fr, err := NewRequestWithContext(r, opts...)
if err != nil {
return nil, err
}
fc, _ := fromContext(fr.Context())
return fc, nil
}
// closeContext sends the response to the client
func (fc *frankenPHPContext) closeContext() {
if fc.isDone {
return
}
close(fc.done)
fc.isDone = true
}
// validate checks if the request should be outright rejected
func (fc *frankenPHPContext) validate() error {
if strings.Contains(fc.request.URL.Path, "\x00") {
fc.reject(ErrInvalidRequestPath)
return ErrInvalidRequestPath
}
contentLengthStr := fc.request.Header.Get("Content-Length")
if contentLengthStr != "" {
if contentLength, err := strconv.Atoi(contentLengthStr); err != nil || contentLength < 0 {
e := fmt.Errorf("%w: %q", ErrInvalidContentLengthHeader, contentLengthStr)
fc.reject(e)
return e
}
}
return nil
}
func (fc *frankenPHPContext) clientHasClosed() bool {
if fc.request == nil {
return false
}
select {
case <-fc.request.Context().Done():
return true
default:
return false
}
}
// 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(re.status)
_, _ = rw.Write([]byte(err.Error()))
if f, ok := rw.(http.Flusher); ok {
f.Flush()
}
}
fc.closeContext()
}

50
debugstate.go Normal file
View File

@@ -0,0 +1,50 @@
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
Name string
State string
IsWaiting bool
IsBusy bool
WaitingSinceMilliseconds int64
}
// EXPERIMENTAL: FrankenPHPDebugState prints the state of all PHP threads - debugging purposes only
type FrankenPHPDebugState struct {
ThreadDebugStates []ThreadDebugState
ReservedThreadCount int
}
// EXPERIMENTAL: DebugState prints the state of all PHP threads - debugging purposes only
func DebugState() FrankenPHPDebugState {
fullState := FrankenPHPDebugState{
ThreadDebugStates: make([]ThreadDebugState, 0, len(phpThreads)),
ReservedThreadCount: 0,
}
for _, thread := range phpThreads {
if thread.state.Is(state.Reserved) {
fullState.ReservedThreadCount++
continue
}
fullState.ThreadDebugStates = append(fullState.ThreadDebugStates, threadDebugState(thread))
}
return fullState
}
// threadDebugState creates a small jsonable status message for debugging purposes
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(),
}
}

View File

@@ -1,65 +1,85 @@
# syntax=docker/dockerfile:1
FROM golang:1.21-alpine
#checkov:skip=CKV_DOCKER_2
#checkov:skip=CKV_DOCKER_3
FROM golang:1.25-alpine
ENV GOTOOLCHAIN=local
ENV CFLAGS="-ggdb3"
ENV PHPIZE_DEPS \
autoconf \
dpkg-dev \
file \
g++ \
gcc \
libc-dev \
make \
pkgconfig \
re2c
ENV PHPIZE_DEPS="\
autoconf \
dpkg-dev \
file \
g++ \
gcc \
libc-dev \
make \
pkgconfig \
re2c"
SHELL ["/bin/ash", "-eo", "pipefail", "-c"]
RUN apk add --no-cache \
$PHPIZE_DEPS \
argon2-dev \
curl-dev \
oniguruma-dev \
readline-dev \
libsodium-dev \
sqlite-dev \
openssl-dev \
libxml2-dev \
zlib-dev \
bison \
nss-tools \
# Dev tools \
git \
clang \
llvm \
gdb \
valgrind \
neovim \
zsh \
libtool && \
echo 'set auto-load safe-path /' > /root/.gdbinit
$PHPIZE_DEPS \
argon2-dev \
brotli-dev \
curl-dev \
oniguruma-dev \
readline-dev \
libsodium-dev \
sqlite-dev \
openssl-dev \
libxml2-dev \
zlib-dev \
bison \
nss-tools \
# file watcher \
libstdc++ \
linux-headers \
# Dev tools \
git \
clang \
cmake \
llvm \
gdb \
valgrind \
neovim \
zsh \
libtool && \
echo 'set auto-load safe-path /' > /root/.gdbinit
WORKDIR /usr/local/src/php
RUN git clone --branch=PHP-8.3 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 && \
./configure \
--enable-embed \
--enable-zts \
--disable-zend-signals \
--enable-zend-max-execution-timers \
--enable-debug && \
make -j"$(nproc)" && \
make install && \
ldconfig /etc/ld.so.conf.d && \
cp php.ini-development /usr/local/lib/php.ini && \
echo "zend_extension=opcache.so" >> /usr/local/lib/php.ini && \
echo "opcache.enable=1" >> /usr/local/lib/php.ini && \
php --version
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 \
--enable-zts \
--disable-zend-signals \
--enable-zend-max-execution-timers \
--with-config-file-path=/etc/frankenphp/php.ini \
--with-config-file-scan-dir=/etc/frankenphp/php.d \
--enable-debug && \
make -j"$(nproc)" && \
make install && \
ldconfig /etc/ld.so.conf.d && \
mkdir -p /etc/frankenphp/php.d && \
cp php.ini-development /etc/frankenphp/php.ini && \
echo "zend_extension=opcache.so" >> /etc/frankenphp/php.ini && \
echo "opcache.enable=1" >> /etc/frankenphp/php.ini && \
php --version
# 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 --build build/ && \
cmake --install build
WORKDIR /go/src/app
COPY . .
WORKDIR /go/src/app/caddy/frankenphp
RUN go build
RUN ../../go.sh build -buildvcs=false
WORKDIR /go/src/app
CMD [ "zsh" ]

View File

@@ -1,70 +1,89 @@
# syntax=docker/dockerfile:1
FROM golang:1.21
#checkov:skip=CKV_DOCKER_2
#checkov:skip=CKV_DOCKER_3
FROM golang:1.25
ENV GOTOOLCHAIN=local
ENV CFLAGS="-ggdb3"
ENV PHPIZE_DEPS \
autoconf \
dpkg-dev \
file \
g++ \
gcc \
libc-dev \
make \
pkg-config \
re2c
ENV PHPIZE_DEPS="\
autoconf \
dpkg-dev \
file \
g++ \
gcc \
libc-dev \
make \
pkg-config \
re2c"
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
# hadolint ignore=DL3009
RUN apt-get update && \
apt-get -y --no-install-recommends install \
$PHPIZE_DEPS \
libargon2-dev \
libcurl4-openssl-dev \
libonig-dev \
libreadline-dev \
libsodium-dev \
libsqlite3-dev \
libssl-dev \
libxml2-dev \
zlib1g-dev \
bison \
libnss3-tools \
# Dev tools \
git \
clang \
llvm \
gdb \
valgrind \
neovim \
zsh \
libtool-bin && \
echo 'set auto-load safe-path /' > /root/.gdbinit && \
echo '* soft core unlimited' >> /etc/security/limits.conf \
&& \
apt-get clean
apt-get -y --no-install-recommends install \
$PHPIZE_DEPS \
libargon2-dev \
libbrotli-dev \
libcurl4-openssl-dev \
libonig-dev \
libreadline-dev \
libsodium-dev \
libsqlite3-dev \
libssl-dev \
libxml2-dev \
zlib1g-dev \
bison \
libnss3-tools \
# Dev tools \
git \
clang \
cmake \
llvm \
gdb \
valgrind \
neovim \
zsh \
libtool-bin && \
echo 'set auto-load safe-path /' > /root/.gdbinit && \
echo '* soft core unlimited' >> /etc/security/limits.conf \
&& \
apt-get clean
WORKDIR /usr/local/src/php
RUN git clone --branch=PHP-8.3 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 && \
./configure \
--enable-embed \
--enable-zts \
--disable-zend-signals \
--enable-zend-max-execution-timers \
--enable-debug && \
make -j"$(nproc)" && \
make install && \
ldconfig && \
cp php.ini-development /usr/local/lib/php.ini && \
echo "zend_extension=opcache.so" >> /usr/local/lib/php.ini && \
echo "opcache.enable=1" >> /usr/local/lib/php.ini && \
php --version
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 \
--enable-embed \
--enable-zts \
--disable-zend-signals \
--enable-zend-max-execution-timers \
--with-config-file-path=/etc/frankenphp/php.ini \
--with-config-file-scan-dir=/etc/frankenphp/php.d \
--enable-debug && \
make -j"$(nproc)" && \
make install && \
ldconfig && \
mkdir -p /etc/frankenphp/php.d && \
cp php.ini-development /etc/frankenphp/php.ini && \
echo "zend_extension=opcache.so" >> /etc/frankenphp/php.ini && \
echo "opcache.enable=1" >> /etc/frankenphp/php.ini && \
php --version
# 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 --build build/ && \
cmake --install build && \
cp build/libwatcher-c.so /usr/local/lib/libwatcher-c.so && \
ldconfig
WORKDIR /go/src/app
COPY . .
COPY --link . ./
WORKDIR /go/src/app/caddy/frankenphp
RUN go build
RUN ../../go.sh build -buildvcs=false
WORKDIR /go/src/app
CMD [ "zsh" ]

View File

@@ -7,38 +7,47 @@ variable "VERSION" {
}
variable "PHP_VERSION" {
default = "8.2,8.3"
default = "8.2,8.3,8.4,8.5"
}
variable "GO_VERSION" {
default = "1.21"
default = "1.25"
}
variable "SPC_OPT_BUILD_ARGS" {
default = ""
}
variable "SHA" {}
variable "LATEST" {
default = false
default = true
}
variable "CACHE" {
default = ""
}
variable "CI" {
# CI flag coming from the environment or --set; empty by default
default = ""
}
variable DEFAULT_PHP_VERSION {
default = "8.3"
default = "8.5"
}
function "tag" {
params = [version, os, php-version, tgt]
result = [
version != "" ? format("%s:%s%s-php%s-%s", IMAGE_NAME, version, tgt == "builder" ? "-builder" : "", php-version, os) : "",
php-version == DEFAULT_PHP_VERSION && os == "bookworm" && version != "" ? format("%s:%s%s", IMAGE_NAME, version, tgt == "builder" ? "-builder" : "") : "",
php-version == DEFAULT_PHP_VERSION && version != "" ? format("%s:%s%s-%s", IMAGE_NAME, version, tgt == "builder" ? "-builder" : "", os) : "",
php-version == DEFAULT_PHP_VERSION && version == "latest" ? format("%s:%s%s", IMAGE_NAME, os, tgt == "builder" ? "-builder" : "") : "",
os == "bookworm" && version != "" ? format("%s:%s%s-php%s", IMAGE_NAME, version, tgt == "builder" ? "-builder" : "", php-version) : "",
version == "" ? "" : "${IMAGE_NAME}:${trimprefix("${version}${tgt == "builder" ? "-builder" : ""}-php${php-version}-${os}", "latest-")}",
php-version == DEFAULT_PHP_VERSION && os == "trixie" && version != "" ? "${IMAGE_NAME}:${trimprefix("${version}${tgt == "builder" ? "-builder" : ""}", "latest-")}" : "",
php-version == DEFAULT_PHP_VERSION && version != "" ? "${IMAGE_NAME}:${trimprefix("${version}${tgt == "builder" ? "-builder" : ""}-${os}", "latest-")}" : "",
os == "trixie" && version != "" ? "${IMAGE_NAME}:${trimprefix("${version}${tgt == "builder" ? "-builder" : ""}-php${php-version}", "latest-")}" : "",
]
}
# cleanTag ensures that the tag is a valid Docker tag
# cleanTag ensures that the tag is a valid Docker tag
# see https://github.com/distribution/distribution/blob/v2.8.2/reference/regexp.go#L37
function "clean_tag" {
@@ -60,7 +69,7 @@ function "_semver" {
function "__semver" {
params = [v]
result = v == {} ? [clean_tag(VERSION)] : v.prerelease == null ? ["latest", v.major, "${v.major}.${v.minor}", "${v.major}.${v.minor}.${v.patch}"] : ["${v.major}.${v.minor}.${v.patch}-${v.prerelease}"]
result = v == {} ? [clean_tag(VERSION)] : v.prerelease == null ? [v.major, "${v.major}.${v.minor}", "${v.major}.${v.minor}.${v.patch}"] : ["${v.major}.${v.minor}.${v.patch}-${v.prerelease}"]
}
function "php_version" {
@@ -76,7 +85,7 @@ function "_php_version" {
target "default" {
name = "${tgt}-php-${replace(php-version, ".", "-")}-${os}"
matrix = {
os = ["bookworm", "alpine"]
os = ["trixie", "bookworm", "alpine"]
php-version = split(",", PHP_VERSION)
tgt = ["builder", "runner"]
}
@@ -87,18 +96,24 @@ target "default" {
dockerfile = os == "alpine" ? "alpine.Dockerfile" : "Dockerfile"
context = "./"
target = tgt
platforms = [
# arm/v6 is only available for Alpine: https://github.com/docker-library/golang/issues/502
platforms = os == "alpine" ? [
"linux/amd64",
"linux/386",
"linux/arm/v6",
"linux/arm/v7",
"linux/arm64",
] : [
"linux/amd64",
"linux/386",
"linux/arm/v7",
"linux/arm64"
]
tags = distinct(flatten(
[for pv in php_version(php-version) : flatten([
LATEST ? tag("latest", os, pv, tgt) : [],
tag(SHA == "" ? "" : "sha-${substr(SHA, 0, 7)}", os, pv, tgt),
[for v in semver(VERSION) : tag(v, os, pv, tgt)]
tag(SHA == "" || VERSION != "dev" ? "" : "sha-${substr(SHA, 0, 7)}", os, pv, tgt),
VERSION == "dev" ? [] : [for v in semver(VERSION) : tag(v, os, pv, tgt)]
])
]))
labels = {
@@ -109,19 +124,24 @@ target "default" {
args = {
FRANKENPHP_VERSION = VERSION
}
secret = ["id=github-token,env=GITHUB_TOKEN"]
}
target "static-builder" {
target "static-builder-musl" {
contexts = {
golang-base = "docker-image://golang:${GO_VERSION}-alpine"
}
dockerfile = "static-builder.Dockerfile"
dockerfile = "static-builder-musl.Dockerfile"
context = "./"
platforms = [
"linux/amd64",
"linux/arm64",
]
tags = distinct(flatten([
LATEST ? "${IMAGE_NAME}:static-builder" : "",
SHA == "" ? "" : "${IMAGE_NAME}:static-builder-sha-${substr(SHA, 0, 7)}",
[for v in semver(VERSION) : "${IMAGE_NAME}:static-builder-${v}"]
]))
LATEST ? "${IMAGE_NAME}:static-builder-musl" : "",
SHA == "" || VERSION != "dev" ? "" : "${IMAGE_NAME}:static-builder-musl-sha-${substr(SHA, 0, 7)}",
VERSION == "dev" ? [] : [for v in semver(VERSION) : "${IMAGE_NAME}:static-builder-musl-${v}"]
]))
labels = {
"org.opencontainers.image.created" = "${timestamp()}"
"org.opencontainers.image.version" = VERSION
@@ -129,6 +149,34 @@ target "static-builder" {
}
args = {
FRANKENPHP_VERSION = VERSION
CI = CI
SPC_OPT_BUILD_ARGS = SPC_OPT_BUILD_ARGS
}
secret = ["id=github-token,env=GITHUB_TOKEN"]
}
target "static-builder-gnu" {
dockerfile = "static-builder-gnu.Dockerfile"
context = "./"
platforms = [
"linux/amd64",
"linux/arm64"
]
tags = distinct(flatten([
LATEST ? "${IMAGE_NAME}:static-builder-gnu" : "",
SHA == "" || VERSION != "dev" ? "" : "${IMAGE_NAME}:static-builder-gnu-sha-${substr(SHA, 0, 7)}",
VERSION == "dev" ? [] : [for v in semver(VERSION) : "${IMAGE_NAME}:static-builder-gnu-${v}"]
]))
labels = {
"org.opencontainers.image.created" = "${timestamp()}"
"org.opencontainers.image.version" = VERSION
"org.opencontainers.image.revision" = SHA
}
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"]
}

11
docs/classic.md Normal file
View File

@@ -0,0 +1,11 @@
# Using Classic Mode
Without any additional configuration, FrankenPHP operates in classic mode. In this mode, FrankenPHP functions like a traditional PHP server, directly serving PHP files. This makes it a seamless drop-in replacement for PHP-FPM or Apache with mod_php.
Similar to Caddy, FrankenPHP accepts an unlimited number of connections and uses a [fixed number of threads](config.md#caddyfile-config) to serve them. The number of accepted and queued connections is limited only by the available system resources.
The PHP thread pool operates with a fixed number of threads initialized at startup, comparable to the static mode of PHP-FPM. It's also possible to let threads [scale automatically at runtime](performance.md#max_threads), similar to the dynamic mode of PHP-FPM.
Queued connections will wait indefinitely until a PHP thread is available to serve them. To avoid this, you can use the max_wait_time [configuration](config.md#caddyfile-config) in FrankenPHP's global configuration to limit the duration a request can wait for a free PHP thread before being rejected.
Additionally, you can set a reasonable [write timeout in Caddy](https://caddyserver.com/docs/caddyfile/options#timeouts).
Each Caddy instance will only spin up one FrankenPHP thread pool, which will be shared across all `php_server` blocks.

218
docs/cn/CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,218 @@
# 贡献
## 编译 PHP
### 使用 Docker (Linux)
构建开发环境 Docker 镜像:
```console
docker build -t frankenphp-dev -f dev.Dockerfile .
docker run --cap-add=SYS_PTRACE --security-opt seccomp=unconfined -p 8080:8080 -p 443:443 -p 443:443/udp -v $PWD:/go/src/app -it frankenphp-dev
```
该镜像包含常用的开发工具Go、GDB、Valgrind、Neovim等并使用以下 php 设置位置
- php.ini: `/etc/frankenphp/php.ini` 默认提供了一个带有开发预设的 php.ini 文件。
- 附加配置文件: `/etc/frankenphp/php.d/*.ini`
- php 扩展: `/usr/lib/frankenphp/modules/`
如果你的 Docker 版本低于 23.0,则会因为 dockerignore [pattern issue](https://github.com/moby/moby/pull/42676) 而导致构建失败。将目录添加到 `.dockerignore`
```patch
!testdata/*.php
!testdata/*.txt
+!caddy
+!internal
```
### 不使用 Docker (Linux 和 macOS)
[按照说明从源代码编译](https://frankenphp.dev/docs/compile/) 并传递 `--debug` 配置标志。
## 运行测试套件
```console
go test -tags watcher -race -v ./...
```
## Caddy 模块
使用 FrankenPHP Caddy 模块构建 Caddy
```console
cd caddy/frankenphp/
go build -tags watcher,brotli,nobadger,nomysql,nopgx
cd ../../
```
使用 FrankenPHP Caddy 模块运行 Caddy
```console
cd testdata/
../caddy/frankenphp/frankenphp run
```
服务器正在监听 `127.0.0.1:80`
> [!NOTE]
> 如果您正在使用 Docker您必须绑定容器的 80 端口或者在容器内部执行命令。
```console
curl -vk http://127.0.0.1/phpinfo.php
```
## 最小测试服务器
构建最小测试服务器:
```console
cd internal/testserver/
go build
cd ../../
```
运行测试服务器:
```console
cd testdata/
../internal/testserver/testserver
```
服务器正在监听 `127.0.0.1:8080`
```console
curl -v http://127.0.0.1:8080/phpinfo.php
```
## 本地构建 Docker 镜像
打印 bake 计划:
```console
docker buildx bake -f docker-bake.hcl --print
```
本地构建 amd64 的 FrankenPHP 镜像:
```console
docker buildx bake -f docker-bake.hcl --pull --load --set "*.platform=linux/amd64"
```
本地构建 arm64 的 FrankenPHP 镜像:
```console
docker buildx bake -f docker-bake.hcl --pull --load --set "*.platform=linux/arm64"
```
从头开始为 arm64 和 amd64 构建 FrankenPHP 镜像并推送到 Docker Hub
```console
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
```
2. 将当前版本的 `frankenphp` 替换为 debug FrankenPHP 可执行文件
3. 照常启动 FrankenPHP或者你可以直接使用 GDB 启动 FrankenPHP `gdb --args frankenphp run`
4. 使用 GDB 附加到进程:
```console
gdb -p `pidof frankenphp`
```
5. 如有必要,请在 GDB shell 中输入 `continue`
6. 使 FrankenPHP 崩溃
7. 在 GDB shell 中输入 `bt`
8. 复制输出
## 在 GitHub Actions 中调试分段错误
1. 打开 `.github/workflows/tests.yml`
2. 启用 PHP 调试符号
```patch
- uses: shivammathur/setup-php@v2
# ...
env:
phpts: ts
+ debug: true
```
3. 启用 `tmate` 以连接到容器
```patch
- name: Set CGO flags
run: echo "CGO_CFLAGS=$(php-config --includes)" >> "$GITHUB_ENV"
+ - run: |
+ sudo apt install gdb
+ mkdir -p /home/runner/.config/gdb/
+ printf "set auto-load safe-path /\nhandle SIG34 nostop noprint pass" > /home/runner/.config/gdb/gdbinit
+ - uses: mxschmitt/action-tmate@v3
```
4. 连接到容器
5. 打开 `frankenphp.go`
6. 启用 `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$
```
9. 当错误修复后,恢复所有这些更改
## 其他开发资源
- [PHP 嵌入 uWSGI](https://github.com/unbit/uwsgi/blob/master/plugins/php/php_plugin.c)
- [PHP 嵌入 NGINX Unit](https://github.com/nginx/unit/blob/master/src/nxt_php_sapi.c)
- [PHP 嵌入 Go (go-php)](https://github.com/deuill/go-php)
- [PHP 嵌入 Go (GoEmPHP)](https://github.com/mikespook/goemphp)
- [PHP 嵌入 C++](https://gist.github.com/paresy/3cbd4c6a469511ac7479aa0e7c42fea7)
- [扩展和嵌入 PHP 作者Sara Golemon](https://books.google.fr/books?id=zMbGvK17_tYC&pg=PA254&lpg=PA254#v=onepage&q&f=false)
- [TSRMLS_CC到底是什么](http://blog.golemon.com/2006/06/what-heck-is-tsrmlscc-anyway.html)
- [SDL 绑定](https://pkg.go.dev/github.com/veandco/go-sdl2@v0.4.21/sdl#Main)
## Docker 相关资源
- [Bake 文件定义](https://docs.docker.com/build/customize/bake/file-definition/)
- [`docker buildx build`](https://docs.docker.com/engine/reference/commandline/buildx_build/)
## 有用的命令
```console
apk add strace util-linux gdb
strace -e 'trace=!futex,epoll_ctl,epoll_pwait,tgkill,rt_sigreturn' -p 1
```
## 翻译文档
要将文档和网站翻译成新语言,请按照下列步骤操作:
1. 在此存储库的 `docs/` 目录中创建一个以语言的 2 个字符的 ISO 代码命名的新目录
2. 将 `docs/` 目录根目录中的所有 `.md` 文件复制到新目录中(始终使用英文版本作为翻译源,因为它始终是最新的)
3. 将 `README.md` 和 `CONTRIBUTING.md` 文件从根目录复制到新目录
4. 翻译文件的内容,但不要更改文件名,也不要翻译以 `> [!` 开头的字符串(这是 GitHub 的特殊标记)
5. 创建翻译的拉取请求
6. 在 [站点存储库](https://github.com/dunglas/frankenphp-website/tree/main) 中,复制并翻译 `content/``data/``i18n/` 目录中的翻译文件
7. 转换创建的 YAML 文件中的值
8. 在站点存储库上打开拉取请求

157
docs/cn/README.md Normal file
View File

@@ -0,0 +1,157 @@
# FrankenPHP: 适用于 PHP 的现代应用服务器
<h1 align="center"><a href="https://frankenphp.dev"><img src="../../frankenphp.png" alt="FrankenPHP" width="600"></a></h1>
FrankenPHP 是建立在 [Caddy](https://caddyserver.com/) Web 服务器之上的现代 PHP 应用程序服务器。
FrankenPHP 凭借其令人惊叹的功能为你的 PHP 应用程序提供了超能力:[早期提示](early-hints.md)、[worker 模式](worker.md)、[实时功能](mercure.md)、自动 HTTPS、HTTP/2 和 HTTP/3 支持......
FrankenPHP 可与任何 PHP 应用程序一起使用,并且由于提供了与 worker 模式的集成,使你的 Symfony 和 Laravel 项目比以往任何时候都更快。
FrankenPHP 也可以用作独立的 Go 库,将 PHP 嵌入到任何使用 `net/http` 的应用程序中。
[**了解更多** _frankenphp.dev_](https://frankenphp.dev/cn/) 以及查看此演示文稿:
<a href="https://dunglas.dev/2022/10/frankenphp-the-modern-php-app-server-written-in-go/"><img src="https://dunglas.dev/wp-content/uploads/2022/10/frankenphp.png" alt="Slides" width="600"></a>
## 开始
在 Windows 上,请使用 [WSL](https://learn.microsoft.com/windows/wsl/) 运行 FrankenPHP。
### 安装脚本
你可以将以下命令复制到终端中,自动安装适用于你平台的版本:
```console
curl https://frankenphp.dev/install.sh | sh
```
### 独立二进制
我们为 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
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
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
此外,还可以使用 [Docker 镜像](https://frankenphp.dev/docs/docker/)
```console
docker run -v .:/app/public \
-p 80:80 -p 443:443 -p 443:443/udp \
dunglas/frankenphp
```
访问 `https://localhost`, 并享受吧!
> [!TIP]
>
> 不要尝试使用 `https://127.0.0.1`。使用 `https://localhost` 并接受自签名证书。
> 使用 [`SERVER_NAME` 环境变量](config.md#environment-variables) 更改要使用的域。
### Homebrew
FrankenPHP 也作为 [Homebrew](https://brew.sh) 软件包提供,适用于 macOS 和 Linux 系统。
安装方法:
```console
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)
- [worker 模式](worker.md)
- [早期提示支持(103 HTTP status code)](early-hints.md)
- [实时功能](mercure.md)
- [高效地服务大型静态文件](x-sendfile.md)
- [配置](config.md)
- [用 Go 编写 PHP 扩展](extensions.md)
- [Docker 镜像](docker.md)
- [在生产环境中部署](production.md)
- [性能优化](performance.md)
- [创建独立、可自行执行的 PHP 应用程序](embed.md)
- [创建静态二进制文件](static.md)
- [从源代码编译](compile.md)
- [Laravel 集成](laravel.md)
- [已知问题](known-issues.md)
- [演示应用程序 (Symfony) 和性能测试](https://github.com/dunglas/frankenphp-demo)
- [Go 库文档](https://pkg.go.dev/github.com/dunglas/frankenphp)
- [贡献和调试](https://frankenphp.dev/docs/contributing/)
## 示例和框架
- [Symfony](https://github.com/dunglas/symfony-docker)
- [API Platform](https://api-platform.com/docs/distribution/)
- [Laravel](laravel.md)
- [Sulu](https://sulu.io/blog/running-sulu-with-frankenphp)
- [WordPress](https://github.com/StephenMiracle/frankenwp)
- [Drupal](https://github.com/dunglas/frankenphp-drupal)
- [Joomla](https://github.com/alexandreelise/frankenphp-joomla)
- [TYPO3](https://github.com/ochorocho/franken-typo3)
- [Magento2](https://github.com/ekino/frankenphp-magento2)

11
docs/cn/classic.md Normal file
View File

@@ -0,0 +1,11 @@
# 使用经典模式
在没有任何额外配置的情况下FrankenPHP 以经典模式运行。在此模式下FrankenPHP 的功能类似于传统的 PHP 服务器,直接提供 PHP 文件服务。这使其成为 PHP-FPM 或 Apache with mod_php 的无缝替代品。
与 Caddy 类似FrankenPHP 接受无限数量的连接,并使用[固定数量的线程](config.md#caddyfile-配置)来为它们提供服务。接受和排队的连接数量仅受可用系统资源的限制。
PHP 线程池使用在启动时初始化的固定数量的线程运行,类似于 PHP-FPM 的静态模式。也可以让线程在[运行时自动扩展](performance.md#max_threads),类似于 PHP-FPM 的动态模式。
排队的连接将无限期等待,直到有 PHP 线程可以为它们提供服务。为了避免这种情况,你可以在 FrankenPHP 的全局配置中使用 max_wait_time [配置](config.md#caddyfile-配置)来限制请求可以等待空闲的 PHP 线程的时间,超时后将被拒绝。
此外,你还可以在 Caddy 中设置合理的[写超时](https://caddyserver.com/docs/caddyfile/options#timeouts)。
每个 Caddy 实例只会启动一个 FrankenPHP 线程池,该线程池将在所有 `php_server` 块之间共享。

127
docs/cn/compile.md Normal file
View File

@@ -0,0 +1,127 @@
# 从源代码编译
本文档解释了如何创建一个 FrankenPHP 构建,它将 PHP 加载为一个动态库。
这是推荐的方法。
或者,你也可以 [编译静态版本](static.md)。
## 安装 PHP
FrankenPHP 支持 PHP 8.2 及更高版本。
### 使用 Homebrew (Linux 和 Mac)
安装与 FrankenPHP 兼容的 libphp 版本的最简单方法是使用 [Homebrew PHP](https://github.com/shivammathur/homebrew-php) 提供的 ZTS 包。
首先,如果尚未安装,请安装 [Homebrew](https://brew.sh)。
然后,安装 PHP 的 ZTS 变体、Brotli可选用于压缩支持和 watcher可选用于文件更改检测
```console
brew install shivammathur/php/php-zts brotli watcher
brew link --overwrite --force shivammathur/php/php-zts
```
### 通过编译 PHP
或者,你可以按照以下步骤,使用 FrankenPHP 所需的选项从源代码编译 PHP。
首先,[获取 PHP 源代码](https://www.php.net/downloads.php) 并提取它们:
```console
tar xf php-*
cd php-*/
```
然后,运行适用于你平台的 `configure` 脚本。
以下 `./configure` 标志是必需的,但你可以添加其他标志,例如编译扩展或附加功能。
#### Linux
```console
./configure \
--enable-embed \
--enable-zts \
--disable-zend-signals \
--enable-zend-max-execution-timers
```
#### Mac
使用 [Homebrew](https://brew.sh/) 包管理器安装所需的和可选的依赖项:
```console
brew install libiconv bison brotli re2c pkg-config watcher
echo 'export PATH="/opt/homebrew/opt/bison/bin:$PATH"' >> ~/.zshrc
```
然后运行 `./configure` 脚本:
```console
./configure \
--enable-embed \
--enable-zts \
--disable-zend-signals \
--with-iconv=/opt/homebrew/opt/libiconv/
```
#### 编译 PHP
最后,编译并安装 PHP
```console
make -j"$(getconf _NPROCESSORS_ONLN)"
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 |
## 编译 Go 应用
你现在可以构建最终的二进制文件。
### 使用 xcaddy
推荐的方法是使用 [xcaddy](https://github.com/caddyserver/xcaddy) 来编译 FrankenPHP。
`xcaddy` 还允许轻松添加 [自定义 Caddy 模块](https://caddyserver.com/docs/modules/) 和 FrankenPHP 扩展:
```console
CGO_ENABLED=1 \
XCADDY_GO_BUILD_FLAGS="-ldflags='-w -s' -tags=nobadger,nomysql,nopgx" \
CGO_CFLAGS=$(php-config --includes) \
CGO_LDFLAGS="$(php-config --ldflags) $(php-config --libs)" \
xcaddy build \
--output frankenphp \
--with github.com/dunglas/frankenphp/caddy \
--with github.com/dunglas/mercure/caddy \
--with github.com/dunglas/vulcain/caddy
# 在这里添加额外的 Caddy 模块和 FrankenPHP 扩展
```
> [!TIP]
>
> 如果你的系统基于 musl libcAlpine Linux 上默认使用)并搭配 Symfony 使用,
> 你可能需要增加默认堆栈大小。
> 否则,你可能会收到如下错误 `PHP Fatal error: Maximum call stack size of 83360 bytes reached during compilation. Try splitting expression`
>
> 请将 `XCADDY_GO_BUILD_FLAGS` 环境变量更改为如下类似的值
> `XCADDY_GO_BUILD_FLAGS=$'-ldflags "-w -s -extldflags \'-Wl,-z,stack-size=0x80000\'"'`
> (根据你的应用需求更改堆栈大小)。
### 不使用 xcaddy
或者,可以通过直接使用 `go` 命令来编译 FrankenPHP 而不使用 `xcaddy`
```console
curl -L https://github.com/php/frankenphp/archive/refs/heads/main.tar.gz | tar xz
cd frankenphp-main/caddy/frankenphp
CGO_CFLAGS=$(php-config --includes) CGO_LDFLAGS="$(php-config --ldflags) $(php-config --libs)" go build -tags=nobadger,nomysql,nopgx
```

305
docs/cn/config.md Normal file
View File

@@ -0,0 +1,305 @@
# 配置
FrankenPHP、Caddy 以及 Mercure 和 Vulcain 模块可以使用 [Caddy 支持的格式](https://caddyserver.com/docs/getting-started#your-first-config) 进行配置。
在 [Docker 镜像](docker.md) 中,`Caddyfile` 位于 `/etc/frankenphp/Caddyfile`
静态二进制文件也会在执行 `frankenphp run` 命令的目录中查找 `Caddyfile`
你可以使用 `-c``--config` 选项指定自定义路径。
PHP 本身可以[使用 `php.ini` 文件](https://www.php.net/manual/zh/configuration.file.php)进行配置。
根据你的安装方法PHP 解释器将在上述位置查找配置文件。
## Docker
- `php.ini`: `/usr/local/etc/php/php.ini`(默认情况下不提供 `php.ini`
- 附加配置文件: `/usr/local/etc/php/conf.d/*.ini`
- PHP 扩展: `/usr/local/lib/php/extensions/no-debug-zts-<YYYYMMDD>/`
- 你应该复制 PHP 项目提供的官方模板:
```dockerfile
FROM dunglas/frankenphp
# 生产环境:
RUN cp $PHP_INI_DIR/php.ini-production $PHP_INI_DIR/php.ini
# 或开发环境:
RUN cp $PHP_INI_DIR/php.ini-development $PHP_INI_DIR/php.ini
```
## RPM 和 Debian 包
- `php.ini`: `/etc/frankenphp/php.ini`(默认情况下提供带有生产预设的 `php.ini` 文件)
- 附加配置文件: `/etc/frankenphp/php.d/*.ini`
- PHP 扩展: `/usr/lib/frankenphp/modules/`
## 静态二进制文件
- `php.ini`: 执行 `frankenphp run``frankenphp php-server` 的目录,然后是 `/etc/frankenphp/php.ini`
- 附加配置文件: `/etc/frankenphp/php.d/*.ini`
- PHP 扩展: 无法加载,将它们打包在二进制文件本身中
- 复制 [PHP 源代码](https://github.com/php/php-src/) 中提供的 `php.ini-production``php.ini-development` 中的一个。
## Caddyfile 配置
可以在站点块中使用 `php_server``php` [HTTP 指令](https://caddyserver.com/docs/caddyfile/concepts#directives) 来为你的 PHP 应用程序提供服务。
最小示例:
```caddyfile
localhost {
# 启用压缩(可选)
encode zstd br gzip
# 在当前目录中执行 PHP 文件并提供资源服务
php_server
}
```
你还可以使用全局选项显式配置 FrankenPHP:
`frankenphp` [全局选项](https://caddyserver.com/docs/caddyfile/concepts#global-options) 可用于配置 FrankenPHP。
```caddyfile
{
frankenphp {
num_threads <num_threads> # 设置要启动的 PHP 线程数量。默认:可用 CPU 数量的 2 倍。
max_threads <max_threads> # 限制可以在运行时启动的额外 PHP 线程的数量。默认值num_threads。可以设置为 'auto'。
max_wait_time <duration> # 设置请求在超时之前可以等待的最大时间,直到找到一个空闲的 PHP 线程。 默认:禁用。
php_ini <key> <value> # 设置一个 php.ini 指令。可以多次使用以设置多个指令。
worker {
file <path> # 设置工作脚本的路径。
num <num> # 设置要启动的 PHP 线程数量,默认为可用 CPU 数量的 2 倍。
env <key> <value> # 设置一个额外的环境变量为给定的值。可以多次指定以设置多个环境变量。
watch <path> # 设置要监视文件更改的路径。可以为多个路径多次指定。
name <name> # 设置worker的名称用于日志和指标。默认值worker文件的绝对路径。
max_consecutive_failures <num> # 设置在工人被视为不健康之前的最大连续失败次数,-1意味着工人将始终重新启动。默认值6。
}
}
}
# ...
```
或者,您可以使用 `worker` 选项的一行简短形式。
```caddyfile
{
frankenphp {
worker <file> <num>
}
}
# ...
```
如果您在同一服务器上服务多个应用程序,您还可以定义多个工作线程:
```caddyfile
app.example.com {
root /path/to/app/public
php_server {
root /path/to/app/public # 允许更好的缓存
worker index.php <num>
}
}
other.example.com {
root /path/to/other/public
php_server {
root /path/to/other/public
worker index.php <num>
}
}
# ...
```
使用 `php_server` 指令通常是您需要的。
但是如果你需要完全控制,你可以使用更低级的 `php` 指令。
`php` 指令将所有输入传递给 PHP而不是先检查是否
是一个PHP文件。在[性能页面](performance.md#try_files)中了解更多关于它的信息。
使用 `php_server` 指令等同于以下配置:
```caddyfile
route {
# 为目录请求添加尾斜杠
@canonicalPath {
file {path}/index.php
not path */
}
redir @canonicalPath {path}/ 308
# 如果请求的文件不存在,则尝试 index 文件
@indexFiles file {
try_files {path} {path}/index.php index.php
split_path .php
}
rewrite @indexFiles {http.matchers.file.relative}
# FrankenPHP!
@phpFiles path *.php
php @phpFiles
file_server
}
```
`php_server``php` 指令有以下选项:
```caddyfile
php_server [<matcher>] {
root <directory> # 将根文件夹设置为站点。默认值:`root` 指令。
split_path <delim...> # 设置用于将 URI 分割成两部分的子字符串。第一个匹配的子字符串将用来将 "路径信息" 与路径分开。第一部分后缀为匹配的子字符串并将被视为实际资源CGI 脚本)名称。第二部分将被设置为脚本使用的 PATH_INFO。默认值`.php`。
resolve_root_symlink false # 禁用通过评估符号链接(如果存在)将 `root` 目录解析为其实际值(默认启用)。
env <key> <value> # 设置一个额外的环境变量为给定的值。可以多次指定以设置多个环境变量。
file_server off # 禁用内置的 file_server 指令。
worker { # 为此服务器创建特定的worker。可以多次指定以创建多个workers。
file <path> # 设置工作脚本的路径,可以相对于 php_server 根目录
num <num> # 设置要启动的 PHP 线程数,默认为可用数量的 2 倍
name <name> # 为worker设置名称用于日志和指标。默认值worker文件的绝对路径。定义在 php_server 块中时,始终以 m# 开头。
watch <path> # 设置要监视文件更改的路径。可以为多个路径多次指定。
env <key> <value> # 设置一个额外的环境变量为给定值。可以多次指定以设置多个环境变量。此工作进程的环境变量也从 php_server 父进程继承,但可以在此处覆盖。
match <path> # 将worker匹配到路径模式。覆盖 try_files并且只能在 php_server 指令中使用。
}
worker <other_file> <num> # 也可以像在全局 frankenphp 块中那样使用简短形式。
}
```
### 监控文件变化
由于 workers 只会启动您的应用程序一次并将其保留在内存中,
因此对您的 PHP 文件的任何更改不会立即反映出来。
Wworkers 可以通过 `watch` 指令在文件更改时重新启动。
这对开发环境很有用。
```caddyfile
{
frankenphp {
worker {
file /path/to/app/public/worker.php
watch
}
}
}
```
如果没有指定 `watch` 目录,它将回退到 `./**/*.{php,yaml,yml,twig,env}`
这将监视启动 FrankenPHP 进程的目录及其子目录中的所有 `.php``.yaml``.yml``.twig``.env` 文件。
你也可以通过 [shell 文件名模式](https://pkg.go.dev/path/filepath#Match) 指定一个或多个目录:
```caddyfile
{
frankenphp {
worker {
file /path/to/app/public/worker.php
watch /path/to/app # 监视 /path/to/app 所有子目录中的所有文件
watch /path/to/app/*.php # 监视位于/path/to/app中的以.php结尾的文件
watch /path/to/app/**/*.php # 监视 /path/to/app 及子目录中的 PHP 文件
watch /path/to/app/**/*.{php,twig} # 在/path/to/app及其子目录中监视PHP和Twig文件
}
}
}
```
- `**` 模式表示递归监视
- 目录也可以是相对的相对于FrankenPHP进程启动的位置
- 如果您定义了多个workers当文件发生更改时将重新启动所有workers。
- 小心查看在运行时创建的文件(如日志),因为它们可能导致不必要的工作进程重启。
文件监视器基于[e-dant/watcher](https://github.com/e-dant/watcher)。
## 将 worker 匹配到一条路径
在传统的PHP应用程序中脚本总是放在公共目录中。
这对于工作脚本也是如此这些脚本被视为任何其他PHP脚本。
如果您想将工作脚本放在公共目录外,可以通过 `match` 指令来实现。
`match` 指令是 `try_files` 的一种优化替代方案,仅在 `php_server``php` 内部可用。
以下示例将在公共目录中提供文件(如果存在)
并将请求转发给与路径模式匹配的 worker。
```caddyfile
{
frankenphp {
php_server {
worker {
file /path/to/worker.php # 文件可以在公共路径之外
match /api/* # 所有以 /api/ 开头的请求将由此 worker 处理
}
}
}
}
```
### 全双工 (HTTP/1)
在使用HTTP/1.x时可能希望启用全双工模式以便在完整主体之前写入响应。
已被阅读。(例如WebSocket、服务器发送事件等。)
这是一个可选配置,需要添加到 `Caddyfile` 中的全局选项中:
```caddyfile
{
servers {
enable_full_duplex
}
}
```
> [!CAUTION]
>
> 启用此选项可能导致不支持全双工的旧HTTP/1.x客户端死锁。
> 这也可以通过配置 `CADDY_GLOBAL_OPTIONS` 环境配置来实现:
```sh
CADDY_GLOBAL_OPTIONS="servers {
enable_full_duplex
}"
```
您可以在[Caddy文档](https://caddyserver.com/docs/caddyfile/options#enable-full-duplex)中找到有关此设置的更多信息。
## 环境变量
可以使用以下环境变量在不修改 `Caddyfile` 的情况下注入 Caddy 指令:
- `SERVER_NAME`: 更改[监听的地址](https://caddyserver.com/docs/caddyfile/concepts#addresses)提供的宿主名也将用于生成的TLS证书。
- `SERVER_ROOT`: 更改网站的根目录,默认为 `public/`
- `CADDY_GLOBAL_OPTIONS`: 注入[全局选项](https://caddyserver.com/docs/caddyfile/options)
- `FRANKENPHP_CONFIG`: 在 `frankenphp` 指令下注入配置
至于 FPM 和 CLI SAPIs环境变量默认在 `$_SERVER` 超全局中暴露。
[the `variables_order` PHP 指令](https://www.php.net/manual/en/ini.core.php#ini.variables-order) 的 `S` 值始终等于 `ES`,无论 `E` 在该指令中的其他位置如何。
## PHP 配置
加载[附加的 PHP 配置文件](https://www.php.net/manual/en/configuration.file.php#configuration.file.scan)
`PHP_INI_SCAN_DIR`环境变量可以被使用。
设置后PHP 将加载给定目录中所有带有 `.ini` 扩展名的文件。
您还可以通过在 `Caddyfile` 中使用 `php_ini` 指令来更改 PHP 配置:
```caddyfile
{
frankenphp {
php_ini memory_limit 256M
# 或者
php_ini {
memory_limit 256M
max_execution_time 15
}
}
}
```
## 启用调试模式
使用Docker镜像时`CADDY_GLOBAL_OPTIONS`环境变量设置为`debug`以启用调试模式:
```console
docker run -v $PWD:/app/public \
-e CADDY_GLOBAL_OPTIONS=debug \
-p 80:80 -p 443:443 -p 443:443/udp \
dunglas/frankenphp
```

204
docs/cn/docker.md Normal file
View File

@@ -0,0 +1,204 @@
# 构建自定义 Docker 镜像
[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 和 8.5 的变体。
标签遵循此模式:`dunglas/frankenphp:<frankenphp-version>-php<php-version>-<os>`
- `<frankenphp-version>``<php-version>` 分别是 FrankenPHP 和 PHP 的版本号,范围从主版本(例如 `1`)、次版本(例如 `1.2`)到补丁版本(例如 `1.2.3`)。
- `<os>` 要么是 `bookworm`(用于 Debian Bookworm要么是 `alpine`(用于 Alpine 的最新稳定版本)。
[浏览标签](https://hub.docker.com/r/dunglas/frankenphp/tags)。
## 如何使用镜像
在项目中创建 `Dockerfile`
```dockerfile
FROM dunglas/frankenphp
COPY . /app/public
```
然后运行以下命令以构建并运行 Docker 镜像:
```console
docker build -t my-php-app .
docker run -it --rm --name my-running-app my-php-app
```
## 如何安装更多 PHP 扩展
[`docker-php-extension-installer`](https://github.com/mlocati/docker-php-extension-installer) 脚本在基础镜像中提供。
添加额外的 PHP 扩展很简单:
```dockerfile
FROM dunglas/frankenphp
# 在此处添加其他扩展:
RUN install-php-extensions \
pdo_mysql \
gd \
intl \
zip \
opcache
```
## 如何安装更多 Caddy 模块
FrankenPHP 建立在 Caddy 之上,所有 [Caddy 模块](https://caddyserver.com/docs/modules/) 都可以与 FrankenPHP 一起使用。
安装自定义 Caddy 模块的最简单方法是使用 [xcaddy](https://github.com/caddyserver/xcaddy)
```dockerfile
FROM dunglas/frankenphp:builder AS builder
# 在构建器镜像中复制 xcaddy
COPY --from=caddy:builder /usr/bin/xcaddy /usr/bin/xcaddy
# 必须启用 CGO 才能构建 FrankenPHP
RUN CGO_ENABLED=1 \
XCADDY_SETCAP=1 \
XCADDY_GO_BUILD_FLAGS="-ldflags='-w -s' -tags=nobadger,nomysql,nopgx" \
CGO_CFLAGS=$(php-config --includes) \
CGO_LDFLAGS="$(php-config --ldflags) $(php-config --libs)" \
xcaddy build \
--output /usr/local/bin/frankenphp \
--with github.com/dunglas/frankenphp=./ \
--with github.com/dunglas/frankenphp/caddy=./caddy/ \
--with github.com/dunglas/caddy-cbrotli \
# Mercure 和 Vulcain 包含在官方版本中,如果不需要你可以删除它们
--with github.com/dunglas/mercure/caddy \
--with github.com/dunglas/vulcain/caddy
# 在此处添加额外的 Caddy 模块
FROM dunglas/frankenphp AS runner
# 将官方二进制文件替换为包含自定义模块的二进制文件
COPY --from=builder /usr/local/bin/frankenphp /usr/local/bin/frankenphp
```
FrankenPHP 提供的 `builder` 镜像包含 `libphp` 的编译版本。
[用于构建的镜像](https://hub.docker.com/r/dunglas/frankenphp/tags?name=builder) 适用于所有版本的 FrankenPHP 和 PHP包括 Alpine 和 Debian。
> [!TIP]
>
> 如果你的系统基于 musl libcAlpine Linux 上默认使用)并搭配 Symfony 使用,
> 你可能需要 [增加默认堆栈大小](compile.md#using-xcaddy)。
## 默认启用 worker 模式
设置 `FRANKENPHP_CONFIG` 环境变量以使用 worker 脚本启动 FrankenPHP
```dockerfile
FROM dunglas/frankenphp
# ...
ENV FRANKENPHP_CONFIG="worker ./public/index.php"
```
## 开发挂载宿主机目录
要使用 FrankenPHP 轻松开发,请从包含应用程序源代码的主机挂载目录作为 Docker 容器中的 volume
```console
docker run -v $PWD:/app/public -p 80:80 -p 443:443 -p 443:443/udp --tty my-php-app
```
> [!TIP]
>
> `--tty` 选项允许使用清晰可读的日志,而不是 JSON 日志。
使用 Docker Compose
```yaml
# compose.yaml
services:
php:
image: dunglas/frankenphp
# 如果要使用自定义 Dockerfile请取消注释以下行
#build: .
# 如果要在生产环境中运行,请取消注释以下行
# restart: always
ports:
- "80:80" # HTTP
- "443:443" # HTTPS
- "443:443/udp" # HTTP/3
volumes:
- ./:/app/public
- caddy_data:/data
- caddy_config:/config
# 在生产环境中注释以下行,它允许在 dev 中使用清晰可读日志
tty: true
# Caddy 证书和配置所需的挂载目录
volumes:
caddy_data:
caddy_config:
```
## 以非 root 用户身份运行
FrankenPHP 可以在 Docker 中以非 root 用户身份运行。
下面是一个示例 `Dockerfile`
```dockerfile
FROM dunglas/frankenphp
ARG USER=appuser
RUN \
# 在基于 alpine 的发行版使用 "adduser -D ${USER}"
useradd ${USER}; \
# 需要开放80和443端口的权限
setcap CAP_NET_BIND_SERVICE=+eip /usr/local/bin/frankenphp; \
# 需要 /config/caddy 和 /data/caddy 目录的写入权限
chown -R ${USER}:${USER} /config/caddy /data/caddy
USER ${USER}
```
### 无权限运行
即使在无根运行时FrankenPHP 也需要 `CAP_NET_BIND_SERVICE` 权限来将
Web 服务器绑定到特权端口80 和 443
如果你在非特权端口1024 及以上)上公开 FrankenPHP则可以以非 root 用户身份运行
Web 服务器,并且不需要任何权限:
```dockerfile
FROM dunglas/frankenphp
ARG USER=appuser
RUN \
# 在基于 alpine 的发行版使用 "adduser -D ${USER}"
useradd ${USER}; \
# 移除默认权限
setcap -r /usr/local/bin/frankenphp; \
# 给予 /config/caddy 和 /data/caddy 写入权限
chown -R ${USER}:${USER} /config/caddy /data/caddy
USER ${USER}
```
接下来,设置 `SERVER_NAME` 环境变量以使用非特权端口。
示例:`:8000`
## 更新
Docker 镜像会按照以下条件更新:
- 发布新的版本后
- 每日 4:00UTC 时间)检查新的 PHP 镜像
## 开发版本
可在此 [`dunglas/frankenphp-dev`](https://hub.docker.com/repository/docker/dunglas/frankenphp-dev) 仓库获取开发版本。
每次在 GitHub 仓库的主分支有新的 commit 都会触发一次新的 build。
`latest*` tag 指向最新的 `main` 分支,且同样支持 `sha-<git-commit-hash>` 的 tag。

21
docs/cn/early-hints.md Normal file
View File

@@ -0,0 +1,21 @@
# 早期提示
FrankenPHP 原生支持 [103 Early Hints 状态码](https://developer.chrome.com/blog/early-hints/)。
使用早期提示可以将网页的加载时间缩短 30%。
```php
<?php
header('Link: </style.css>; rel=preload; as=style');
headers_send(103);
// 慢速算法和 SQL 查询
echo <<<'HTML'
<!DOCTYPE html>
<title>Hello FrankenPHP</title>
<link rel="stylesheet" href="style.css">
HTML;
```
早期提示由普通模式和 [worker](worker.md) 模式支持。

144
docs/cn/embed.md Normal file
View File

@@ -0,0 +1,144 @@
# PHP 应用程序作为独立二进制文件
FrankenPHP 能够将 PHP 应用程序的源代码和资源文件嵌入到静态的、独立的二进制文件中。
由于这个特性PHP 应用程序可以作为独立的二进制文件分发包括应用程序本身、PHP 解释器和生产级 Web 服务器 Caddy。
了解有关此功能的更多信息 [Kévin 在 SymfonyCon 上的演讲](https://dunglas.dev/2023/12/php-and-symfony-apps-as-standalone-binaries/)。
有关嵌入 Laravel 应用程序,请[阅读此特定文档条目](laravel.md#laravel-apps-as-standalone-binaries)。
## 准备你的应用
在创建独立二进制文件之前,请确保应用已准备好进行打包。
例如,你可能希望:
- 给应用安装生产环境的依赖
- 导出 autoloader
- 如果可能,为应用启用生产模式
- 丢弃不需要的文件,例如 `.git` 或测试文件,以减小最终二进制文件的大小
例如,对于 Symfony 应用程序,你可以使用以下命令:
```console
# 导出项目以避免 .git/ 等目录
mkdir $TMPDIR/my-prepared-app
git archive HEAD | tar -x -C $TMPDIR/my-prepared-app
cd $TMPDIR/my-prepared-app
# 设置适当的环境变量
echo APP_ENV=prod > .env.local
echo APP_DEBUG=0 >> .env.local
# 删除测试和其他不需要的文件以节省空间
# 或者,将这些文件添加到您的 .gitattributes 文件中,并设置 export-ignore 属性
rm -Rf tests/
# 安装依赖项
composer install --ignore-platform-reqs --no-dev -a
# 优化 .env
composer dump-env prod
```
### 自定义配置
要自定义[配置](config.md),您可以放置一个 `Caddyfile` 以及一个 `php.ini` 文件
在应用程序的主目录中嵌入(在之前的示例中是`$TMPDIR/my-prepared-app`)。
## 创建 Linux 二进制文件
创建 Linux 二进制文件的最简单方法是使用我们提供的基于 Docker 的构建器。
1. 在准备好的应用的存储库中创建一个名为 `static-build.Dockerfile` 的文件。
```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/
RUN EMBED=dist/app/ ./build-static.sh
```
> [!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 .
```
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
```
生成的二进制文件是当前目录中名为 `my-app` 的文件。
## 为其他操作系统创建二进制文件
如果你不想使用 Docker或者想要构建 macOS 二进制文件,你可以使用我们提供的 shell 脚本:
```console
git clone https://github.com/php/frankenphp
cd frankenphp
EMBED=/path/to/your/app ./build-static.sh
```
在 `dist/` 目录中生成的二进制文件名称为 `frankenphp-<os>-<arch>`。
## 使用二进制文件
就是这样!`my-app` 文件(或其他操作系统上的 `dist/frankenphp-<os>-<arch>`)包含你的独立应用程序!
若要启动 Web 应用,请执行:
```console
./my-app php-server
```
如果你的应用包含 [worker 脚本](worker.md),请使用如下命令启动 worker
```console
./my-app php-server --worker public/index.php
```
要启用 HTTPS自动创建 Let's Encrypt 证书、HTTP/2 和 HTTP/3请指定要使用的域名
```console
./my-app php-server --domain localhost
```
你还可以运行二进制文件中嵌入的 PHP CLI 脚本:
```console
./my-app php-cli bin/console
```
## PHP Extensions
默认情况下,脚本将构建您项目的 `composer.json` 文件中所需的扩展(如果有的话)。
如果 `composer.json` 文件不存在,将构建默认扩展,如 [静态构建条目](static.md) 中所述。
要自定义扩展,请使用 `PHP_EXTENSIONS` 环境变量。
## 自定义构建
[阅读静态构建文档](static.md) 查看如何自定义二进制文件扩展、PHP 版本等)。
## 分发二进制文件
在Linux上创建的二进制文件使用[UPX](https://upx.github.io)进行压缩。
在Mac上您可以在发送文件之前压缩它以减小文件大小。
我们推荐使用 `xz`。

747
docs/cn/extensions.md Normal file
View File

@@ -0,0 +1,747 @@
# 使用 Go 编写 PHP 扩展
使用 FrankenPHP你可以**使用 Go 编写 PHP 扩展**,这允许你创建**高性能的原生函数**,可以直接从 PHP 调用。你的应用程序可以利用任何现有或新的 Go 库,以及直接从你的 PHP 代码中使用**协程goroutines的并发模型**。
编写 PHP 扩展通常使用 C 语言完成但通过一些额外的工作也可以使用其他语言编写。PHP 扩展允许你利用底层语言的强大功能来扩展 PHP 的功能,例如,通过添加原生函数或优化特定操作。
借助 Caddy 模块,你可以使用 Go 编写 PHP 扩展,并将其快速集成到 FrankenPHP 中。
## 两种方法
FrankenPHP 提供两种方式来创建 Go 语言的 PHP 扩展:
1. **使用扩展生成器** - 推荐的方法,为大多数用例生成所有必要的样板代码,让你专注于编写 Go 代码
2. **手动实现** - 对于高级用例,完全控制扩展结构
我们将从生成器方法开始,因为这是最简单的入门方式,然后为那些需要完全控制的人展示手动实现。
## 使用扩展生成器
FrankenPHP 捆绑了一个工具,允许你**仅使用 Go 创建 PHP 扩展**。**无需编写 C 代码**或直接使用 CGOFrankenPHP 还包含一个**公共类型 API**,帮助你在 Go 中编写扩展,而无需担心**PHP/C 和 Go 之间的类型转换**。
> [!TIP]
> 如果你想了解如何从头开始在 Go 中编写扩展,可以阅读下面的手动实现部分,该部分演示了如何在不使用生成器的情况下在 Go 中编写 PHP 扩展。
请记住,此工具**不是功能齐全的扩展生成器**。它旨在帮助你在 Go 中编写简单的扩展,但它不提供 PHP 扩展的最高级功能。如果你需要编写更**复杂和优化**的扩展,你可能需要编写一些 C 代码或直接使用 CGO。
### 先决条件
正如下面的手动实现部分所涵盖的,你需要[获取 PHP 源代码](https://www.php.net/downloads.php)并创建一个新的 Go 模块。
#### 创建新模块并获取 PHP 源代码
在 Go 中编写 PHP 扩展的第一步是创建一个新的 Go 模块。你可以使用以下命令:
```console
go mod init github.com/my-account/my-module
```
第二步是为后续步骤[获取 PHP 源代码](https://www.php.net/downloads.php)。获取后,将它们解压到你选择的目录中,不要放在你的 Go 模块内:
```console
tar xf php-*
```
### 编写扩展
现在一切都设置好了,可以在 Go 中编写你的原生函数。创建一个名为 `stringext.go` 的新文件。我们的第一个函数将接受一个字符串作为参数,重复次数,一个布尔值来指示是否反转字符串,并返回结果字符串。这应该看起来像这样:
```go
import (
"C"
"github.com/dunglas/frankenphp"
"strings"
)
//export_php:function repeat_this(string $str, int $count, bool $reverse): string
func repeat_this(s *C.zend_string, count int64, reverse bool) unsafe.Pointer {
str := frankenphp.GoString(unsafe.Pointer(s))
result := strings.Repeat(str, int(count))
if reverse {
runes := []rune(result)
for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
runes[i], runes[j] = runes[j], runes[i]
}
result = string(runes)
}
return frankenphp.PHPString(result, false)
}
```
这里有两个重要的事情要注意:
- 指令注释 `//export_php:function` 定义了 PHP 中的函数签名。这是生成器知道如何使用正确的参数和返回类型生成 PHP 函数的方式;
- 函数必须返回 `unsafe.Pointer`。FrankenPHP 提供了一个 API 来帮助你在 C 和 Go 之间进行类型转换。
虽然第一点不言自明,但第二点可能更难理解。让我们在下一节中深入了解类型转换。
### 类型转换
虽然一些变量类型在 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() | ✅ |
| `mixed` | `any` | ❌ | `GoValue()` | `PHPValue()` | ❌ |
| `object` | `struct` | ❌ | _尚未实现_ | _尚未实现_ | ❌ |
> [!NOTE]
> 此表尚不详尽,将随着 FrankenPHP 类型 API 变得更加完整而完善。
>
> 特别是对于类方法,目前支持原始类型和数组。对象尚不能用作方法参数或返回类型。
如果你参考上一节的代码片段,你可以看到助手用于转换第一个参数和返回值。我们的 `repeat_this()` 函数的第二和第三个参数不需要转换,因为底层类型的内存表示对于 C 和 Go 都是相同的。
#### 处理数组
FrankenPHP 通过 `frankenphp.Array` 类型为 PHP 数组提供原生支持。此类型表示 PHP 索引数组(列表)和关联数组(哈希映射),具有有序的键值对。
**在 Go 中创建和操作数组:**
```go
//export_php:function process_data(array $input): array
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 {
result.SetString("processed_"+key.Str, value)
} else {
result.SetInt(key.Int+100, value)
}
}
// 转换回 PHP 数组
return frankenphp.PHPArray(result)
}
```
**`frankenphp.Array` 的关键特性:**
- **有序键值对** - 像 PHP 数组一样维护插入顺序
- **混合键类型** - 在同一数组中支持整数和字符串键
- **类型安全** - `PHPKey` 类型确保正确的键处理
- **自动列表检测** - 转换为 PHP 时,自动检测数组应该是打包列表还是哈希映射
- **不支持对象** - 目前,只有标量类型和数组可以用作值。提供对象将导致 PHP 数组中的 `null` 值。
**可用方法:**
- `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 类
生成器支持将 Go 结构体声明为**不透明类**,可用于创建 PHP 对象。你可以使用 `//export_php:class` 指令注释来定义 PHP 类。例如:
```go
//export_php:class User
type UserStruct struct {
Name string
Age int
}
```
#### 什么是不透明类?
**不透明类**是内部结构(属性)对 PHP 代码隐藏的类。这意味着:
- **无直接属性访问**:你不能直接从 PHP 读取或写入属性(`$user->name` 不起作用)
- **仅方法接口** - 所有交互必须通过你定义的方法进行
- **更好的封装** - 内部数据结构完全由 Go 代码控制
- **类型安全** - 没有 PHP 代码使用错误类型破坏内部状态的风险
- **更清晰的 API** - 强制设计适当的公共接口
这种方法提供了更好的封装,并防止 PHP 代码意外破坏 Go 对象的内部状态。与对象的所有交互都必须通过你明确定义的方法进行。
#### 为类添加方法
由于属性不能直接访问,你**必须定义方法**来与不透明类交互。使用 `//export_php:method` 指令来定义行为:
```go
//export_php:class User
type UserStruct struct {
Name string
Age int
}
//export_php:method User::getName(): string
func (us *UserStruct) GetUserName() unsafe.Pointer {
return frankenphp.PHPString(us.Name, false)
}
//export_php:method User::setAge(int $age): void
func (us *UserStruct) SetUserAge(age int64) {
us.Age = int(age)
}
//export_php:method User::getAge(): int
func (us *UserStruct) GetUserAge() int64 {
return int64(us.Age)
}
//export_php:method User::setNamePrefix(string $prefix = "User"): void
func (us *UserStruct) SetNamePrefix(prefix *C.zend_string) {
us.Name = frankenphp.GoString(unsafe.Pointer(prefix)) + ": " + us.Name
}
```
#### 可空参数
生成器支持在 PHP 签名中使用 `?` 前缀的可空参数。当参数可空时,它在你的 Go 函数中变成指针,允许你检查值在 PHP 中是否为 `null`
```go
//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不为 null
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
}
}
```
**关于可空参数的要点:**
- **可空原始类型**`?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`)。
生成扩展后,你将被允许在 PHP 中使用类及其方法。请注意,你**不能直接访问属性**
```php
<?php
$user = new User();
// ✅ 这可以工作 - 使用方法
$user->setAge(25);
echo $user->getName(); // 输出:(空,默认值)
echo $user->getAge(); // 输出25
$user->setNamePrefix("Employee");
// ✅ 这也可以工作 - 可空参数
$user->updateInfo("John", 30, true); // 提供所有参数
$user->updateInfo("Jane", null, false); // Age 为 null
$user->updateInfo(null, 25, null); // Name 和 active 为 null
// ❌ 这不会工作 - 直接属性访问
// echo $user->name; // 错误:无法访问私有属性
// $user->age = 30; // 错误:无法访问私有属性
```
这种设计确保你的 Go 代码完全控制如何访问和修改对象的状态,提供更好的封装和类型安全。
### 声明常量
生成器支持使用两个指令将 Go 常量导出到 PHP`//export_php:const` 用于全局常量,`//export_php:classconst` 用于类常量。这允许你在 Go 和 PHP 代码之间共享配置值、状态代码和其他常量。
#### 全局常量
使用 `//export_php:const` 指令创建全局 PHP 常量:
```go
//export_php:const
const MAX_CONNECTIONS = 100
//export_php:const
const API_VERSION = "1.2.3"
//export_php:const
const STATUS_OK = iota
//export_php:const
const STATUS_ERROR = iota
```
#### 类常量
使用 `//export_php:classconst ClassName` 指令创建属于特定 PHP 类的常量:
```go
//export_php:classconst User
const STATUS_ACTIVE = 1
//export_php:classconst User
const STATUS_INACTIVE = 0
//export_php:classconst User
const ROLE_ADMIN = "admin"
//export_php:classconst Order
const STATE_PENDING = iota
//export_php:classconst Order
const STATE_PROCESSING = iota
//export_php:classconst Order
const STATE_COMPLETED = iota
```
类常量在 PHP 中使用类名作用域访问:
```php
<?php
// 全局常量
echo MAX_CONNECTIONS; // 100
echo API_VERSION; // "1.2.3"
// 类常量
echo User::STATUS_ACTIVE; // 1
echo User::ROLE_ADMIN; // "admin"
echo Order::STATE_PENDING; // 0
```
该指令支持各种值类型,包括字符串、整数、布尔值、浮点数和 iota 常量。使用 `iota`生成器自动分配顺序值0、1、2 等)。全局常量在你的 PHP 代码中作为全局常量可用,而类常量使用公共可见性限定在各自的类中。使用整数时,支持不同的可能记法(二进制、十六进制、八进制)并在 PHP 存根文件中按原样转储。
你可以像在 Go 代码中习惯的那样使用常量。例如,让我们采用我们之前声明的 `repeat_this()` 函数,并将最后一个参数更改为整数:
```go
import (
"C"
"github.com/dunglas/frankenphp"
"strings"
)
//export_php:const
const STR_REVERSE = iota
//export_php:const
const STR_NORMAL = iota
//export_php:classconst StringProcessor
const MODE_LOWERCASE = 1
//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))
result := strings.Repeat(str, int(count))
if mode == STR_REVERSE {
// 反转字符串
}
if mode == STR_NORMAL {
// 无操作,只是为了展示常量
}
return frankenphp.PHPString(result, false)
}
//export_php:class StringProcessor
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)
}
```
### 使用命名空间
生成器支持使用 `//export_php:namespace` 指令将 PHP 扩展的函数、类和常量组织在命名空间下。这有助于避免命名冲突,并为扩展的 API 提供更好的组织。
#### 声明命名空间
在你的 Go 文件顶部使用 `//export_php:namespace` 指令,将所有导出的符号放在特定命名空间下:
```go
//export_php:namespace My\Extension
package main
import "C"
//export_php:function hello(): string
func hello() string {
return "Hello from My\\Extension namespace!"
}
//export_php:class User
type UserStruct struct {
// 内部字段
}
//export_php:method User::getName(): string
func (u *UserStruct) GetName() unsafe.Pointer {
return frankenphp.PHPString("John Doe", false)
}
//export_php:const
const STATUS_ACTIVE = 1
```
#### 在 PHP 中使用命名空间扩展
当声明命名空间时,所有函数、类和常量都放在 PHP 中的该命名空间下:
```php
<?php
echo My\Extension\hello(); // "Hello from My\Extension namespace!"
$user = new My\Extension\User();
echo $user->getName(); // "John Doe"
echo My\Extension\STATUS_ACTIVE; // 1
```
#### 重要说明
- 每个文件只允许**一个**命名空间指令。如果找到多个命名空间指令,生成器将返回错误。
- 命名空间适用于文件中的**所有**导出符号:函数、类、方法和常量。
- 命名空间名称遵循 PHP 命名空间约定,使用反斜杠(`\`)作为分隔符。
- 如果没有声明命名空间,符号将照常导出到全局命名空间。
### 生成扩展
这就是魔法发生的地方,现在可以生成你的扩展。你可以使用以下命令运行生成器:
```console
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` 脚本。
如果一切顺利,应该创建了一个名为 `build` 的新目录。此目录包含扩展的生成文件,包括带有生成的 PHP 函数存根的 `my_extension.go` 文件。
### 将生成的扩展集成到 FrankenPHP 中
我们的扩展现在已准备好编译并集成到 FrankenPHP 中。为此,请参阅 FrankenPHP [编译文档](compile.md)以了解如何编译 FrankenPHP。使用 `--with` 标志添加模块,指向你的模块路径:
```console
CGO_ENABLED=1 \
XCADDY_GO_BUILD_FLAGS="-ldflags='-w -s' -tags=nobadger,nomysql,nopgx" \
CGO_CFLAGS=$(php-config --includes) \
CGO_LDFLAGS="$(php-config --ldflags) $(php-config --libs)" \
xcaddy build \
--output frankenphp \
--with github.com/my-account/my-module/build
```
请注意,你指向在生成步骤中创建的 `/build` 子目录。但是,这不是强制性的:你也可以将生成的文件复制到你的模块目录并直接指向它。
### 测试你的生成扩展
你可以创建一个 PHP 文件来测试你创建的函数和类。例如,创建一个包含以下内容的 `index.php` 文件:
```php
<?php
// 使用全局常量
var_dump(repeat_this('Hello World', 5, STR_REVERSE));
// 使用类常量
$processor = new StringProcessor();
echo $processor->process('Hello World', StringProcessor::MODE_LOWERCASE); // "hello world"
echo $processor->process('Hello World', StringProcessor::MODE_UPPERCASE); // "HELLO WORLD"
```
一旦你按照上一节所示将扩展集成到 FrankenPHP 中,你就可以使用 `./frankenphp php-server` 运行此测试文件,你应该看到你的扩展正在工作。
## 手动实现
如果你想了解扩展的工作原理或需要完全控制你的扩展,你可以手动编写它们。这种方法给你完全的控制,但需要更多的样板代码。
### 基本函数
我们将看到如何在 Go 中编写一个简单的 PHP 扩展,定义一个新的原生函数。此函数将从 PHP 调用,并将触发一个在 Caddy 日志中记录消息的协程。此函数不接受任何参数并且不返回任何内容。
#### 定义 Go 函数
在你的模块中,你需要定义一个新的原生函数,该函数将从 PHP 调用。为此,创建一个你想要的名称的文件,例如 `extension.go`,并添加以下代码:
```go
package ext_go
//#include "extension.h"
import "C"
import (
"unsafe"
"github.com/caddyserver/caddy/v2"
"github.com/dunglas/frankenphp"
)
func init() {
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!")
}()
}
```
`frankenphp.RegisterExtension()` 函数通过处理内部 PHP 注册逻辑简化了扩展注册过程。`go_print_something` 函数使用 `//export` 指令表示它将在我们将编写的 C 代码中可访问,这要归功于 CGO。
在此示例中,我们的新函数将触发一个在 Caddy 日志中记录消息的协程。
#### 定义 PHP 函数
为了允许 PHP 调用我们的函数,我们需要定义相应的 PHP 函数。为此,我们将创建一个存根文件,例如 `extension.stub.php`,其中包含以下代码:
```php
<?php
/** @generate-class-entries */
function go_print(): void {}
```
此文件定义了 `go_print()` 函数的签名,该函数将从 PHP 调用。`@generate-class-entries` 指令允许 PHP 自动为我们的扩展生成函数条目。
这不是手动完成的,而是使用 PHP 源代码中提供的脚本(确保根据你的 PHP 源代码所在位置调整 `gen_stub.php` 脚本的路径):
```bash
php ../php-src/build/gen_stub.php extension.stub.php
```
此脚本将生成一个名为 `extension_arginfo.h` 的文件,其中包含 PHP 知道如何定义和调用我们函数所需的信息。
#### 编写 Go 和 C 之间的桥梁
现在,我们需要编写 Go 和 C 之间的桥梁。在你的模块目录中创建一个名为 `extension.h` 的文件,内容如下:
```c
#ifndef _EXTENSION_H
#define _EXTENSION_H
#include <php.h>
extern zend_module_entry ext_module_entry;
#endif
```
接下来,创建一个名为 `extension.c` 的文件,该文件将执行以下步骤:
- 包含 PHP 头文件;
- 声明我们的新原生 PHP 函数 `go_print()`
- 声明扩展元数据。
让我们首先包含所需的头文件:
```c
#include <php.h>
#include "extension.h"
#include "extension_arginfo.h"
// 包含 Go 导出的符号
#include "_cgo_export.h"
```
然后我们将 PHP 函数定义为原生语言函数:
```c
PHP_FUNCTION(go_print)
{
ZEND_PARSE_PARAMETERS_NONE();
go_print_something();
}
zend_module_entry ext_module_entry = {
STANDARD_MODULE_HEADER,
"ext_go",
ext_functions, /* Functions */
NULL, /* MINIT */
NULL, /* MSHUTDOWN */
NULL, /* RINIT */
NULL, /* RSHUTDOWN */
NULL, /* MINFO */
"0.1.1",
STANDARD_MODULE_PROPERTIES
};
```
在这种情况下,我们的函数不接受参数并且不返回任何内容。它只是调用我们之前定义的 Go 函数,使用 `//export` 指令导出。
最后,我们在 `zend_module_entry` 结构中定义扩展的元数据,例如其名称、版本和属性。这些信息对于 PHP 识别和加载我们的扩展是必需的。请注意,`ext_functions` 是指向我们定义的 PHP 函数的指针数组,它由 `gen_stub.php` 脚本在 `extension_arginfo.h` 文件中自动生成。
扩展注册由我们在 Go 代码中调用的 FrankenPHP 的 `RegisterExtension()` 函数自动处理。
### 高级用法
现在我们知道了如何在 Go 中创建基本的 PHP 扩展,让我们复杂化我们的示例。我们现在将创建一个 PHP 函数,该函数接受一个字符串作为参数并返回其大写版本。
#### 定义 PHP 函数存根
为了定义新的 PHP 函数,我们将修改我们的 `extension.stub.php` 文件以包含新的函数签名:
```php
<?php
/** @generate-class-entries */
/**
* 将字符串转换为大写。
*
* @param string $string 要转换的字符串。
* @return string 字符串的大写版本。
*/
function go_upper(string $string): string {}
```
> [!TIP]
> 不要忽视函数的文档!你可能会与其他开发人员共享扩展存根,以记录如何使用你的扩展以及哪些功能可用。
通过使用 `gen_stub.php` 脚本重新生成存根文件,`extension_arginfo.h` 文件应该如下所示:
```c
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_go_upper, 0, 1, IS_STRING, 0)
ZEND_ARG_TYPE_INFO(0, string, IS_STRING, 0)
ZEND_END_ARG_INFO()
ZEND_FUNCTION(go_upper);
static const zend_function_entry ext_functions[] = {
ZEND_FE(go_upper, arginfo_go_upper)
ZEND_FE_END
};
```
我们可以看到 `go_upper` 函数定义了一个 `string` 类型的参数和一个 `string` 的返回类型。
#### Go 和 PHP/C 之间的类型转换
你的 Go 函数不能直接接受 PHP 字符串作为参数。你需要将其转换为 Go 字符串。幸运的是FrankenPHP 提供了助手函数来处理 PHP 字符串和 Go 字符串之间的转换,类似于我们在生成器方法中看到的。
头文件保持简单:
```c
#ifndef _EXTENSION_H
#define _EXTENSION_H
#include <php.h>
extern zend_module_entry ext_module_entry;
#endif
```
我们现在可以在我们的 `extension.c` 文件中编写 Go 和 C 之间的桥梁。我们将 PHP 字符串直接传递给我们的 Go 函数:
```c
PHP_FUNCTION(go_upper)
{
zend_string *str;
ZEND_PARSE_PARAMETERS_START(1, 1)
Z_PARAM_STR(str)
ZEND_PARSE_PARAMETERS_END();
zend_string *result = go_upper(str);
RETVAL_STR(result);
}
```
你可以在 [PHP 内部手册](https://www.phpinternalsbook.com/php7/extensions_design/php_functions.html#parsing-parameters-zend-parse-parameters) 的专门页面中了解更多关于 `ZEND_PARSE_PARAMETERS_START` 和参数解析的信息。在这里,我们告诉 PHP 我们的函数接受一个 `string` 类型的强制参数作为 `zend_string`。然后我们将此字符串直接传递给我们的 Go 函数,并使用 `RETVAL_STR` 返回结果。
只剩下一件事要做:在 Go 中实现 `go_upper` 函数。
#### 实现 Go 函数
我们的 Go 函数将接受 `*C.zend_string` 作为参数,使用 FrankenPHP 的助手函数将其转换为 Go 字符串,处理它,并将结果作为新的 `*C.zend_string` 返回。助手函数为我们处理所有内存管理和转换复杂性。
```go
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))
}
```
这种方法比手动内存管理更清洁、更安全。FrankenPHP 的助手函数自动处理 PHP 的 `zend_string` 格式和 Go 字符串之间的转换。`PHPString()` 中的 `false` 参数表示我们想要创建一个新的非持久字符串(在请求结束时释放)。
> [!TIP]
> 在此示例中,我们不执行任何错误处理,但你应该始终检查指针不是 `nil` 并且数据在 Go 函数中使用之前是有效的。
### 将扩展集成到 FrankenPHP 中
我们的扩展现在已准备好编译并集成到 FrankenPHP 中。为此,请参阅 FrankenPHP [编译文档](compile.md)以了解如何编译 FrankenPHP。使用 `--with` 标志添加模块,指向你的模块路径:
```console
CGO_ENABLED=1 \
XCADDY_GO_BUILD_FLAGS="-ldflags='-w -s' -tags=nobadger,nomysql,nopgx" \
CGO_CFLAGS=$(php-config --includes) \
CGO_LDFLAGS="$(php-config --ldflags) $(php-config --libs)" \
xcaddy build \
--output frankenphp \
--with github.com/my-account/my-module
```
就是这样!你的扩展现在集成到 FrankenPHP 中,可以在你的 PHP 代码中使用。
### 测试你的扩展
将扩展集成到 FrankenPHP 后,你可以为你实现的函数创建一个包含示例的 `index.php` 文件:
```php
<?php
// 测试基本函数
go_print();
// 测试高级函数
echo go_upper("hello world") . "\n";
```
你现在可以使用 `./frankenphp php-server` 运行带有此文件的 FrankenPHP你应该看到你的扩展正在工作。

31
docs/cn/github-actions.md Normal file
View File

@@ -0,0 +1,31 @@
# 使用 GitHub Actions
此存储库构建 Docker 镜像并将其部署到 [Docker Hub](https://hub.docker.com/r/dunglas/frankenphp) 上
每个批准的拉取请求或设置后在你自己的分支上。
## 设置 GitHub Actions
在存储库设置中的 `secrets` 下,添加以下字段:
- `REGISTRY_LOGIN_SERVER`: 要使用的 Docker registry`docker.io`)。
- `REGISTRY_USERNAME`: 用于登录 registry 的用户名(如 `dunglas`)。
- `REGISTRY_PASSWORD`: 用于登录 registry 的密码(如 `access key`)。
- `IMAGE_NAME`: 镜像的名称(如 `dunglas/frankenphp`)。
## 构建和推送镜像
1. 创建 Pull Request 或推送到你的 Fork 分支。
2. GitHub Actions 将生成镜像并运行每项测试。
3. 如果生成成功,则将使用 `pr-x` 推送 registry其中 `x` 是 PR 编号,作为标记将镜像推送到注册表。
## 部署镜像
1. 合并 Pull Request 后GitHub Actions 将再次运行测试并生成新镜像。
2. 如果构建成功,则 Docker 注册表中的 `main` tag 将更新。
## 发布
1. 在项目仓库中创建新 Tag。
2. GitHub Actions 将生成镜像并运行每项测试。
3. 如果构建成功,镜像将使用标记名称作为标记推送到 registry例如将创建 `v1.2.3``v1.2`)。
4. `latest` 标签也将更新。

143
docs/cn/known-issues.md Normal file
View File

@@ -0,0 +1,143 @@
# 已知问题
## 不支持的 PHP 扩展
已知以下扩展与 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/) | 不安全的线程 | - |
## 有缺陷的 PHP 扩展
以下扩展在与 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
[get_browser()](https://www.php.net/manual/en/function.get-browser.php) 函数在一段时间后似乎表现不佳。解决方法是缓存(例如使用 [APCu](https://www.php.net/manual/zh/book.apcu.php))每个 User-Agent因为它们是不变的。
## 独立的二进制和基于 Alpine 的 Docker 镜像
独立的二进制文件和基于 Alpine 的 Docker 镜像 (`dunglas/frankenphp:*-alpine`) 使用的是 [musl libc](https://musl.libc.org/) 而不是 [glibc and friends](https://www.etalabs.net/compare_libcs.html)为的是保持较小的二进制大小。这可能会导致一些兼容性问题。特别是glob 标志 `GLOB_BRACE` [不可用](https://www.php.net/manual/en/function.glob.php)。
## 在 Docker 中使用 `https://127.0.0.1`
默认情况下FrankenPHP 会为 `localhost` 生成一个 TLS 证书。
这是本地开发最简单且推荐的选项。
如果确实想使用 `127.0.0.1` 作为主机,可以通过将服务器名称设置为 `127.0.0.1` 来配置它以为其生成证书。
如果你使用 Docker因为 [Docker 网络](https://docs.docker.com/network/) 问题,只做这些是不够的。
你将收到类似于以下内容的 TLS 错误 `curl: (35) LibreSSL/3.3.6: error:1404B438:SSL routines:ST_CONNECT:tlsv1 alert internal error`
如果你使用的是 Linux解决方案是使用 [使用宿主机网络](https://docs.docker.com/network/network-tutorial-host/)
```console
docker run \
-e SERVER_NAME="127.0.0.1" \
-v $PWD:/app/public \
--network host \
dunglas/frankenphp
```
Mac 和 Windows 不支持 Docker 使用宿主机网络。在这些平台上,你必须猜测容器的 IP 地址并将其包含在服务器名称中。
运行 `docker network inspect bridge` 并查看 `Containers`,找到 `IPv4Address` 当前分配的最后一个 IP 地址,并增加 1。如果没有容器正在运行则第一个分配的 IP 地址通常为 `172.17.0.2`
然后将其包含在 `SERVER_NAME` 环境变量中:
```console
docker run \
-e SERVER_NAME="127.0.0.1, 172.17.0.3" \
-v $PWD:/app/public \
-p 80:80 -p 443:443 -p 443:443/udp \
dunglas/frankenphp
```
> [!CAUTION]
>
> 请务必将 `172.17.0.3` 替换为将分配给容器的 IP。
你现在应该能够从主机访问 `https://127.0.0.1`
如果不是这种情况,请在调试模式下启动 FrankenPHP 以尝试找出问题:
```console
docker run \
-e CADDY_GLOBAL_OPTIONS="debug" \
-e SERVER_NAME="127.0.0.1" \
-v $PWD:/app/public \
-p 80:80 -p 443:443 -p 443:443/udp \
dunglas/frankenphp
```
## Composer 脚本引用 `@php`
[Composer 脚本](https://getcomposer.org/doc/articles/scripts.md) 可能想要执行一个 PHP 二进制文件来完成一些任务,例如在 [Laravel 项目](laravel.md) 中运行 `@php artisan package:discover --ansi`。这 [目前失败](https://github.com/php/frankenphp/issues/483#issuecomment-1899890915) 的原因有两个:
- Composer 不知道如何调用 FrankenPHP 二进制文件;
- Composer 可以在命令中使用 `-d` 标志添加 PHP 设置,而 FrankenPHP 目前尚不支持。
作为一种变通方法,我们可以在 `/usr/local/bin/php` 中创建一个 Shell 脚本,该脚本会去掉不支持的参数,然后调用 FrankenPHP:
```bash
#!/usr/bin/env bash
args=("$@")
index=0
for i in "$@"
do
if [ "$i" == "-d" ]; then
unset 'args[$index]'
unset 'args[$index+1]'
fi
index=$((index+1))
done
/usr/local/bin/frankenphp php-cli ${args[@]}
```
然后将环境变量 `PHP_BINARY` 设置为我们 `php` 脚本的路径,并运行 Composer
```console
export PHP_BINARY=/usr/local/bin/php
composer install
```
## 使用静态二进制文件排查 TLS/SSL 问题
在使用静态二进制文件时您可能会遇到以下与TLS相关的错误例如在使用STARTTLS发送电子邮件时
```text
Unable to connect with STARTTLS: stream_socket_enable_crypto(): SSL operation failed with code 5. OpenSSL Error messages:
error:80000002:system library::No such file or directory
error:80000002:system library::No such file or directory
error:80000002:system library::No such file or directory
error:0A000086:SSL routines::certificate verify failed
```
由于静态二进制不捆绑 TLS 证书,因此您需要将 OpenSSL 指向本地 CA 证书安装。
检查 [`openssl_get_cert_locations()`](https://www.php.net/manual/en/function.openssl-get-cert-locations.php) 的输出,
以找到 CA 证书必须安装的位置,并将它们存储在该位置。
> [!WARNING]
>
> Web 和命令行界面可能有不同的设置。
> 确保在适当的上下文中运行 `openssl_get_cert_locations()`。
[从Mozilla提取的CA证书可以在curl网站上下载](https://curl.se/docs/caextract.html)。
或者,许多发行版,包括 Debian、Ubuntu 和 Alpine提供名为 `ca-certificates` 的软件包,其中包含这些证书。
还可以使用 `SSL_CERT_FILE``SSL_CERT_DIR` 来提示 OpenSSL 在哪里查找 CA 证书:
```console
# Set TLS certificates environment variables
export SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt
export SSL_CERT_DIR=/etc/ssl/certs
```

185
docs/cn/laravel.md Normal file
View File

@@ -0,0 +1,185 @@
# Laravel
## Docker
使用 FrankenPHP 为 [Laravel](https://laravel.com) Web 应用程序提供服务就像将项目挂载到官方 Docker 镜像的 `/app` 目录中一样简单。
从 Laravel 应用程序的主目录运行以下命令:
```console
docker run -p 80:80 -p 443:443 -p 443:443/udp -v $PWD:/app dunglas/frankenphp
```
尽情享受吧!
## 本地安装
或者,你可以从本地机器上使用 FrankenPHP 运行 Laravel 项目:
1. [下载与你的系统相对应的二进制文件](https://github.com/php/frankenphp/releases)
2. 将以下配置添加到 Laravel 项目根目录中名为 `Caddyfile` 的文件中:
```caddyfile
{
frankenphp
}
# 服务器的域名
localhost {
# 将 webroot 设置为 public/ 目录
root public/
# 启用压缩(可选)
encode zstd br gzip
# 执行当前目录中的 PHP 文件并提供资源
php_server {
try_files {path} index.php
}
}
```
3. 从 Laravel 项目的根目录启动 FrankenPHP`frankenphp run`
## Laravel Octane
Octane 可以通过 Composer 包管理器安装:
```console
composer require laravel/octane
```
安装 Octane 后,你可以执行 `octane:install` Artisan 命令,该命令会将 Octane 的配置文件安装到你的应用程序中:
```console
php artisan octane:install --server=frankenphp
```
Octane 服务可以通过 `octane:frankenphp` Artisan 命令启动。
```console
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`: 在指定日志级别或高于指定日志级别的日志消息
> [!TIP]
> 要获取结构化的 JSON 日志(在使用日志分析解决方案时非常有用),请明确传递 `--log-level` 选项。
你可以了解更多关于 [Laravel Octane 官方文档](https://laravel.com/docs/octane)。
## Laravel 应用程序作为独立的可执行文件
使用[FrankenPHP 的应用嵌入功能](embed.md),可以将 Laravel 应用程序作为
独立的二进制文件分发。
按照以下步骤将您的Laravel应用程序打包为Linux的独立二进制文件
1. 在您的应用程序的存储库中创建一个名为 `static-build.Dockerfile` 的文件:
```dockerfile
FROM --platform=linux/amd64 dunglas/frankenphp:static-builder-gnu
# 如果你打算在 musl-libc 系统上运行该二进制文件,请使用 static-builder-musl
# 复制你的应用
WORKDIR /go/src/app/dist/app
COPY . .
# 删除测试和其他不必要的文件以节省空间
# 或者,将这些文件添加到 .dockerignore 文件中
RUN rm -Rf tests/
# 复制 .env 文件
RUN cp .env.example .env
# 将 APP_ENV 和 APP_DEBUG 更改为适合生产环境
RUN sed -i'' -e 's/^APP_ENV=.*/APP_ENV=production/' -e 's/^APP_DEBUG=.*/APP_DEBUG=false/' .env
# 根据需要对您的 .env 文件进行其他更改
# 安装依赖项
RUN composer install --ignore-platform-reqs --no-dev -a
# 构建静态二进制文件
WORKDIR /go/src/app/
RUN EMBED=dist/app/ ./build-static.sh
```
> [!CAUTION]
>
> 一些 `.dockerignore` 文件
> 将忽略 `vendor/` 目录和 `.env` 文件。在构建之前,请确保调整或删除 `.dockerignore` 文件。
2. 构建:
```console
docker build -t static-laravel-app -f static-build.Dockerfile .
```
3. 提取二进制:
```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. 填充缓存:
```console
frankenphp php-cli artisan optimize
```
5. 运行数据库迁移(如果有的话):
```console
frankenphp php-cli artisan migrate
```
6. 生成应用程序的密钥:
```console
frankenphp php-cli artisan key:generate
```
7. 启动服务器:
```console
frankenphp php-server
```
您的应用程序现在准备好了!
了解有关可用选项的更多信息,以及如何为其他操作系统构建二进制文件,请参见 [应用程序嵌入](embed.md)
文档。
### 更改存储路径
默认情况下Laravel 将上传的文件、缓存、日志等存储在应用程序的 `storage/` 目录中。
这不适合嵌入式应用,因为每个新版本将被提取到不同的临时目录中。
设置 `LARAVEL_STORAGE_PATH` 环境变量(例如,在 `.env` 文件中)或调用 `Illuminate\Foundation\Application::useStoragePath()` 方法以使用临时目录之外的目录。
### 使用独立二进制文件运行 Octane
甚至可以将 Laravel Octane 应用打包为独立的二进制文件!
为此,[正确安装 Octane](#laravel-octane) 并遵循 [前一部分](#laravel-应用程序作为独立的可执行文件) 中描述的步骤。
然后,通过 Octane 在工作模式下启动 FrankenPHP运行
```console
PATH="$PWD:$PATH" frankenphp php-cli artisan octane:frankenphp
```
> [!CAUTION]
>
> 为了使命令有效,独立二进制文件**必须**命名为 `frankenphp`
> 因为 Octane 需要一个名为 `frankenphp` 的程序在路径中可用。

15
docs/cn/mercure.md Normal file
View File

@@ -0,0 +1,15 @@
# 实时
FrankenPHP 配备了内置的 [Mercure](https://mercure.rocks) 中心!
Mercure 允许将事件实时推送到所有连接的设备:它们将立即收到 JavaScript 事件。
无需 JS 库或 SDK
![Mercure](../mercure-hub.png)
要启用 Mercure Hub请按照 [Mercure 网站](https://mercure.rocks/docs/hub/config) 中的说明更新 `Caddyfile`
Mercure hub 的路径是`/.well-known/mercure`.
在 Docker 中运行 FrankenPHP 时,完整的发送 URL 将类似于 `http://php/.well-known/mercure` (其中 `php` 是运行 FrankenPHP 的容器名称)。
要从你的代码中推送 Mercure 更新,我们推荐 [Symfony Mercure Component](https://symfony.com/components/Mercure)(不需要 Symfony 框架来使用)。

17
docs/cn/metrics.md Normal file
View File

@@ -0,0 +1,17 @@
# 指标
当启用 [Caddy 指标](https://caddyserver.com/docs/metrics) 时FrankenPHP 公开以下指标:
- `frankenphp_total_threads`PHP 线程的总数。
- `frankenphp_busy_threads`:当前正在处理请求的 PHP 线程数(运行中的 worker 始终占用一个线程)。
- `frankenphp_queue_depth`:常规排队请求的数量
- `frankenphp_total_workers{worker="[worker_name]"}`worker 的总数。
- `frankenphp_busy_workers{worker="[worker_name]"}`:当前正在处理请求的 worker 数量。
- `frankenphp_worker_request_time{worker="[worker_name]"}`:所有 worker 处理请求所花费的时间。
- `frankenphp_worker_request_count{worker="[worker_name]"}`:所有 worker 处理的请求数量。
- `frankenphp_ready_workers{worker="[worker_name]"}`:至少调用过一次 `frankenphp_handle_request` 的 worker 数量。
- `frankenphp_worker_crashes{worker="[worker_name]"}`worker 意外终止的次数。
- `frankenphp_worker_restarts{worker="[worker_name]"}`worker 被故意重启的次数。
- `frankenphp_worker_queue_depth{worker="[worker_name]"}`:排队请求的数量。
对于 worker 指标,`[worker_name]` 占位符被 Caddyfile 中的 worker 名称替换,否则将使用 worker 文件的绝对路径。

157
docs/cn/performance.md Normal file
View File

@@ -0,0 +1,157 @@
# 性能
默认情况下FrankenPHP 尝试在性能和易用性之间提供良好的折衷。
但是,通过使用适当的配置,可以大幅提高性能。
## 线程和 Worker 数量
默认情况下FrankenPHP 启动的线程和 worker在 worker 模式下)数量是可用 CPU 数量的 2 倍。
适当的值很大程度上取决于你的应用程序是如何编写的、它做什么以及你的硬件。
我们强烈建议更改这些值。为了获得最佳的系统稳定性,建议 `num_threads` x `memory_limit` < `available_memory`
要找到正确的值,最好运行模拟真实流量的负载测试。
[k6](https://k6.io) 和 [Gatling](https://gatling.io) 是很好的工具。
要配置线程数,请使用 `php_server``php` 指令的 `num_threads` 选项。
要更改 worker 数量,请使用 `frankenphp` 指令的 `worker` 部分的 `num` 选项。
### `max_threads`
虽然准确了解你的流量情况总是更好,但现实应用往往更加
不可预测。`max_threads` [配置](config.md#caddyfile-config) 允许 FrankenPHP 在运行时自动生成额外线程,直到指定的限制。
`max_threads` 可以帮助你确定需要多少线程来处理你的流量,并可以使服务器对延迟峰值更具弹性。
如果设置为 `auto`,限制将基于你的 `php.ini` 中的 `memory_limit` 进行估算。如果无法这样做,
`auto` 将默认为 2x `num_threads`。请记住,`auto` 可能会严重低估所需的线程数。
`max_threads` 类似于 PHP FPM 的 [pm.max_children](https://www.php.net/manual/en/install.fpm.configuration.php#pm.max-children)。主要区别是 FrankenPHP 使用线程而不是
进程,并根据需要自动在不同的 worker 脚本和"经典模式"之间委派它们。
## Worker 模式
启用 [worker 模式](worker.md) 大大提高了性能,
但你的应用必须适配以兼容此模式:
你需要创建一个 worker 脚本并确保应用不会泄漏内存。
## 不要使用 musl
官方 Docker 镜像的 Alpine Linux 变体和我们提供的默认二进制文件使用 [musl libc](https://musl.libc.org)。
众所周知,当使用这个替代 C 库而不是传统的 GNU 库时PHP [更慢](https://gitlab.alpinelinux.org/alpine/aports/-/issues/14381)
特别是在以 ZTS 模式(线程安全)编译时,这是 FrankenPHP 所必需的。在大量线程环境中,差异可能很显著。
另外,[一些错误只在使用 musl 时发生](https://github.com/php/php-src/issues?q=sort%3Aupdated-desc+is%3Aissue+is%3Aopen+label%3ABug+musl)。
在生产环境中,我们建议使用链接到 glibc 的 FrankenPHP。
这可以通过使用 Debian Docker 镜像(默认)、从我们的 [Releases](https://github.com/php/frankenphp/releases) 下载 -gnu 后缀二进制文件,或通过[从源代码编译 FrankenPHP](compile.md) 来实现。
或者,我们提供使用 [mimalloc 分配器](https://github.com/microsoft/mimalloc) 编译的静态 musl 二进制文件,这缓解了线程场景中的问题。
## Go 运行时配置
FrankenPHP 是用 Go 编写的。
一般来说Go 运行时不需要任何特殊配置,但在某些情况下,
特定的配置可以提高性能。
你可能想要将 `GODEBUG` 环境变量设置为 `cgocheck=0`FrankenPHP Docker 镜像中的默认值)。
如果你在容器Docker、Kubernetes、LXC...)中运行 FrankenPHP 并限制容器的可用内存,
请将 `GOMEMLIMIT` 环境变量设置为可用内存量。
有关更多详细信息,[专门针对此主题的 Go 文档页面](https://pkg.go.dev/runtime#hdr-Environment_Variables) 是充分利用运行时的必读内容。
## `file_server`
默认情况下,`php_server` 指令自动设置文件服务器来
提供存储在根目录中的静态文件(资产)。
此功能很方便,但有成本。
要禁用它,请使用以下配置:
```caddyfile
php_server {
file_server off
}
```
## `try_files`
除了静态文件和 PHP 文件外,`php_server` 还会尝试提供你应用程序的索引
和目录索引文件(`/path/` -> `/path/index.php`)。如果你不需要目录索引,
你可以通过明确定义 `try_files` 来禁用它们,如下所示:
```caddyfile
php_server {
try_files {path} index.php
root /root/to/your/app # 在这里明确添加根目录允许更好的缓存
}
```
这可以显著减少不必要的文件操作数量。
另一种具有 0 个不必要文件系统操作的方法是改用 `php` 指令并按路径将
文件与 PHP 分开。如果你的整个应用程序由一个入口文件提供服务,这种方法效果很好。
一个在 `/assets` 文件夹后面提供静态文件的示例[配置](config.md#caddyfile-config)可能如下所示:
```caddyfile
route {
@assets {
path /assets/*
}
# /assets 后面的所有内容都由文件服务器处理
file_server @assets {
root /root/to/your/app
}
# 不在 /assets 中的所有内容都由你的索引或 worker PHP 文件处理
rewrite index.php
php {
root /root/to/your/app # 在这里明确添加根目录允许更好的缓存
}
}
```
## 占位符
你可以在 `root``env` 指令中使用[占位符](https://caddyserver.com/docs/conventions#placeholders)。
但是,这会阻止缓存这些值,并带来显著的性能成本。
如果可能,请避免在这些指令中使用占位符。
## `resolve_root_symlink`
默认情况下如果文档根目录是符号链接FrankenPHP 会自动解析它(这对于 PHP 正常工作是必要的)。
如果文档根目录不是符号链接,你可以禁用此功能。
```caddyfile
php_server {
resolve_root_symlink false
}
```
如果 `root` 指令包含[占位符](https://caddyserver.com/docs/conventions#placeholders),这将提高性能。
在其他情况下,收益将可以忽略不计。
## 日志
日志显然非常有用,但根据定义,
它需要 I/O 操作和内存分配,这会大大降低性能。
确保你[正确设置日志级别](https://caddyserver.com/docs/caddyfile/options#log)
并且只记录必要的内容。
## PHP 性能
FrankenPHP 使用官方 PHP 解释器。
所有常见的 PHP 相关性能优化都适用于 FrankenPHP。
特别是:
- 检查 [OPcache](https://www.php.net/manual/zh/book.opcache.php) 是否已安装、启用并正确配置
- 启用 [Composer 自动加载器优化](https://getcomposer.org/doc/articles/autoloader-optimization.md)
- 确保 `realpath` 缓存对于你的应用程序需求足够大
- 使用[预加载](https://www.php.net/manual/zh/opcache.preloading.php)
有关更多详细信息,请阅读[专门的 Symfony 文档条目](https://symfony.com/doc/current/performance.html)
(即使你不使用 Symfony大多数提示也很有用

144
docs/cn/production.md Normal file
View File

@@ -0,0 +1,144 @@
# 在生产环境中部署
在本教程中,我们将学习如何使用 Docker Compose 在单个服务器上部署 PHP 应用程序。
如果你使用的是 Symfony请阅读 Symfony Docker 项目(使用 FrankenPHP的 [在生产环境中部署](https://github.com/dunglas/symfony-docker/blob/main/docs/production.md) 文档条目。
如果你使用的是 API Platform同样使用 FrankenPHP请参阅 [框架的部署文档](https://api-platform.com/docs/deployment/)。
## 准备应用
首先,在 PHP 项目的根目录中创建一个 `Dockerfile`
```dockerfile
FROM dunglas/frankenphp
# 请将 "your-domain-name.example.com" 替换为你的域名
ENV SERVER_NAME=your-domain-name.example.com
# 如果要禁用 HTTPS请改用以下值
#ENV SERVER_NAME=:80
# 如果你的项目不使用 "public" 目录作为 web 根目录,你可以在这里设置:
# ENV SERVER_ROOT=web/
# 启用 PHP 生产配置
RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
# 将项目的 PHP 文件复制到 public 目录中
COPY . /app/public
# 如果你使用 Symfony 或 Laravel你需要复制整个项目
#COPY . /app
```
有关更多详细信息和选项,请参阅 [构建自定义 Docker 镜像](docker.md)。
要了解如何自定义配置,请安装 PHP 扩展和 Caddy 模块。
如果你的项目使用 Composer
请务必将其包含在 Docker 镜像中并安装你的依赖。
然后,添加一个 `compose.yaml` 文件:
```yaml
services:
php:
image: dunglas/frankenphp
restart: always
ports:
- "80:80" # HTTP
- "443:443" # HTTPS
- "443:443/udp" # HTTP/3
volumes:
- caddy_data:/data
- caddy_config:/config
# Caddy 证书和配置所需的挂载目录
volumes:
caddy_data:
caddy_config:
```
> [!NOTE]
>
> 前面的示例适用于生产用途。
> 在开发中,你可能希望使用挂载目录,不同的 PHP 配置和不同的 `SERVER_NAME` 环境变量值。
>
> 见 [Symfony Docker](https://github.com/dunglas/symfony-docker) 项目
> (使用 FrankenPHP作为使用多阶段镜像的更高级示例
> Composer、额外的 PHP 扩展等。
最后,如果你使用 Git请提交这些文件并推送。
## 准备服务器
若要在生产环境中部署应用程序,需要一台服务器。
在本教程中,我们将使用 DigitalOcean 提供的虚拟机,但任何 Linux 服务器都可以工作。
如果你已经有安装了 Docker 的 Linux 服务器,你可以直接跳到 [下一节](#配置域名)。
否则,请使用 [此会员链接](https://m.do.co/c/5d8aabe3ab80) 获得 200 美元的免费信用额度创建一个帐户然后单击“Create a Droplet”。
然后单击“Choose an image”部分下的“Marketplace”选项卡然后搜索名为“Docker”的应用程序。
这将配置已安装最新版本的 Docker 和 Docker Compose 的 Ubuntu 服务器!
出于测试目的,最便宜的就足够了。
对于实际的生产用途你可能需要在“general purpose”部分中选择一个计划来满足你的需求。
![使用 Docker 在 DigitalOcean 上部署 FrankenPHP](../digitalocean-droplet.png)
你可以保留其他设置的默认值,也可以根据需要进行调整。
不要忘记添加你的 SSH 密钥或创建密码,然后点击“完成并创建”按钮。
然后,在 Droplet 预配时等待几秒钟。
Droplet 准备就绪后,使用 SSH 进行连接:
```console
ssh root@<droplet-ip>
```
## 配置域名
在大多数情况下,你需要将域名与你的网站相关联。
如果你还没有域名,则必须通过注册商购买。
然后为你的域名创建类型为 `A` 的 DNS 记录,指向服务器的 IP 地址:
```dns
your-domain-name.example.com. IN A 207.154.233.113
```
DigitalOcean 域服务示例“Networking” > “Domains”
![在 DigitalOcean 上配置 DNS](../digitalocean-dns.png)
> [!NOTE]
>
> Let's Encrypt 是 FrankenPHP 默认用于自动生成 TLS 证书的服务,不支持使用裸 IP 地址。使用域名是使用 Let's Encrypt 的必要条件。
## 部署
使用 `git clone``scp` 或任何其他可能适合你需要的工具在服务器上复制你的项目。
如果使用 GitHub则可能需要使用 [部署密钥](https://docs.github.com/en/free-pro-team@latest/developers/overview/managing-deploy-keys#deploy-keys)。
部署密钥也 [由 GitLab 支持](https://docs.gitlab.com/ee/user/project/deploy_keys/)。
Git 示例:
```console
git clone git@github.com:<username>/<project-name>.git
```
进入包含项目 (`<project-name>`) 的目录,并在生产模式下启动应用:
```console
docker compose up --wait
```
你的服务器已启动并运行,并且已自动为你生成 HTTPS 证书。
`https://your-domain-name.example.com` 享受吧!
> [!CAUTION]
>
> Docker 有一个缓存层,请确保每个部署都有正确的构建,或者使用 `--no-cache` 选项重新构建项目以避免缓存问题。
## 在多个节点上部署
如果要在计算机集群上部署应用程序,可以使用 [Docker Swarm](https://docs.docker.com/engine/swarm/stack-deploy/)
它与提供的 Compose 文件兼容。
要在 Kubernetes 上部署,请查看 [API 平台提供的 Helm 图表](https://api-platform.com/docs/deployment/kubernetes/),同样也使用 FrankenPHP。

161
docs/cn/static.md Normal file
View File

@@ -0,0 +1,161 @@
# 创建静态构建
与其使用本地安装的PHP库
由于伟大的 [static-php-cli 项目](https://github.com/crazywhalecc/static-php-cli),创建一个静态或基本静态的 FrankenPHP 构建是可能的(尽管它的名字,这个项目支持所有的 SAPI而不仅仅是 CLI
使用这种方法,我们可构建一个包含 PHP 解释器、Caddy Web 服务器和 FrankenPHP 的可移植二进制文件!
完全静态的本地可执行文件不需要任何依赖,并且可以在 [`scratch` Docker 镜像](https://docs.docker.com/build/building/base-images/#create-a-minimal-base-image-using-scratch) 上运行。
然而,它们无法加载动态 PHP 扩展(例如 Xdebug并且由于使用了 musl libc有一些限制。
大多数静态二进制文件只需要 `glibc` 并且可以加载动态扩展。
在可能的情况下我们建议使用基于glibc的、主要是静态构建的版本。
FrankenPHP 还支持 [将 PHP 应用程序嵌入到静态二进制文件中](embed.md)。
## Linux
我们提供了一个 Docker 镜像来构建 Linux 静态二进制文件:
### 基于musl的完全静态构建
对于一个在任何Linux发行版上运行且不需要依赖项的完全静态二进制文件但不支持动态加载扩展
```console
docker buildx bake --load static-builder-musl
docker cp $(docker create --name static-builder-musl dunglas/frankenphp:static-builder-musl):/go/src/app/dist/frankenphp-linux-$(uname -m) frankenphp ; docker rm static-builder-musl
```
为了在高度并发的场景中获得更好的性能,请考虑使用 [mimalloc](https://github.com/microsoft/mimalloc) 分配器。
```console
docker buildx bake --load --set static-builder-musl.args.MIMALLOC=1 static-builder-musl
```
### 基于glibc的主要静态构建支持动态扩展
对于一个支持动态加载 PHP 扩展的二进制文件,同时又将所选扩展静态编译:
```console
docker buildx bake --load static-builder-gnu
docker cp $(docker create --name static-builder-gnu dunglas/frankenphp:static-builder-gnu):/go/src/app/dist/frankenphp-linux-$(uname -m) frankenphp ; docker rm static-builder-gnu
```
该二进制文件支持所有glibc版本2.17及以上但不支持基于musl的系统如Alpine Linux
生成的主要是静态的(除了 `glibc`)二进制文件名为 `frankenphp`,并且可以在当前目录中找到。
如果你想在没有 Docker 的情况下构建静态二进制文件,请查看 macOS 说明,它也适用于 Linux。
### 自定义扩展
默认情况下,大多数流行的 PHP 扩展都会被编译。
为了减少二进制文件的大小和减少攻击面,您可以选择使用 `PHP_EXTENSIONS` Docker ARG 构建的扩展列表。
例如,运行以下命令仅构建 `opcache` 扩展:
```console
docker buildx bake --load --set static-builder-musl.args.PHP_EXTENSIONS=opcache,pdo_sqlite static-builder-musl
# ...
```
若要将启用其他功能的库添加到已启用的扩展中,可以使用 `PHP_EXTENSION_LIBS` Docker 参数:
```console
docker buildx bake \
--load \
--set static-builder-musl.args.PHP_EXTENSIONS=gd \
--set static-builder-musl.args.PHP_EXTENSION_LIBS=libjpeg,libwebp \
static-builder-musl
```
### 额外的 Caddy 模块
要向 [xcaddy](https://github.com/caddyserver/xcaddy) 添加额外的 Caddy 模块或传递其他参数,请使用 `XCADDY_ARGS` Docker 参数:
```console
docker buildx bake \
--load \
--set static-builder-musl.args.XCADDY_ARGS="--with github.com/darkweak/souin/plugins/caddy --with github.com/dunglas/caddy-cbrotli --with github.com/dunglas/mercure/caddy --with github.com/dunglas/vulcain/caddy" \
static-builder-musl
```
在本例中,我们为 Caddy 添加了 [Souin](https://souin.io) HTTP 缓存模块,以及 [cbrotli](https://github.com/dunglas/caddy-cbrotli)、[Mercure](https://mercure.rocks) 和 [Vulcain](https://vulcain.rocks) 模块。
> [!TIP]
>
> 如果 `XCADDY_ARGS` 为空或未设置,则默认包含 cbrotli、Mercure 和 Vulcain 模块。
> 如果自定义了 `XCADDY_ARGS` 的值,则必须显式地包含它们。
参见:[自定义构建](#自定义构建)
### GitHub Token
如果遇到了 GitHub API 速率限制,请在 `GITHUB_TOKEN` 的环境变量中设置 GitHub Personal Access Token
```console
GITHUB_TOKEN="xxx" docker --load buildx bake static-builder-musl
# ...
```
## macOS
运行以下脚本以创建适用于 macOS 的静态二进制文件(需要先安装 [Homebrew](https://brew.sh/)
```console
git clone https://github.com/php/frankenphp
cd frankenphp
./build-static.sh
```
注意:此脚本也适用于 Linux可能也适用于其他 Unix 系统),我们提供的用于构建静态二进制的 Docker 镜像也在内部使用这个脚本。
## 自定义构建
以下环境变量可以传递给 `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 上
## 扩展
使用glibc或基于macOS的二进制文件您可以动态加载PHP扩展。然而这些扩展必须使用ZTS支持进行编译。
由于大多数软件包管理器目前不提供其扩展的 ZTS 版本,因此您必须自己编译它们。
为此,您可以构建并运行 `static-builder-gnu` Docker 容器,远程进入它,并使用 `./configure --with-php-config=/go/src/app/dist/static-php-cli/buildroot/bin/php-config` 编译扩展。
关于 [Xdebug 扩展](https://xdebug.org) 的示例步骤:
```console
docker build -t gnu-ext -f static-builder-gnu.Dockerfile --build-arg FRANKENPHP_VERSION=1.0 .
docker create --name static-builder-gnu -it gnu-ext /bin/sh
docker start static-builder-gnu
docker exec -it static-builder-gnu /bin/sh
cd /go/src/app/dist/static-php-cli/buildroot/bin
git clone https://github.com/xdebug/xdebug.git && cd xdebug
source scl_source enable devtoolset-10
../phpize
./configure --with-php-config=/go/src/app/dist/static-php-cli/buildroot/bin/php-config
make
exit
docker cp static-builder-gnu:/go/src/app/dist/static-php-cli/buildroot/bin/xdebug/modules/xdebug.so xdebug-zts.so
docker cp static-builder-gnu:/go/src/app/dist/frankenphp-linux-$(uname -m) ./frankenphp
docker stop static-builder-gnu
docker rm static-builder-gnu
docker rmi gnu-ext
```
这将在当前目录中创建 `frankenphp``xdebug-zts.so`
如果你将 `xdebug-zts.so` 移动到你的扩展目录中,添加 `zend_extension=xdebug-zts.so` 到你的 php.ini 并运行 FrankenPHP它将加载 Xdebug。

179
docs/cn/worker.md Normal file
View File

@@ -0,0 +1,179 @@
# 使用 FrankenPHP Workers
启动一次应用程序并将其保存在内存中。
FrankenPHP 将在几毫秒内处理传入请求。
## 启动 Worker 脚本
### Docker
`FRANKENPHP_CONFIG` 环境变量的值设置为 `worker /path/to/your/worker/script.php`
```console
docker run \
-e FRANKENPHP_CONFIG="worker /app/path/to/your/worker/script.php" \
-v $PWD:/app \
-p 80:80 -p 443:443 -p 443:443/udp \
dunglas/frankenphp
```
### 独立二进制文件
使用 `php-server` 命令的 `--worker` 选项通过 worker 为当前目录的内容提供服务:
```console
frankenphp php-server --worker /path/to/your/worker/script.php
```
如果你的 PHP 应用程序已[嵌入到二进制文件中](embed.md),你可以在应用程序的根目录中添加自定义的 `Caddyfile`
它将被自动使用。
还可以使用 `--watch` 选项在[文件更改时重启 worker](config.md#watching-for-file-changes)。
如果 `/path/to/your/app/` 目录或子目录中任何以 `.php` 结尾的文件被修改,以下命令将触发重启:
```console
frankenphp php-server --worker /path/to/your/worker/script.php --watch="/path/to/your/app/**/*.php"
```
## Symfony Runtime
FrankenPHP 的 worker 模式由 [Symfony Runtime Component](https://symfony.com/doc/current/components/runtime.html) 支持。
要在 worker 中启动任何 Symfony 应用程序,请安装 [PHP Runtime](https://github.com/php-runtime/runtime) 的 FrankenPHP 包:
```console
composer require runtime/frankenphp-symfony
```
通过定义 `APP_RUNTIME` 环境变量来使用 FrankenPHP Symfony Runtime 启动你的应用服务器:
```console
docker run \
-e FRANKENPHP_CONFIG="worker ./public/index.php" \
-e APP_RUNTIME=Runtime\\FrankenPhpSymfony\\Runtime \
-v $PWD:/app \
-p 80:80 -p 443:443 -p 443:443/udp \
dunglas/frankenphp
```
## Laravel Octane
请参阅[专门的文档](laravel.md#laravel-octane)。
## 自定义应用程序
以下示例展示了如何创建自己的 worker 脚本而不依赖第三方库:
```php
<?php
// public/index.php
// 防止客户端连接中断时 worker 脚本终止
ignore_user_abort(true);
// 启动你的应用程序
require __DIR__.'/vendor/autoload.php';
$myApp = new \App\Kernel();
$myApp->boot();
// 在循环外的处理器以获得更好的性能(减少工作量)
$handler = static function () use ($myApp) {
// 当收到请求时调用,
// 超全局变量、php://input 等都会被重置
echo $myApp->handle($_GET, $_POST, $_COOKIE, $_FILES, $_SERVER);
};
$maxRequests = (int)($_SERVER['MAX_REQUESTS'] ?? 0);
for ($nbRequests = 0; !$maxRequests || $nbRequests < $maxRequests; ++$nbRequests) {
$keepRunning = \frankenphp_handle_request($handler);
// 在发送 HTTP 响应后做一些事情
$myApp->terminate();
// 调用垃圾收集器以减少在页面生成过程中触发垃圾收集的可能性
gc_collect_cycles();
if (!$keepRunning) break;
}
// 清理
$myApp->shutdown();
```
然后,启动你的应用程序并使用 `FRANKENPHP_CONFIG` 环境变量配置你的 worker
```console
docker run \
-e FRANKENPHP_CONFIG="worker ./public/index.php" \
-v $PWD:/app \
-p 80:80 -p 443:443 -p 443:443/udp \
dunglas/frankenphp
```
默认情况下,每个 CPU 启动 2 个 worker。
你也可以配置要启动的 worker 数量:
```console
docker run \
-e FRANKENPHP_CONFIG="worker ./public/index.php 42" \
-v $PWD:/app \
-p 80:80 -p 443:443 -p 443:443/udp \
dunglas/frankenphp
```
### 在处理一定数量的请求后重启 Worker
由于 PHP 最初不是为长时间运行的进程而设计的,仍有许多库和传统代码会泄漏内存。
在 worker 模式下使用此类代码的一个解决方法是在处理一定数量的请求后重启 worker 脚本:
前面的 worker 代码片段允许通过设置名为 `MAX_REQUESTS` 的环境变量来配置要处理的最大请求数。
### 手动重启 Workers
虽然可以在[文件更改时重启 workers](config.md#watching-for-file-changes),但也可以通过 [Caddy admin API](https://caddyserver.com/docs/api) 优雅地重启所有 workers。如果在你的 [Caddyfile](config.md#caddyfile-config) 中启用了 admin你可以通过简单的 POST 请求 ping 重启端点,如下所示:
```console
curl -X POST http://localhost:2019/frankenphp/workers/restart
```
### Worker 故障
如果 worker 脚本因非零退出代码而崩溃FrankenPHP 将使用指数退避策略重启它。
如果 worker 脚本保持运行的时间超过上次退避 × 2
它将不会惩罚 worker 脚本并再次重启它。
但是,如果 worker 脚本在短时间内继续以非零退出代码失败
例如脚本中有拼写错误FrankenPHP 将崩溃并出现错误:`too many consecutive failures`
可以在你的 [Caddyfile](config.md#caddyfile-配置) 中使用 `max_consecutive_failures` 选项配置连续失败的次数:
```caddyfile
frankenphp {
worker {
# ...
max_consecutive_failures 10
}
}
```
## 超全局变量行为
[PHP 超全局变量](https://www.php.net/manual/zh/language.variables.superglobals.php)`$_SERVER``$_ENV``$_GET`...
行为如下:
- 在第一次调用 `frankenphp_handle_request()` 之前,超全局变量包含绑定到 worker 脚本本身的值
- 在调用 `frankenphp_handle_request()` 期间和之后,超全局变量包含从处理的 HTTP 请求生成的值,每次调用 `frankenphp_handle_request()` 都会更改超全局变量的值
要在回调内访问 worker 脚本的超全局变量,必须复制它们并将副本导入到回调的作用域中:
```php
<?php
// 在第一次调用 frankenphp_handle_request() 之前复制 worker 的 $_SERVER 超全局变量
$workerServer = $_SERVER;
$handler = static function () use ($workerServer) {
var_dump($_SERVER); // 与请求绑定的 $_SERVER
var_dump($workerServer); // worker 脚本的 $_SERVER
};
// ...
```

69
docs/cn/x-sendfile.md Normal file
View File

@@ -0,0 +1,69 @@
# 高效服务大型静态文件 (`X-Sendfile`/`X-Accel-Redirect`)
通常,静态文件可以直接由 Web 服务器提供服务,
但有时在发送它们之前需要执行一些 PHP 代码:
访问控制、统计、自定义 HTTP 头...
不幸的是,与直接使用 Web 服务器相比,使用 PHP 服务大型静态文件效率低下
(内存过载、性能降低...)。
FrankenPHP 让你在执行自定义 PHP 代码**之后**将静态文件的发送委托给 Web 服务器。
为此,你的 PHP 应用程序只需定义一个包含要服务的文件路径的自定义 HTTP 头。FrankenPHP 处理其余部分。
此功能在 Apache 中称为 **`X-Sendfile`**,在 NGINX 中称为 **`X-Accel-Redirect`**。
在以下示例中,我们假设项目的文档根目录是 `public/` 目录,
并且我们想要使用 PHP 来服务存储在 `public/` 目录外的文件,
来自名为 `private-files/` 的目录。
## 配置
首先,将以下配置添加到你的 `Caddyfile` 以启用此功能:
```patch
root public/
# ...
+ # Symfony、Laravel 和其他使用 Symfony HttpFoundation 组件的项目需要
+ request_header X-Sendfile-Type x-accel-redirect
+ request_header X-Accel-Mapping ../private-files=/private-files
+
+ intercept {
+ @accel header X-Accel-Redirect *
+ handle_response @accel {
+ root private-files/
+ rewrite * {resp.header.X-Accel-Redirect}
+ method * GET
+
+ # 删除 PHP 设置的 X-Accel-Redirect 头以提高安全性
+ header -X-Accel-Redirect
+
+ file_server
+ }
+ }
php_server
```
## 纯 PHP
将相对文件路径(从 `private-files/`)设置为 `X-Accel-Redirect` 头的值:
```php
header('X-Accel-Redirect: file.txt');
```
## 使用 Symfony HttpFoundation 组件的项目Symfony、Laravel、Drupal...
Symfony HttpFoundation [原生支持此功能](https://symfony.com/doc/current/components/http_foundation.html#serving-files)。
它将自动确定 `X-Accel-Redirect` 头的正确值并将其添加到响应中。
```php
use Symfony\Component\HttpFoundation\BinaryFileResponse;
BinaryFileResponse::trustXSendfileTypeHeader();
$response = new BinaryFileResponse(__DIR__.'/../private-files/file.txt');
// ...
```

View File

@@ -1,24 +1,42 @@
# Compile From Sources
This document explain how to create a FrankenPHP build that will load PHP as a dymanic library.
This document explains how to create a FrankenPHP binary that will load PHP as a dynamic library.
This is the recommended method.
Alternatively, [creating static builds](static.md) is also possible.
Alternatively, [fully and mostly static builds](static.md) can also be created.
## Install PHP
FrankenPHP is compatible with the PHP 8.2 and superior.
FrankenPHP is compatible with PHP 8.2 and superior.
First, [get the sources of PHP](https://www.php.net/downloads.php) and extract them:
### With Homebrew (Linux and Mac)
The easiest way to install a version of libphp compatible with FrankenPHP is to use the ZTS packages provided by [Homebrew PHP](https://github.com/shivammathur/homebrew-php).
First, if not already done, install [Homebrew](https://brew.sh).
Then, install the ZTS variant of PHP, Brotli (optional, for compression support) and watcher (optional, for file change detection):
```console
brew install shivammathur/php/php-zts brotli watcher
brew link --overwrite --force shivammathur/php/php-zts
```
### By Compiling PHP
Alternatively, you can compile PHP from sources with the options needed by FrankenPHP by following these steps.
First, [get the PHP sources](https://www.php.net/downloads.php) and extract them:
```console
tar xf php-*
cd php-*/
```
Then, configure PHP for your platform:
Then, run the `configure` script with the options needed for your platform.
The following `./configure` flags are mandatory, but you can add others, for example, to compile extensions or additional features.
### Linux
#### Linux
```console
./configure \
@@ -28,20 +46,12 @@ Then, configure PHP for your platform:
--enable-zend-max-execution-timers
```
Finally, compile and install PHP:
#### Mac
Use the [Homebrew](https://brew.sh/) package manager to install the required and optional dependencies:
```console
make -j$(nproc)
sudo make install
```
### Mac
Use the [Homebrew](https://brew.sh/) package manager to install
`libiconv`, `bison`, `re2c` and `pkg-config`:
```console
brew install libiconv bison re2c pkg-config
brew install libiconv bison brotli re2c pkg-config watcher
echo 'export PATH="/opt/homebrew/opt/bison/bin:$PATH"' >> ~/.zshrc
```
@@ -49,48 +59,57 @@ Then run the configure script:
```console
./configure \
--enable-embed=static \
--enable-embed \
--enable-zts \
--disable-zend-signals \
--disable-opcache-jit \
--enable-static \
--enable-shared=no \
--with-iconv=/opt/homebrew/opt/libiconv/
```
These flags are required, but you can add other flags (e.g. extra extensions)
if needed.
#### Compile PHP
Finally, compile and install PHP:
```console
make -j$(sysctl -n hw.logicalcpu)
make -j"$(getconf _NPROCESSORS_ONLN)"
sudo make install
```
## Install Optional Dependencies
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 |
| [Mercure](mercure.md) | [Mercure Go library](https://pkg.go.dev/github.com/dunglas/mercure) (automatically installed, AGPL licensed) | nomercure |
## Compile the Go App
You can now use the Go library and compile our Caddy build:
```console
curl -L https://github.com/dunglas/frankenphp/archive/refs/heads/main.tar.gz | tar x
cd frankenphp-main/caddy/frankenphp
CGO_CFLAGS=$(php-config --includes) CGO_LDFLAGS="$(php-config --ldflags) $(php-config --libs)" go build
```
You can now build the final binary.
### Using xcaddy
Alternatively, use [xcaddy](https://github.com/caddyserver/xcaddy) to compile FrankenPHP with [custom Caddy modules](https://caddyserver.com/docs/modules/):
The recommended way is to use [xcaddy](https://github.com/caddyserver/xcaddy) to compile FrankenPHP.
`xcaddy` also allows to easily add [custom Caddy modules](https://caddyserver.com/docs/modules/) and FrankenPHP extensions:
```console
CGO_ENABLED=1 \
XCADDY_GO_BUILD_FLAGS="-ldflags '-w -s'" \
XCADDY_GO_BUILD_FLAGS="-ldflags='-w -s' -tags=nobadger,nomysql,nopgx" \
CGO_CFLAGS=$(php-config --includes) \
CGO_LDFLAGS="$(php-config --ldflags) $(php-config --libs)" \
xcaddy build \
--output frankenphp \
--with github.com/dunglas/frankenphp/caddy \
--with github.com/dunglas/mercure/caddy \
--with github.com/dunglas/vulcain/caddy
# Add extra Caddy modules here
--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]
@@ -101,4 +120,14 @@ xcaddy build \
>
> To do so, change the `XCADDY_GO_BUILD_FLAGS` environment variable to something like
> `XCADDY_GO_BUILD_FLAGS=$'-ldflags "-w -s -extldflags \'-Wl,-z,stack-size=0x80000\'"'`
> (change the value of the stack size according to your app needs).
> (change the stack size value according to your app needs).
### Without xcaddy
Alternatively, it's possible to compile FrankenPHP without `xcaddy` by using the `go` command directly:
```console
curl -L https://github.com/php/frankenphp/archive/refs/heads/main.tar.gz | tar xz
cd frankenphp-main/caddy/frankenphp
CGO_CFLAGS=$(php-config --includes) CGO_LDFLAGS="$(php-config --ldflags) $(php-config --libs)" go build -tags=nobadger,nomysql,nopgx
```

View File

@@ -1,54 +1,109 @@
# 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 image, the `Caddyfile` is located at `/etc/caddy/Caddyfile`.
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.
You can also configure PHP using `php.ini` as usual.
A minimal `Caddyfile` to serve a PHP application is shown below:
In the Docker image, the `php.ini` file is not present, you can create it or `COPY` manually.
```caddyfile
# The hostname to respond to
localhost
If you copy `php.ini` from `$PHP_INI_DIR/php.ini-production` or `$PHP_INI_DIR/php.ini-development`, you also must set the variable `variables_order = "EGPCS"`, because the default value for `variables_order` is `"EGPCS"`, but in `php.ini-production` and `php.ini-development` we have `"GPCS"`. And in this case, `worker` does not work properly.
# 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, 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>/`
- You should copy an official template provided by the PHP project:
```dockerfile
FROM dunglas/frankenphp
RUN cp $PHP_INI_DIR/php.ini-production $PHP_INI_DIR/php.ini; \
sed -i 's/variables_order = "GPCS"/variables_order = "EGPCS"/' $PHP_INI_DIR/php.ini;
# Production:
RUN cp $PHP_INI_DIR/php.ini-production $PHP_INI_DIR/php.ini
# Or development:
RUN cp $PHP_INI_DIR/php.ini-development $PHP_INI_DIR/php.ini
```
## RPM and Debian packages
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
- copy one of `php.ini-production` or `php.ini-development` provided [in the PHP sources](https://github.com/php/php-src/).
## Caddyfile Config
To register the FrankenPHP executor, the `frankenphp` [global option](https://caddyserver.com/docs/caddyfile/concepts#global-options) must be set, then the `php_server` or the `php` [HTTP directives](https://caddyserver.com/docs/caddyfile/concepts#directives) may be used within the site blocks to serve your PHP app.
The `php_server` or the `php` [HTTP directives](https://caddyserver.com/docs/caddyfile/concepts#directives) may be used within the site blocks to serve your PHP app.
Minimal example:
```caddyfile
{
# Enable FrankenPHP
frankenphp
# Configure when the directive must be executed
order php_server before file_server
}
localhost {
# Enable compression (optional)
encode zstd gzip
encode zstd br gzip
# Execute PHP files in the current directory and serve assets
php_server
}
```
Optionally, the number of threads to create and [worker scripts](worker.md) to start with the server can be specified under the global option.
You can also explicitly configure FrankenPHP using the [global option](https://caddyserver.com/docs/caddyfile/concepts#global-options) `frankenphp`:
```caddyfile
{
frankenphp {
num_threads <num_threads> # Sets the number of PHP threads to start. Default: 2x the number of available CPUs.
max_threads <num_threads> # Limits the number of additional PHP threads that can be started at runtime. Default: num_threads. Can be set to 'auto'.
max_wait_time <duration> # Sets the maximum time a request may wait for a free PHP thread before timing out. Default: disabled.
php_ini <key> <value> # Set a php.ini directive. Can be used several times to set multiple directives.
worker {
file <path> # Sets the path to the worker script.
num <num> # Sets the number of PHP threads to start, defaults to 2x the number of available CPUs.
env <key> <value> # Sets an extra environment variable to the given value. Can be specified more than once for multiple environment variables.
watch <path> # Sets the path to watch for file changes. Can be specified more than once for multiple paths.
name <name> # Sets the name of the worker, used in logs and metrics. Default: absolute path of worker file
max_consecutive_failures <num> # Sets the maximum number of consecutive failures before the worker is considered unhealthy, -1 means the worker will always restart. Default: 6.
}
}
}
@@ -71,27 +126,29 @@ Alternatively, you may use the one-line short form of the `worker` option:
You can also define multiple workers if you serve multiple apps on the same server:
```caddyfile
{
frankenphp {
worker /path/to/app/public/index.php <num>
worker /path/to/other/public/index.php <num>
app.example.com {
root /path/to/app/public
php_server {
root /path/to/app/public # allows for better caching
worker index.php <num>
}
}
app.example.com {
root * /path/to/app/public
php_server
other.example.com {
root /path/to/other/public
php_server {
root /path/to/other/public
worker index.php <num>
}
}
other.example.com {
root * /path/to/other/public
php_server
}
...
# ...
```
Using the `php_server` directive is generaly what you need,
but if you need full control, you can use the lower level `php` directive:
Using the `php_server` directive is generally what you need,
but if you need full control, you can use the lower-level `php` directive.
The `php` directive passes all input to PHP, instead of first checking whether
it's a PHP file or not. Read more about it in the [performance page](performance.md#try_files).
Using the `php_server` directive is equivalent to this configuration:
@@ -121,9 +178,87 @@ The `php_server` and the `php` directives have the following options:
```caddyfile
php_server [<matcher>] {
root <directory> # Sets the root folder to the site. Default: `root` directive.
split_path <delim...> # Sets the substrings for splitting the URI into two parts. The first matching substring will be used to split the "path info" from the path. The first piece is suffixed with the matching substring and will be assumed as the actual resource (CGI script) name. The second piece will be set to PATH_INFO for the CGI script to use. Default: `.php`
resolve_root_symlink # Enables resolving the `root` directory to its actual value by evaluating a symbolic link, if one exists.
split_path <delim...> # Sets the substrings for splitting the URI into two parts. The first matching substring will be used to split the "path info" from the path. The first piece is suffixed with the matching substring and will be assumed as the actual resource (CGI script) name. The second piece will be set to PATH_INFO for the script to use. Default: `.php`
resolve_root_symlink false # Disables resolving the `root` directory to its actual value by evaluating a symbolic link, if one exists (enabled by default).
env <key> <value> # Sets an extra environment variable to the given value. Can be specified more than once for multiple environment variables.
file_server off # Disables the built-in file_server directive.
worker { # Creates a worker specific to this server. Can be specified more than once for multiple workers.
file <path> # Sets the path to the worker script, can be relative to the php_server root
num <num> # Sets the number of PHP threads to start, defaults to 2x the number of available
name <name> # Sets the name for the worker, used in logs and metrics. Default: absolute path of worker file. Always starts with m# when defined in a php_server block.
watch <path> # Sets the path to watch for file changes. Can be specified more than once for multiple paths.
env <key> <value> # Sets an extra environment variable to the given value. Can be specified more than once for multiple environment variables. Environment variables for this worker are also inherited from the php_server parent, but can be overwritten here.
match <path> # match the worker to a path pattern. Overrides try_files and can only be used in the php_server directive.
}
worker <other_file> <num> # Can also use the short form like in the global frankenphp block.
}
```
### Watching for File Changes
Since workers only boot your application once and keep it in memory, any changes
to your PHP files will not be reflected immediately.
Workers can instead be restarted on file changes via the `watch` directive.
This is useful for development environments.
```caddyfile
{
frankenphp {
worker {
file /path/to/app/public/worker.php
watch
}
}
}
```
If the `watch` directory is not specified, it will fall back to `./**/*.{php,yaml,yml,twig,env}`,
which watches all `.php`, `.yaml`, `.yml`, `.twig` and `.env` files in the directory and subdirectories
where the FrankenPHP process was started. You can instead also specify one or more directories via a
[shell filename pattern](https://pkg.go.dev/path/filepath#Match):
```caddyfile
{
frankenphp {
worker {
file /path/to/app/public/worker.php
watch /path/to/app # watches all files in all subdirectories of /path/to/app
watch /path/to/app/*.php # watches files ending in .php in /path/to/app
watch /path/to/app/**/*.php # watches PHP files in /path/to/app and subdirectories
watch /path/to/app/**/*.{php,twig} # watches PHP and Twig files in /path/to/app and subdirectories
}
}
}
```
- The `**` pattern signifies recursive watching
- Directories can also be relative (to where the FrankenPHP process is started from)
- If you have multiple workers defined, all of them will be restarted when a file changes
- Be wary about watching files that are created at runtime (like logs) since they might cause unwanted worker restarts.
The file watcher is based on [e-dant/watcher](https://github.com/e-dant/watcher).
## Matching the worker to a path
In traditional PHP applications, scripts are always placed in the public directory.
This is also true for worker scripts, which are treated like any other PHP script.
If you want to instead put the worker script outside the public directory, you can do so via the `match` directive.
The `match` directive is an optimized alternative to `try_files` only available inside `php_server` and `php`.
The following example will always serve a file in the public directory if present
and otherwise forward the request to the worker matching the path pattern.
```caddyfile
{
frankenphp {
php_server {
worker {
file /path/to/worker.php # file can be outside of public path
match /api/* # all requests starting with /api/ will be handled by this worker
}
}
}
}
```
@@ -131,13 +266,74 @@ php_server [<matcher>] {
The following environment variables can be used to inject Caddy directives in the `Caddyfile` without modifying it:
* `SERVER_NAME` change the server name
* `CADDY_GLOBAL_OPTIONS`: inject [global options](https://caddyserver.com/docs/caddyfile/options)
* `FRANKENPHP_CONFIG`: inject config under the `frankenphp` directive
- `SERVER_NAME`: change [the addresses on which to listen](https://caddyserver.com/docs/caddyfile/concepts#addresses), the provided hostnames will also be used for the generated TLS certificate
- `SERVER_ROOT`: change the root directory of the site, defaults to `public/`
- `CADDY_GLOBAL_OPTIONS`: inject [global options](https://caddyserver.com/docs/caddyfile/options)
- `FRANKENPHP_CONFIG`: inject config under the `frankenphp` directive
Unlike with FPM and CLI SAPIs, environment variables are **not** exposed by default in superglobals `$_SERVER` and `$_ENV`.
As for FPM and CLI SAPIs, environment variables are exposed by default in the `$_SERVER` superglobal.
To propagate environment variables to `$_SERVER` and `$_ENV`, set the `php.ini` `variables_order` directive to `EGPCS`.
The `S` value of [the `variables_order` PHP directive](https://www.php.net/manual/en/ini.core.php#ini.variables-order) is always equivalent to `ES` regardless of the placement of `E` elsewhere in this directive.
## PHP config
To load [additional PHP configuration files](https://www.php.net/manual/en/configuration.file.php#configuration.file.scan),
the `PHP_INI_SCAN_DIR` environment variable can be used.
When set, PHP will load all the file with the `.ini` extension present in the given directories.
You can also change the PHP configuration using the `php_ini` directive in the `Caddyfile`:
```caddyfile
{
frankenphp {
php_ini memory_limit 256M
# or
php_ini {
memory_limit 256M
max_execution_time 15
}
}
}
```
### 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
@@ -146,6 +342,6 @@ When using the Docker image, set the `CADDY_GLOBAL_OPTIONS` environment variable
```console
docker run -v $PWD:/app/public \
-e CADDY_GLOBAL_OPTIONS=debug \
-p 80:80 -p 443:443 \
-p 80:80 -p 443:443 -p 443:443/udp \
dunglas/frankenphp
```

BIN
docs/digitalocean-dns.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

View File

@@ -1,6 +1,17 @@
# 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/). Alpine Linux and Debian variants are provided for popular architectures. Variants for PHP 8.2 and PHP 8.3 are provided. [Browse tags](https://hub.docker.com/r/dunglas/frankenphp/tags).
[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, 8.4 and 8.5 are provided.
The tags follow this pattern: `dunglas/frankenphp:<frankenphp-version>-php<php-version>-<os>`
- `<frankenphp-version>` and `<php-version>` are version numbers of FrankenPHP and PHP respectively, ranging from major (e.g. `1`), minor (e.g. `1.2`) to patch versions (e.g. `1.2.3`).
- `<os>` is either `trixie` (for Debian Trixie), `bookworm` (for Debian Bookworm), or `alpine` (for the latest stable version of Alpine).
[Browse tags](https://hub.docker.com/r/dunglas/frankenphp/tags).
## How to Use The Images
@@ -12,13 +23,18 @@ FROM dunglas/frankenphp
COPY . /app/public
```
Then, run the commands to build and run the Docker image:
Then, run these commands to build and run the Docker image:
```console
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.
@@ -29,13 +45,11 @@ FROM dunglas/frankenphp
# add additional extensions here:
RUN install-php-extensions \
pdo_mysql \
gd \
intl \
zip \
opcache
# ...
pdo_mysql \
gd \
intl \
zip \
opcache
```
## How to Install More Caddy Modules
@@ -45,21 +59,26 @@ FrankenPHP is built on top of Caddy, and all [Caddy modules](https://caddyserver
The easiest way to install custom Caddy modules is to use [xcaddy](https://github.com/caddyserver/xcaddy):
```dockerfile
FROM dunglas/frankenphp:latest-builder AS builder
FROM dunglas/frankenphp:builder AS builder
# Copy xcaddy in the builder image
COPY --from=caddy:builder /usr/bin/xcaddy /usr/bin/xcaddy
# CGO must be enabled to build FrankenPHP
ENV CGO_ENABLED=1 XCADDY_SETCAP=1 XCADDY_GO_BUILD_FLAGS="-ldflags '-w -s'"
RUN xcaddy build \
--output /usr/local/bin/frankenphp \
--with github.com/dunglas/frankenphp=./ \
--with github.com/dunglas/frankenphp/caddy=./caddy/ \
# Mercure and Vulcain are included in the official build, but feel free to remove them
--with github.com/dunglas/mercure/caddy \
--with github.com/dunglas/vulcain/caddy
# Add extra Caddy modules here
RUN CGO_ENABLED=1 \
XCADDY_SETCAP=1 \
XCADDY_GO_BUILD_FLAGS="-ldflags='-w -s' -tags=nobadger,nomysql,nopgx" \
CGO_CFLAGS=$(php-config --includes) \
CGO_LDFLAGS="$(php-config --ldflags) $(php-config --libs)" \
xcaddy build \
--output /usr/local/bin/frankenphp \
--with github.com/dunglas/frankenphp=./ \
--with github.com/dunglas/frankenphp/caddy=./caddy/ \
--with github.com/dunglas/caddy-cbrotli \
# Mercure and Vulcain are included in the official build, but feel free to remove them
--with github.com/dunglas/mercure/caddy \
--with github.com/dunglas/vulcain/caddy
# Add extra Caddy modules here
FROM dunglas/frankenphp AS runner
@@ -67,8 +86,8 @@ FROM dunglas/frankenphp AS runner
COPY --from=builder /usr/local/bin/frankenphp /usr/local/bin/frankenphp
```
The `builder` image provided by FrankenPHP contains a compiled version of libphp.
[Builders images](https://hub.docker.com/r/dunglas/frankenphp/tags?name=builder) are provided for all versions of FrankenPHP and PHP, both for Alpine and Debian.
The `builder` image provided by FrankenPHP contains a compiled version of `libphp`.
[Builders images](https://hub.docker.com/r/dunglas/frankenphp/tags?name=builder) are provided for all versions of FrankenPHP and PHP, both for Debian and Alpine.
> [!TIP]
>
@@ -92,9 +111,13 @@ ENV FRANKENPHP_CONFIG="worker ./public/index.php"
To develop easily with FrankenPHP, mount the directory from your host containing the source code of the app as a volume in the Docker container:
```console
docker run -v $PWD:/app/public -p 80:80 -p 443:443 my-php-app
docker run -v $PWD:/app/public -p 80:80 -p 443:443 -p 443:443/udp --tty my-php-app
```
> [!TIP]
>
> The `--tty` option allows to have nice human-readable logs instead of JSON logs.
With Docker Compose:
```yaml
@@ -108,8 +131,9 @@ services:
# uncomment the following line if you want to run this in a production environment
# restart: always
ports:
- 80:80
- 443:443
- "80:80" # HTTP
- "443:443" # HTTPS
- "443:443/udp" # HTTP/3
volumes:
- ./:/app/public
- caddy_data:/data
@@ -122,3 +146,67 @@ volumes:
caddy_data:
caddy_config:
```
## Running as a Non-Root User
FrankenPHP can run as non-root user in Docker.
Here is a sample `Dockerfile` doing this:
```dockerfile
FROM dunglas/frankenphp
ARG USER=appuser
RUN \
# Use "adduser -D ${USER}" for alpine based distros
useradd ${USER}; \
# Add additional capability to bind to port 80 and 443
setcap CAP_NET_BIND_SERVICE=+eip /usr/local/bin/frankenphp; \
# Give write access to /config/caddy and /data/caddy
chown -R ${USER}:${USER} /config/caddy /data/caddy
USER ${USER}
```
### Running With No Capabilities
Even when running rootless, FrankenPHP needs the `CAP_NET_BIND_SERVICE` capability to bind the
web server on privileged ports (80 and 443).
If you expose FrankenPHP on a non-privileged port (1024 and above), it's possible to run
the webserver as a non-root user, and without the need for any capability:
```dockerfile
FROM dunglas/frankenphp
ARG USER=appuser
RUN \
# Use "adduser -D ${USER}" for alpine based distros
useradd ${USER}; \
# Remove default capability
setcap -r /usr/local/bin/frankenphp; \
# Give write access to /config/caddy and /data/caddy
chown -R ${USER}:${USER} /config/caddy /data/caddy
USER ${USER}
```
Next, set the `SERVER_NAME` environment variable to use an unprivileged port.
Example: `:8000`
## Updates
The Docker images are built:
- when a new release is tagged
- daily at 4 am UTC, if new versions of the official PHP images are available
## Development Versions
Development versions are available in the [`dunglas/frankenphp-dev`](https://hub.docker.com/repository/docker/dunglas/frankenphp-dev) Docker repository.
A new build is triggered every time a commit is pushed to the main branch of the GitHub repository.
The `latest*` tags point to the head of the `main` branch.
Tags of the form `sha-<git-commit-hash>` are also available.

View File

@@ -2,20 +2,22 @@
FrankenPHP has the ability to embed the source code and assets of PHP applications in a static, self-contained binary.
Thanks to this feature, PHP applications can be distributed as standalone binaries that include the application itself, the PHP interpreter and Caddy, a production-level web server.
Thanks to this feature, PHP applications can be distributed as standalone binaries that include the application itself, the PHP interpreter, and Caddy, a production-level web server.
Learn more about this feature [in the presentation made by Kévin at SymfonyCon](https://dunglas.dev/2023/12/php-and-symfony-apps-as-standalone-binaries/).
Learn more about this feature [in the presentation made by Kévin at SymfonyCon 2023](https://dunglas.dev/2023/12/php-and-symfony-apps-as-standalone-binaries/).
For embedding Laravel applications, [read this specific documentation entry](laravel.md#laravel-apps-as-standalone-binaries).
## Preparing Your App
Before creating the self-contained binary be sure that your app is ready for embedding.
For instance you likely want to:
For instance, you likely want to:
* Install the production dependencies of the app
* Dump the autoloader
* Enable the production mode of your application (if any)
* Strip uneeded files such as `.git` or tests to reduce the size of your final binary
- Install the production dependencies of the app
- Dump the autoloader
- Enable the production mode of your application (if any)
- Strip unneeded files such as `.git` or tests to reduce the size of your final binary
For instance, for a Symfony app, you can use the following commands:
@@ -29,7 +31,8 @@ cd $TMPDIR/my-prepared-app
echo APP_ENV=prod > .env.local
echo APP_DEBUG=0 >> .env.local
# Remove the tests
# Remove the tests and other unneeded files to save space
# Alternatively, add these files with the export-ignore attribute in your .gitattributes file
rm -Rf tests/
# Install the dependencies
@@ -39,37 +42,46 @@ composer install --ignore-platform-reqs --no-dev -a
composer dump-env prod
```
### Customizing the Configuration
To customize [the configuration](config.md), you can put a `Caddyfile` as well as a `php.ini` file
in the main directory of the app to be embedded (`$TMPDIR/my-prepared-app` in the previous example).
## Creating a Linux Binary
The easiest way to create a Linux binary is to use the Docker-based builder we provide.
1. Create a file named `static-build.Dockerfile` in the repository of your prepared app:
1. Create a file named `static-build.Dockerfile` in the repository of your app:
```dockerfile
FROM --platform=linux/amd64 dunglas/frankenphp:static-builder
```dockerfile
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
COPY . .
# 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/ \
PHP_EXTENSIONS=ctype,iconv,pdo_sqlite \
./build-static.sh
```
# Build the static binary
WORKDIR /go/src/app/
RUN EMBED=dist/app/ ./build-static.sh
```
> [!CAUTION]
>
> Some `.dockerignore` files (e.g. default [Symfony Docker `.dockerignore`](https://github.com/dunglas/symfony-docker/blob/main/.dockerignore))
> will ignore the `vendor/` directory and `.env` files. Be sure to adjust or remove the `.dockerignore` file before the build.
2. Build:
```console
docker build -t static-app -f static-build.Dockerfile .
```
```console
docker build -t static-app -f static-build.Dockerfile .
```
3. Extract the binary
3. Extract the binary:
```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
```
The resulting binary is the file named `my-app` in the current directory.
@@ -78,11 +90,9 @@ The resulting binary is the file named `my-app` in the current directory.
If you don't want to use Docker, or want to build a macOS binary, use the shell script we provide:
```console
git clone https://github.com/dunglas/frankenphp
git clone https://github.com/php/frankenphp
cd frankenphp
EMBED=/path/to/your/app \
PHP_EXTENSIONS=ctype,iconv,pdo_sqlite \
./build-static.sh
EMBED=/path/to/your/app ./build-static.sh
```
The resulting binary is the file named `frankenphp-<os>-<arch>` in the `dist/` directory.
@@ -103,7 +113,7 @@ If your app contains a [worker script](worker.md), start the worker with somethi
./my-app php-server --worker public/index.php
```
To enable HTTPS (a Let's Encrypt certificate is automatically created), HTTP/2 and HTTP/3, specify the domain name to use:
To enable HTTPS (a Let's Encrypt certificate is automatically created), HTTP/2, and HTTP/3, specify the domain name to use:
```console
./my-app php-server --domain localhost
@@ -115,13 +125,20 @@ You can also run the PHP CLI scripts embedded in your binary:
./my-app php-cli bin/console
```
## PHP Extensions
By default, the script will build extensions required by the `composer.json` file of your project, if any.
If the `composer.json` file doesn't exist, the default extensions are built, as documented in [the static builds entry](static.md).
To customize the extensions, use the `PHP_EXTENSIONS` environment variable.
## Customizing The Build
[Read the static build documentation](static.md) to see how to customize the binary (extensions, PHP version...).
## Distributing The Binary
The created binary isn't compressed.
To reduce the size of the file before sending it, you can compress it.
On Linux, the created binary is compressed using [UPX](https://upx.github.io).
On Mac, to reduce the size of the file before sending it, you can compress it.
We recommend `xz`.

893
docs/extensions.md Normal file
View File

@@ -0,0 +1,893 @@
# Writing PHP Extensions in Go
With FrankenPHP, you can **write PHP extensions in Go**, which allows you to create **high-performance native functions** that can be called directly from PHP. Your applications can leverage any existing or new Go library, as well as the infamous concurrency model of **goroutines right from your PHP code**.
Writing PHP extensions is typically done in C, but it's also possible to write them in other languages with a bit of extra work. PHP extensions allow you to leverage the power of low-level languages to extend PHP's functionalities, for example, by adding native functions or optimizing specific operations.
Thanks to Caddy modules, you can write PHP extensions in Go and integrate them very quickly into FrankenPHP.
## Two Approaches
FrankenPHP provides two ways to create PHP extensions in Go:
1. **Using the Extension Generator** - The recommended approach that generates all necessary boilerplate for most use cases, allowing you to focus on writing your Go code
2. **Manual Implementation** - Full control over the extension structure for advanced use cases
We'll start with the generator approach as it's the easiest way to get started, then show the manual implementation for those who need complete control.
## Using the Extension Generator
FrankenPHP is bundled with a tool that allows you **to create a PHP extension** only using Go. **No need to write C code** or use CGO directly: FrankenPHP also includes a **public types API** to help you write your extensions in Go without having to worry about **the type juggling between PHP/C and Go**.
> [!TIP]
> If you want to understand how extensions can be written in Go from scratch, you can read the manual implementation section below demonstrating how to write a PHP extension in Go without using the generator.
Keep in mind that this tool is **not a full-fledged extension generator**. It is meant to help you write simple extensions in Go, but it does not provide the most advanced features of PHP extensions. If you need to write a more **complex and optimized** extension, you may need to write some C code or use CGO directly.
### Prerequisites
As covered in the manual implementation section below as well, you need to [get the PHP sources](https://www.php.net/downloads.php) and create a new Go module.
#### Create a New Module and Get PHP Sources
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 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:
```console
tar xf php-*
```
### Writing the Extension
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 (
"strings"
"unsafe"
"github.com/dunglas/frankenphp"
)
//export_php:function repeat_this(string $str, int $count, bool $reverse): string
func repeat_this(s *C.zend_string, count int64, reverse bool) unsafe.Pointer {
str := frankenphp.GoString(unsafe.Pointer(s))
result := strings.Repeat(str, int(count))
if reverse {
runes := []rune(result)
for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
runes[i], runes[j] = runes[j], runes[i]
}
result = string(runes)
}
return frankenphp.PHPString(result, false)
}
```
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.
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:
| 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()` | ✅ |
| `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.
If you refer to the code snippet of the previous section, you can see that helpers are used to convert the first parameter and the return value. The second and third parameter of our `repeat_this()` function don't need to be converted as memory representation of the underlying types are the same for both C and Go.
#### Working with Arrays
FrankenPHP provides native support for PHP arrays through `frankenphp.AssociativeArray` or direct conversion to a map or slice.
`AssociativeArray` represents a [hash map](https://en.wikipedia.org/wiki/Hash_table) composed of a `Map: map[string]any`field and an optional `Order: []string` field (unlike PHP "associative arrays", Go maps aren't ordered).
If order or association are not needed, it's also possible to directly convert to a slice `[]any` or unordered map `map[string]any`.
**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.zend_array) unsafe.Pointer {
// Convert PHP associative array to Go while keeping the order
associativeArray, err := frankenphp.GoAssociativeArray[any](unsafe.Pointer(arr))
if err != nil {
// handle error
}
// loop over the entries in order
for _, key := range associativeArray.Order {
value, _ = associativeArray.Map[key]
// do something with key and value
}
// return an ordered array
// if 'Order' is not empty, only the key-value pairs in 'Order' will be respected
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.zend_array) unsafe.Pointer {
// Convert PHP associative array to a Go map without keeping the order
// ignoring the order will be more performant
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 {
// do something with key and value
}
// return an unordered array
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.zend_array) unsafe.Pointer {
// Convert PHP packed array to Go
goSlice, err := frankenphp.GoPackedArray(unsafe.Pointer(arr))
if err != nil {
// handle error
}
// loop over the slice in order
for index, value := range goSlice {
// do something with index and value
}
// return a packed array
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.
##### 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.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
Age int
}
```
#### What are Opaque Classes?
**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
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.
#### Adding Methods to Classes
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
Age int
}
//export_php:method User::getName(): string
func (us *UserStruct) GetUserName() unsafe.Pointer {
return frankenphp.PHPString(us.Name, false)
}
//export_php:method User::setAge(int $age): void
func (us *UserStruct) SetUserAge(age int64) {
us.Age = int(age)
}
//export_php:method User::getAge(): int
func (us *UserStruct) GetUserAge() int64 {
return int64(us.Age)
}
//export_php:method User::setNamePrefix(string $prefix = "User"): void
func (us *UserStruct) SetNamePrefix(prefix *C.zend_string) {
us.Name = frankenphp.GoString(unsafe.Pointer(prefix)) + ": " + us.Name
}
```
#### Nullable Parameters
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
}
}
```
**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
> [!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**:
```php
<?php
$user = new User();
// ✅ This works - using methods
$user->setAge(25);
echo $user->getName(); // Output: (empty, default value)
echo $user->getAge(); // Output: 25
$user->setNamePrefix("Employee");
// ✅ This also works - nullable parameters
$user->updateInfo("John", 30, true); // All parameters provided
$user->updateInfo("Jane", null, false); // Age is null
$user->updateInfo(null, 25, null); // Name and active are null
// ❌ This will NOT work - direct property access
// echo $user->name; // Error: Cannot access private property
// $user->age = 30; // Error: Cannot access private property
```
This design ensures that your Go code has complete control over how the object's state is accessed and modified, providing better encapsulation and type safety.
### Declaring Constants
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
//export_php:const
const API_VERSION = "1.2.3"
//export_php:const
const STATUS_OK = iota
//export_php:const
const STATUS_ERROR = iota
```
#### Class Constants
Use the `//export_php:classconst ClassName` directive to create constants that belong to a specific PHP class:
```go
package example
//export_php:classconst User
const STATUS_ACTIVE = 1
//export_php:classconst User
const STATUS_INACTIVE = 0
//export_php:classconst User
const ROLE_ADMIN = "admin"
//export_php:classconst Order
const STATE_PENDING = iota
//export_php:classconst Order
const STATE_PROCESSING = iota
//export_php:classconst Order
const STATE_COMPLETED = iota
```
Class constants are accessible using the class name scope in PHP:
```php
<?php
// Global constants
echo MAX_CONNECTIONS; // 100
echo API_VERSION; // "1.2.3"
// Class constants
echo User::STATUS_ACTIVE; // 1
echo User::ROLE_ADMIN; // "admin"
echo Order::STATE_PENDING; // 0
```
The directive supports various value types including strings, integers, booleans, floats, and iota constants. When using `iota`, the generator automatically assigns sequential values (0, 1, 2, etc.). Global constants become available in your PHP code as global constants, while class constants are scoped to their respective classes using the public visibility. When using integers, different possible notation (binary, hex, octal) are supported and dumped as is in the PHP stub file.
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 (
"strings"
"unsafe"
"github.com/dunglas/frankenphp"
)
//export_php:const
const STR_REVERSE = iota
//export_php:const
const STR_NORMAL = iota
//export_php:classconst StringProcessor
const MODE_LOWERCASE = 1
//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))
result := strings.Repeat(str, int(count))
if mode == STR_REVERSE {
// reverse the string
}
if mode == STR_NORMAL {
// no-op, just to showcase the constant
}
return frankenphp.PHPString(result, false)
}
//export_php:class StringProcessor
type StringProcessorStruct struct {
// 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)
}
```
### Using Namespaces
The generator supports organizing your PHP extension's functions, classes, and constants under a namespace using the `//export_php:namespace` directive. This helps avoid naming conflicts and provides better organization for your extension's API.
#### Declaring a Namespace
Use the `//export_php:namespace` directive at the top of your Go file to place all exported symbols under a specific namespace:
```go
//export_php:namespace My\Extension
package example
import (
"unsafe"
"github.com/dunglas/frankenphp"
)
//export_php:function hello(): string
func hello() string {
return "Hello from My\\Extension namespace!"
}
//export_php:class User
type UserStruct struct {
// internal fields
}
//export_php:method User::getName(): string
func (u *UserStruct) GetName() unsafe.Pointer {
return frankenphp.PHPString("John Doe", false)
}
//export_php:const
const STATUS_ACTIVE = 1
```
#### Using Namespaced Extension in PHP
When a namespace is declared, all functions, classes, and constants are placed under that namespace in PHP:
```php
<?php
echo My\Extension\hello(); // "Hello from My\Extension namespace!"
$user = new My\Extension\User();
echo $user->getName(); // "John Doe"
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.
### 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
```
> [!NOTE]
> Don't forget to set the `GEN_STUB_SCRIPT` environment variable to the path of the `gen_stub.php` file in the PHP sources you downloaded earlier. This is the same `gen_stub.php` script mentioned in the manual implementation section.
If everything went well, a new directory named `build` should have been created. This directory contains the generated files for your extension, including the `my_extension.go` file with the generated PHP function stubs.
### Integrating the Generated Extension into FrankenPHP
Our extension is now ready to be compiled and integrated into FrankenPHP. To do this, refer to the FrankenPHP [compilation documentation](compile.md) to learn how to compile FrankenPHP. Add the module using the `--with` flag, pointing to the path of your module:
```console
CGO_ENABLED=1 \
XCADDY_GO_BUILD_FLAGS="-ldflags='-w -s' -tags=nobadger,nomysql,nopgx" \
CGO_CFLAGS=$(php-config --includes) \
CGO_LDFLAGS="$(php-config --ldflags) $(php-config --libs)" \
xcaddy build \
--output frankenphp \
--with github.com/my-account/my-module/build
```
Note that you point to the `/build` subdirectory that was created during the generation step. However, this is not mandatory: you can also copy the generated files to your module directory and point to it directly.
### Testing Your Generated Extension
You can create a PHP file to test the functions and classes you've created. For example, create an `index.php` file with the following content:
```php
<?php
// Using global constants
var_dump(repeat_this('Hello World', 5, STR_REVERSE));
// Using class constants
$processor = new StringProcessor();
echo $processor->process('Hello World', StringProcessor::MODE_LOWERCASE); // "hello world"
echo $processor->process('Hello World', StringProcessor::MODE_UPPERCASE); // "HELLO WORLD"
```
Once you've integrated your extension into FrankenPHP as demonstrated in the previous section, you can run this test file using `./frankenphp php-server`, and you should see your extension working.
## Manual Implementation
If you want to understand how extensions work or need full control over your extension, you can write them manually. This approach gives you complete control but requires more boilerplate code.
### Basic Function
We'll see how to write a simple PHP extension in Go that defines a new native function. This function will be called from PHP and will trigger a goroutine that logs a message in Caddy's logs. This function doesn't take any parameters and returns nothing.
#### Define the Go Function
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 example
// #include "extension.h"
import "C"
import (
"log/slog"
"unsafe"
"github.com/dunglas/frankenphp"
)
func init() {
frankenphp.RegisterExtension(unsafe.Pointer(&C.ext_module_entry))
}
//export go_print_something
func go_print_something() {
go func() {
slog.Info("Hello from a goroutine!")
}()
}
```
The `frankenphp.RegisterExtension()` function simplifies the extension registration process by handling the internal PHP registration logic. The `go_print_something` function uses the `//export` directive to indicate that it will be accessible in the C code we will write, thanks to CGO.
In this example, our new function will trigger a goroutine that logs a message in Caddy's logs.
#### Define the PHP Function
To allow PHP to call our function, we need to define a corresponding PHP function. For this, we will create a stub file, for example, `extension.stub.php`, which will contain the following code:
```php
<?php
/** @generate-class-entries */
function go_print(): void {}
```
This file defines the signature of the `go_print()` function, which will be called from PHP. The `@generate-class-entries` directive allows PHP to automatically generate function entries for our extension.
This is not done manually but using a script provided in the PHP sources (make sure to adjust the path to the `gen_stub.php` script based on where your PHP sources are located):
```bash
php ../php-src/build/gen_stub.php extension.stub.php
```
This script will generate a file named `extension_arginfo.h` that contains the necessary information for PHP to know how to define and call our function.
#### Write the Bridge Between Go and C
Now, we need to write the bridge between Go and C. Create a file named `extension.h` in your module directory with the following content:
```c
#ifndef _EXTENSION_H
#define _EXTENSION_H
#include <php.h>
extern zend_module_entry ext_module_entry;
#endif
```
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.
Let's start by including the required headers:
```c
#include <php.h>
#include "extension.h"
#include "extension_arginfo.h"
// Contains symbols exported by Go
#include "_cgo_export.h"
```
We then define our PHP function as a native language function:
```c
PHP_FUNCTION(go_print)
{
ZEND_PARSE_PARAMETERS_NONE();
go_print_something();
}
zend_module_entry ext_module_entry = {
STANDARD_MODULE_HEADER,
"ext_go",
ext_functions, /* Functions */
NULL, /* MINIT */
NULL, /* MSHUTDOWN */
NULL, /* RINIT */
NULL, /* RSHUTDOWN */
NULL, /* MINFO */
"0.1.1",
STANDARD_MODULE_PROPERTIES
};
```
In this case, our function takes no parameters and returns nothing. It simply calls the Go function we defined earlier, exported using the `//export` directive.
Finally, we define the extension's metadata in a `zend_module_entry` structure, such as its name, version, and properties. This information is necessary for PHP to recognize and load our extension. Note that `ext_functions` is an array of pointers to the PHP functions we defined, and it was automatically generated by the `gen_stub.php` script in the `extension_arginfo.h` file.
The extension registration is automatically handled by FrankenPHP's `RegisterExtension()` function that we call in our Go code.
### Advanced Usage
Now that we know how to create a basic PHP extension in Go, let's complexify our example. We will now create a PHP function that takes a string as a parameter and returns its uppercase version.
#### Define the PHP Function Stub
To define the new PHP function, we will modify our `extension.stub.php` file to include the new function signature:
```php
<?php
/** @generate-class-entries */
/**
* Converts a string to uppercase.
*
* @param string $string The string to convert.
* @return string The uppercase version of the string.
*/
function go_upper(string $string): string {}
```
> [!TIP]
> Don't neglect the documentation of your functions! You are likely to share your extension stubs with other developers to document how to use your extension and which features are available.
By regenerating the stub file with the `gen_stub.php` script, the `extension_arginfo.h` file should look like this:
```c
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_go_upper, 0, 1, IS_STRING, 0)
ZEND_ARG_TYPE_INFO(0, string, IS_STRING, 0)
ZEND_END_ARG_INFO()
ZEND_FUNCTION(go_upper);
static const zend_function_entry ext_functions[] = {
ZEND_FE(go_upper, arginfo_go_upper)
ZEND_FE_END
};
```
We can see that the `go_upper` function is defined with a parameter of type `string` and a return type of `string`.
#### Type Juggling Between Go and PHP/C
Your Go function cannot directly accept a PHP string as a parameter. You need to convert it to a Go string. Fortunately, FrankenPHP provides helper functions to handle the conversion between PHP strings and Go strings, similar to what we saw in the generator approach.
The header file remains simple:
```c
#ifndef _EXTENSION_H
#define _EXTENSION_H
#include <php.h>
extern zend_module_entry ext_module_entry;
#endif
```
We can now write the bridge between Go and C in our `extension.c` file. We will pass the PHP string directly to our Go function:
```c
PHP_FUNCTION(go_upper)
{
zend_string *str;
ZEND_PARSE_PARAMETERS_START(1, 1)
Z_PARAM_STR(str)
ZEND_PARSE_PARAMETERS_END();
zend_string *result = go_upper(str);
RETVAL_STR(result);
}
```
You can learn more about the `ZEND_PARSE_PARAMETERS_START` and parameters parsing in the dedicated page of [the PHP Internals Book](https://www.phpinternalsbook.com/php7/extensions_design/php_functions.html#parsing-parameters-zend-parse-parameters). Here, we tell PHP that our function takes one mandatory parameter of type `string` as a `zend_string`. We then pass this string directly to our Go function and return the result using `RETVAL_STR`.
There's only one thing left to do: implement the `go_upper` function in Go.
#### Implement the Go Function
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
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).
> [!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
Our extension is now ready to be compiled and integrated into FrankenPHP. To do this, refer to the FrankenPHP [compilation documentation](compile.md) to learn how to compile FrankenPHP. Add the module using the `--with` flag, pointing to the path of your module:
```console
CGO_ENABLED=1 \
XCADDY_GO_BUILD_FLAGS="-ldflags='-w -s' -tags=nobadger,nomysql,nopgx" \
CGO_CFLAGS=$(php-config --includes) \
CGO_LDFLAGS="$(php-config --ldflags) $(php-config --libs)" \
xcaddy build \
--output frankenphp \
--with github.com/my-account/my-module
```
That's it! Your extension is now integrated into FrankenPHP and can be used in your PHP code.
### Testing Your Extension
After integrating your extension into FrankenPHP, you can create an `index.php` file with examples for the functions you've implemented:
```php
<?php
// Test basic function
go_print();
// Test advanced function
echo go_upper("hello world") . "\n";
```
You can now run FrankenPHP with this file using `./frankenphp php-server`, and you should see your extension working.

220
docs/fr/CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,220 @@
# Contribuer
## Compiler PHP
### Avec Docker (Linux)
Construisez l'image Docker de développement :
```console
docker build -t frankenphp-dev -f dev.Dockerfile .
docker run --cap-add=SYS_PTRACE --security-opt seccomp=unconfined -p 8080:8080 -p 443:443 -p 443:443/udp -v $PWD:/go/src/app -it frankenphp-dev
```
L'image contient les outils de développement habituels (Go, GDB, Valgrind, Neovim...) et utilise les emplacements de configuration PHP suivants
- php.ini: `/etc/frankenphp/php.ini` Un fichier php.ini avec des préréglages de développement est fourni par défaut.
- fichiers de configuration supplémentaires: `/etc/frankenphp/php.d/*.ini`
- extensions php: `/usr/lib/frankenphp/modules/`
Si votre version de Docker est inférieure à 23.0, la construction échouera à cause d'un [problème de pattern](https://github.com/moby/moby/pull/42676) dans `.dockerignore`. Ajoutez les répertoires à `.dockerignore`.
```patch
!testdata/*.php
!testdata/*.txt
+!caddy
+!internal
```
### Sans Docker (Linux et macOS)
[Suivez les instructions pour compiler à partir des sources](compile.md) et passez l'indicateur de configuration `--debug`.
## Exécution de la suite de tests
```console
go test -tags watcher -race -v ./...
```
## Module Caddy
Construire Caddy avec le module FrankenPHP :
```console
cd caddy/frankenphp/
go build -tags watcher,brotli,nobadger,nomysql,nopgx
cd ../../
```
Exécuter Caddy avec le module FrankenPHP :
```console
cd testdata/
../caddy/frankenphp/frankenphp run
```
Le serveur est configuré pour écouter à l'adresse `127.0.0.1:80`:
> [!NOTE]
>
> Si vous utilisez Docker, vous devrez soit lier le port 80 du conteneur, soit exécuter depuis l'intérieur du conteneur.
```console
curl -vk http://127.0.0.1/phpinfo.php
```
## Serveur de test minimal
Construire le serveur de test minimal :
```console
cd internal/testserver/
go build
cd ../../
```
Lancer le test serveur :
```console
cd testdata/
../internal/testserver/testserver
```
Le serveur est configuré pour écouter à l'adresse `127.0.0.1:8080`:
```console
curl -v http://127.0.0.1:8080/phpinfo.php
```
## Construire localement les images Docker
Afficher le plan de compilation :
```console
docker buildx bake -f docker-bake.hcl --print
```
Construire localement les images FrankenPHP pour amd64 :
```console
docker buildx bake -f docker-bake.hcl --pull --load --set "*.platform=linux/amd64"
```
Construire localement les images FrankenPHP pour arm64 :
```console
docker buildx bake -f docker-bake.hcl --pull --load --set "*.platform=linux/arm64"
```
Construire à partir de zéro les images FrankenPHP pour arm64 & amd64 et les pousser sur Docker Hub :
```console
docker buildx bake -f docker-bake.hcl --pull --no-cache --push
```
## Déboguer les erreurs de segmentation avec les builds statiques
1. Téléchargez la version de débogage du binaire FrankenPHP depuis GitHub ou créez votre propre build statique incluant des symboles de débogage :
```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. Remplacez votre version actuelle de `frankenphp` par l'exécutable de débogage de FrankenPHP.
3. Démarrez FrankenPHP comme d'habitude (alternativement, vous pouvez directement démarrer FrankenPHP avec GDB : `gdb --args frankenphp run`).
4. Attachez-vous au processus avec GDB :
```console
gdb -p `pidof frankenphp`
```
5. Si nécessaire, tapez `continue` dans le shell GDB
6. Faites planter FrankenPHP.
7. Tapez `bt` dans le shell GDB
8. Copiez la sortie
## Déboguer les erreurs de segmentation dans GitHub Actions
1. Ouvrir `.github/workflows/tests.yml`
2. Activer les symboles de débogage de la bibliothèque PHP
```patch
- uses: shivammathur/setup-php@v2
# ...
env:
phpts: ts
+ debug: true
```
3. Activer `tmate` pour se connecter au conteneur
```patch
- name: Set CGO flags
run: echo "CGO_CFLAGS=$(php-config --includes)" >> "$GITHUB_ENV"
+ - run: |
+ sudo apt install gdb
+ mkdir -p /home/runner/.config/gdb/
+ printf "set auto-load safe-path /\nhandle SIG34 nostop noprint pass" > /home/runner/.config/gdb/gdbinit
+ - uses: mxschmitt/action-tmate@v3
```
4. Se connecter au conteneur
5. Ouvrir `frankenphp.go`
6. Activer `cgosymbolizer`
```patch
- //_ "github.com/ianlancetaylor/cgosymbolizer"
+ _ "github.com/ianlancetaylor/cgosymbolizer"
```
7. Télécharger le module : `go get`
8. Dans le conteneur, vous pouvez utiliser GDB et similaires :
```console
go test -tags watcher -c -ldflags=-w
gdb --args frankenphp.test -test.run ^MyTest$
```
9. Quand le bug est corrigé, annulez tous les changements.
## Ressources Diverses pour le Développement
- [Intégration de PHP dans uWSGI](https://github.com/unbit/uwsgi/blob/master/plugins/php/php_plugin.c)
- [Intégration de PHP dans NGINX Unit](https://github.com/nginx/unit/blob/master/src/nxt_php_sapi.c)
- [Intégration de PHP dans Go (go-php)](https://github.com/deuill/go-php)
- [Intégration de PHP dans Go (GoEmPHP)](https://github.com/mikespook/goemphp)
- [Intégration de PHP dans C++](https://gist.github.com/paresy/3cbd4c6a469511ac7479aa0e7c42fea7)
- [Extending and Embedding PHP par Sara Golemon](https://books.google.fr/books?id=zMbGvK17_tYC&pg=PA254&lpg=PA254#v=onepage&q&f=false)
- [Qu'est-ce que TSRMLS_CC, au juste ?](http://blog.golemon.com/2006/06/what-heck-is-tsrmlscc-anyway.html)
- [Intégration de PHP sur Mac](https://gist.github.com/jonnywang/61427ffc0e8dde74fff40f479d147db4)
- [Bindings SDL](https://pkg.go.dev/github.com/veandco/go-sdl2@v0.4.21/sdl#Main)
## Ressources Liées à Docker
- [Définition du fichier Bake](https://docs.docker.com/build/customize/bake/file-definition/)
- [`docker buildx build`](https://docs.docker.com/engine/reference/commandline/buildx_build/)
## Commande utile
```console
apk add strace util-linux gdb
strace -e 'trace=!futex,epoll_ctl,epoll_pwait,tgkill,rt_sigreturn' -p 1
```
## Traduire la documentation
Pour traduire la documentation et le site dans une nouvelle langue, procédez comme suit :
1. Créez un nouveau répertoire nommé avec le code ISO à 2 caractères de la langue dans le répertoire `docs/` de ce dépôt
2. Copiez tous les fichiers `.md` à la racine du répertoire `docs/` dans le nouveau répertoire (utilisez toujours la version anglaise comme source de traduction, car elle est toujours à jour).
3. Copiez les fichiers `README.md` et `CONTRIBUTING.md` du répertoire racine vers le nouveau répertoire.
4. Traduisez le contenu des fichiers, mais ne changez pas les noms de fichiers, ne traduisez pas non plus les chaînes commençant par `> [!` (c'est un balisage spécial pour GitHub).
5. Créez une Pull Request avec les traductions
6. Dans le [référentiel du site](https://github.com/dunglas/frankenphp-website/tree/main), copiez et traduisez les fichiers de traduction dans les répertoires `content/`, `data/` et `i18n/`.
7. Traduire les valeurs dans le fichier YAML créé.
8. Ouvrir une Pull Request sur le dépôt du site.

159
docs/fr/README.md Normal file
View File

@@ -0,0 +1,159 @@
# FrankenPHP : le serveur d'applications PHP moderne, écrit en Go
<h1 align="center"><a href="https://frankenphp.dev"><img src="../../frankenphp.png" alt="FrankenPHP" width="600"></a></h1>
FrankenPHP est un serveur d'applications moderne pour PHP construit à partir du serveur web [Caddy](https://caddyserver.com/).
FrankenPHP donne des super-pouvoirs à vos applications PHP grâce à ses fonctionnalités à la pointe : [_Early Hints_](early-hints.md), [mode worker](worker.md), [fonctionnalités en temps réel](mercure.md), HTTPS automatique, prise en charge de HTTP/2 et HTTP/3...
FrankenPHP fonctionne avec n'importe quelle application PHP et rend vos projets Laravel et Symfony plus rapides que jamais grâce à leurs intégrations officielles avec le mode worker.
FrankenPHP peut également être utilisé comme une bibliothèque Go autonome qui permet d'intégrer PHP dans n'importe quelle application en utilisant `net/http`.
Découvrez plus de détails sur ce serveur dapplication dans le replay de cette conférence donnée au Forum PHP 2022 :
<a href="https://dunglas.dev/2022/10/frankenphp-the-modern-php-app-server-written-in-go/"><img src="https://dunglas.dev/wp-content/uploads/2022/10/frankenphp.png" alt="Diapositives" width="600"></a>
## Pour Commencer
Sur Windows, utilisez [WSL](https://learn.microsoft.com/windows/wsl/) pour exécuter FrankenPHP.
### Script d'installation
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
```
### 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
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
```
**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
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
Des [images Docker](https://frankenphp.dev/docs/fr/docker/) sont également disponibles :
```console
docker run -v .:/app/public \
-p 80:80 -p 443:443 -p 443:443/udp \
dunglas/frankenphp
```
Rendez-vous sur `https://localhost`, c'est parti !
> [!TIP]
>
> Ne tentez pas d'utiliser `https://127.0.0.1`. Utilisez `https://localhost` et acceptez le certificat auto-signé.
> Utilisez [la variable d'environnement `SERVER_NAME`](config.md#variables-denvironnement) pour changer le domaine à utiliser.
### Homebrew
FrankenPHP est également disponible sous forme de paquet [Homebrew](https://brew.sh) pour macOS et Linux.
Pour l'installer :
```console
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)
- [Le mode worker](worker.md)
- [Le support des Early Hints (code de statut HTTP 103)](early-hints.md)
- [Temps réel](mercure.md)
- [Servir efficacement les fichiers statiques volumineux](x-sendfile.md)
- [Configuration](config.md)
- [Écrire des extensions PHP en Go](extensions.md)
- [Images Docker](docker.md)
- [Déploiement en production](production.md)
- [Optimisation des performances](performance.md)
- [Créer des applications PHP **standalone**, auto-exécutables](embed.md)
- [Créer un build statique](static.md)
- [Compiler depuis les sources](compile.md)
- [Surveillance de FrankenPHP](metrics.md)
- [Intégration Laravel](laravel.md)
- [Problèmes connus](known-issues.md)
- [Application de démo (Symfony) et benchmarks](https://github.com/dunglas/frankenphp-demo)
- [Documentation de la bibliothèque Go](https://pkg.go.dev/github.com/dunglas/frankenphp)
- [Contribuer et débugger](CONTRIBUTING.md)
## Exemples et squelettes
- [Symfony](https://github.com/dunglas/symfony-docker)
- [API Platform](https://api-platform.com/docs/distribution/)
- [Laravel](laravel.md)
- [Sulu](https://sulu.io/blog/running-sulu-with-frankenphp)
- [WordPress](https://github.com/StephenMiracle/frankenwp)
- [Drupal](https://github.com/dunglas/frankenphp-drupal)
- [Joomla](https://github.com/alexandreelise/frankenphp-joomla)
- [TYPO3](https://github.com/ochorocho/franken-typo3)
- [Magento2](https://github.com/ekino/frankenphp-magento2)

11
docs/fr/classic.md Normal file
View File

@@ -0,0 +1,11 @@
# Utilisation du mode classique
Sans aucune configuration additionnelle, FrankenPHP fonctionne en mode classique. Dans ce mode, FrankenPHP fonctionne comme un serveur PHP traditionnel, en servant directement les fichiers PHP. Cela en fait un remplaçant parfait à PHP-FPM ou Apache avec mod_php.
Comme Caddy, FrankenPHP accepte un nombre illimité de connexions et utilise un [nombre fixe de threads](config.md#configuration-du-caddyfile) pour les servir. Le nombre de connexions acceptées et en attente n'est limité que par les ressources système disponibles.
Le pool de threads PHP fonctionne avec un nombre fixe de threads initialisés au démarrage, comparable au mode statique de PHP-FPM. Il est également possible de laisser les threads [s'adapter automatiquement à l'exécution](performance.md#max_threads), comme dans le mode dynamique de PHP-FPM.
Les connexions en file d'attente attendront indéfiniment jusqu'à ce qu'un thread PHP soit disponible pour les servir. Pour éviter cela, vous pouvez utiliser la [configuration](config.md#configuration-du-caddyfile) `max_wait_time` dans la configuration globale de FrankenPHP pour limiter la durée pendant laquelle une requête peut attendre un thread PHP libre avant d'être rejetée.
En outre, vous pouvez définir un [délai d'écriture dans Caddy](https://caddyserver.com/docs/caddyfile/options#timeouts) raisonnable.
Chaque instance de Caddy n'utilisera qu'un seul pool de threads FrankenPHP, qui sera partagé par tous les blocs `php_server`.

128
docs/fr/compile.md Normal file
View File

@@ -0,0 +1,128 @@
# Compiler depuis les sources
Ce document explique comment créer un build FrankenPHP qui chargera PHP en tant que bibliothèque dynamique.
C'est la méthode recommandée.
Alternativement, il est aussi possible de [créer des builds statiques](static.md).
## Installer PHP
FrankenPHP est compatible avec PHP 8.2 et versions ultérieures.
### Avec Homebrew (Linux et Mac)
La manière la plus simple d'installer une version de libphp compatible avec FrankenPHP est d'utiliser les paquets ZTS fournis par [Homebrew PHP](https://github.com/shivammathur/homebrew-php).
Tout d'abord, si ce n'est déjà fait, installez [Homebrew](https://brew.sh).
Ensuite, installez la variante ZTS de PHP, Brotli (facultatif, pour la prise en charge de la compression) et watcher (facultatif, pour la détection des modifications de fichiers) :
```console
brew install shivammathur/php/php-zts brotli watcher
brew link --overwrite --force shivammathur/php/php-zts
```
### En compilant PHP
Vous pouvez également compiler PHP à partir des sources avec les options requises par FrankenPHP en suivant ces étapes.
Tout d'abord, [téléchargez les sources de PHP](https://www.php.net/downloads.php) et extrayez-les :
```console
tar xf php-*
cd php-*/
```
Ensuite, configurez PHP pour votre système d'exploitation.
Les options de configuration suivantes sont nécessaires pour la compilation, mais vous pouvez également inclure d'autres options selon vos besoins, par exemple pour ajouter des extensions et fonctionnalités supplémentaires.
### Linux
```console
./configure \
--enable-embed \
--enable-zts \
--disable-zend-signals \
--enable-zend-max-execution-timers
```
### Mac
Utilisez le gestionnaire de paquets [Homebrew](https://brew.sh/) pour installer les dépendances obligatoires et optionnelles :
```console
brew install libiconv bison brotli re2c pkg-config watcher
echo 'export PATH="/opt/homebrew/opt/bison/bin:$PATH"' >> ~/.zshrc
```
Puis exécutez le script de configuration :
```console
./configure \
--enable-embed \
--enable-zts \
--disable-zend-signals \
--disable-opcache-jit \
--with-iconv=/opt/homebrew/opt/libiconv/
```
### Compilez PHP
Finalement, compilez et installez PHP :
```console
make -j"$(getconf _NPROCESSORS_ONLN)"
sudo make install
```
## Installez les dépendances optionnelles
Certaines fonctionnalités de FrankenPHP nécessitent des dépendances optionnelles qui doivent être installées.
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 |
## Compiler l'application Go
### Utiliser xcaddy
La méthode recommandée consiste à utiliser [xcaddy](https://github.com/caddyserver/xcaddy) pour compiler FrankenPHP.
`xcaddy` permet également d'ajouter facilement des [modules Caddy personnalisés](https://caddyserver.com/docs/modules/) et des extensions FrankenPHP :
```console
CGO_ENABLED=1 \
XCADDY_GO_BUILD_FLAGS="-ldflags='-w -s' -tags=nobadger,nomysql,nopgx" \
CGO_CFLAGS=$(php-config --includes) \
CGO_LDFLAGS="$(php-config --ldflags) $(php-config --libs)" \
xcaddy build \
--output frankenphp \
--with github.com/dunglas/frankenphp/caddy \
--with github.com/dunglas/caddy-cbrotli \
--with github.com/dunglas/mercure/caddy \
--with github.com/dunglas/vulcain/caddy
# Ajoutez les modules Caddy supplémentaires et les extensions FrankenPHP ici
```
> [!TIP]
>
> Si vous utilisez musl libc (la bibliothèque par défaut sur Alpine Linux) et Symfony,
> vous pourriez avoir besoin d'augmenter la taille par défaut de la pile.
> Sinon, vous pourriez rencontrer des erreurs telles que `PHP Fatal error: Maximum call stack size of 83360 bytes reached during compilation. Try splitting expression`
>
> Pour ce faire, modifiez la variable d'environnement `XCADDY_GO_BUILD_FLAGS` en quelque chose comme
> `XCADDY_GO_BUILD_FLAGS=$'-ldflags "-w -s -extldflags \'-Wl,-z,stack-size=0x80000\'"'`
> (modifiez la valeur de la taille de la pile selon les besoins de votre application).
### Sans xcaddy
Il est également possible de compiler FrankenPHP sans `xcaddy` en utilisant directement la commande `go` :
```console
curl -L https://github.com/php/frankenphp/archive/refs/heads/main.tar.gz | tar xz
cd frankenphp-main/caddy/frankenphp
CGO_CFLAGS=$(php-config --includes) CGO_LDFLAGS="$(php-config --ldflags) $(php-config --libs)" go build -tags=nobadger,nomysql,nopgx
```

281
docs/fr/config.md Normal file
View File

@@ -0,0 +1,281 @@
# 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).
Dans [les images Docker](docker.md), le `Caddyfile` est situé dans `/etc/frankenphp/Caddyfile`.
Le binaire statique cherchera le `Caddyfile` dans le répertoire dans lequel il est démarré.
PHP lui-même peut être configuré [en utilisant un fichier `php.ini`](https://www.php.net/manual/fr/configuration.file.php).
L'interpréteur PHP cherchera dans les emplacements suivants :
Docker :
- php.ini : `/usr/local/etc/php/php.ini` Aucun php.ini n'est fourni par défaut.
- fichiers de configuration supplémentaires : `/usr/local/etc/php/conf.d/*.ini`
- extensions php : `/usr/local/lib/php/extensions/no-debug-zts-<YYYYMMDD>/`
- Vous devriez copier un modèle officiel fourni par le projet PHP :
```dockerfile
FROM dunglas/frankenphp
# Production :
RUN cp $PHP_INI_DIR/php.ini-production $PHP_INI_DIR/php.ini
# Ou développement :
RUN cp $PHP_INI_DIR/php.ini-development $PHP_INI_DIR/php.ini
```
Installation de FrankenPHP (.rpm ou .deb) :
- php.ini : `/etc/frankenphp/php.ini` Un fichier php.ini avec des préréglages de production est fourni par défaut.
- fichiers de configuration supplémentaires : `/etc/frankenphp/php.d/*.ini`
- extensions php : `/usr/lib/frankenphp/modules/`
Binaire statique :
- php.ini : Le répertoire dans lequel `frankenphp run` ou `frankenphp php-server` est exécuté, puis `/etc/frankenphp/php.ini`
- fichiers de configuration supplémentaires : `/etc/frankenphp/php.d/*.ini`
- extensions php : ne peuvent pas être chargées
- copiez l'un des fichiers `php.ini-production` ou `php.ini-development` fournis [dans les sources de PHP](https://github.com/php/php-src/).
## Configuration du Caddyfile
Les [directives HTTP](https://caddyserver.com/docs/caddyfile/concepts#directives) `php_server` ou `php` peuvent être utilisées dans les blocs de site pour servir votre application PHP.
Exemple minimal :
```caddyfile
localhost {
# Activer la compression (optionnel)
encode zstd br gzip
# Exécuter les fichiers PHP dans le répertoire courant et servir les assets
php_server
}
```
Vous pouvez également configurer explicitement FrankenPHP en utilisant l'option globale :
L'[option globale](https://caddyserver.com/docs/caddyfile/concepts#global-options) `frankenphp` peut être utilisée pour configurer FrankenPHP.
```caddyfile
{
frankenphp {
num_threads <num_threads> # Définit le nombre de threads PHP à démarrer. Par défaut : 2x le nombre de CPUs disponibles.
max_threads <num_threads> # Limite le nombre de threads PHP supplémentaires qui peuvent être démarrés au moment de l'exécution. Valeur par défaut : num_threads. Peut être mis à 'auto'.
max_wait_time <duration> # Définit le temps maximum pendant lequel une requête peut attendre un thread PHP libre avant d'être interrompue. Valeur par défaut : désactivé.
php_ini <key> <value> Définit une directive php.ini. Peut être utilisé plusieurs fois pour définir plusieurs directives.
worker {
file <path> # Définit le chemin vers le script worker.
num <num> # Définit le nombre de threads PHP à démarrer, par défaut 2x le nombre de CPUs disponibles.
env <key> <value> # Définit une variable d'environnement supplémentaire avec la valeur donnée. Peut être spécifié plusieurs fois pour régler plusieurs variables d'environnement.
watch <path> # Définit le chemin d'accès à surveiller pour les modifications de fichiers. Peut être spécifié plusieurs fois pour plusieurs chemins.
name <name> # Définit le nom du worker, utilisé dans les journaux et les métriques. Défaut : chemin absolu du fichier du worker
max_consecutive_failures <num> # Définit le nombre maximum d'échecs consécutifs avant que le worker ne soit considéré comme défaillant, -1 signifie que le worker redémarre toujours. Par défaut : 6.
}
}
}
# ...
```
Vous pouvez également utiliser la forme courte de l'option worker en une seule ligne :
```caddyfile
{
frankenphp {
worker <file> <num>
}
}
# ...
```
Vous pouvez aussi définir plusieurs workers si vous servez plusieurs applications sur le même serveur :
```caddyfile
app.example.com {
root /path/to/app/public
php_server {
root /path/to/app/public # permet une meilleure mise en cache
worker index.php <num>
}
}
other.example.com {
root /path/to/other/public
php_server {
root /path/to/other/public
worker index.php <num>
}
}
# ...
```
L'utilisation de la directive `php_server` est généralement suffisante,
mais si vous avez besoin d'un contrôle total, vous pouvez utiliser la directive `php`, qui permet un plus grand niveau de finesse dans la configuration.
La directive `php` transmet toutes les entrées à PHP, au lieu de vérifier d'abord si
c'est un fichier PHP ou pas. En savoir plus à ce sujet dans la [page performances](performance.md#try_files).
Utiliser la directive `php_server` est équivalent à cette configuration :
```caddyfile
route {
# Ajoute un slash final pour les requêtes de répertoire
@canonicalPath {
file {path}/index.php
not path */
}
redir @canonicalPath {path}/ 308
# Si le fichier demandé n'existe pas, essayer les fichiers index
@indexFiles file {
try_files {path} {path}/index.php index.php
split_path .php
}
rewrite @indexFiles {http.matchers.file.relative}
# FrankenPHP!
@phpFiles path *.php
php @phpFiles
file_server
}
```
Les directives `php_server` et `php` disposent des options suivantes :
```caddyfile
php_server [<matcher>] {
root <directory> # Définit le dossier racine du le site. Par défaut : valeur de la directive `root` parente.
split_path <delim...> # Définit les sous-chaînes pour diviser l'URI en deux parties. La première sous-chaîne correspondante sera utilisée pour séparer le "path info" du chemin. La première partie est suffixée avec la sous-chaîne correspondante et sera considérée comme le nom réel de la ressource (script CGI). La seconde partie sera définie comme PATH_INFO pour utilisation par le script. Par défaut : `.php`
resolve_root_symlink false # Désactive la résolution du répertoire `root` vers sa valeur réelle en évaluant un lien symbolique, s'il existe (activé par défaut).
env <key> <value> # Définit une variable d'environnement supplémentaire avec la valeur donnée. Peut être spécifié plusieurs fois pour plusieurs variables d'environnement.
file_server off # Désactive la directive file_server intégrée.
worker { # Crée un worker spécifique à ce serveur. Peut être spécifié plusieurs fois pour plusieurs workers.
file <path> # Définit le chemin vers le script worker, peut être relatif à la racine du php_server
num <num> # Définit le nombre de threads PHP à démarrer, par défaut 2x le nombre de CPUs disponibles
name <name> # Définit le nom du worker, utilisé dans les journaux et les métriques. Défaut : chemin absolu du fichier du worker. Commence toujours par m# lorsqu'il est défini dans un bloc php_server.
watch <path> # Définit le chemin d'accès à surveiller pour les modifications de fichiers. Peut être spécifié plusieurs fois pour plusieurs chemins.
env <key> <value> # Définit une variable d'environnement supplémentaire avec la valeur donnée. Peut être spécifié plusieurs fois pour plusieurs variables d'environnement. Les variables d'environnement pour ce worker sont également héritées du parent php_server, mais peuvent être écrasées ici.
}
worker <other_file> <num> # Peut également utiliser la forme courte comme dans le bloc frankenphp global.
}
```
### Surveillance des modifications de fichier
Vu que les workers ne démarrent votre application qu'une seule fois et la gardent en mémoire, toute modification
apportée à vos fichiers PHP ne sera pas répercutée immédiatement.
Les workers peuvent être redémarrés en cas de changement de fichier via la directive `watch`.
Ceci est utile pour les environnements de développement.
```caddyfile
{
frankenphp {
worker {
file /path/to/app/public/worker.php
watch
}
}
}
```
Si le répertoire `watch` n'est pas précisé, il se rabattra sur `./**/*.{php,yaml,yml,twig,env}`,
qui surveille tous les fichiers `.php`, `.yaml`, `.yml`, `.twig` et `.env` dans le répertoire et les sous-répertoires
où le processus FrankenPHP a été lancé. Vous pouvez également spécifier un ou plusieurs répertoires via une commande
[motif de nom de fichier shell](https://pkg.go.dev/path/filepath#Match) :
```caddyfile
{
frankenphp {
worker {
file /path/to/app/public/worker.php
watch /path/to/app # surveille tous les fichiers dans tous les sous-répertoires de /path/to/app
watch /path/to/app/*.php # surveille les fichiers se terminant par .php dans /path/to/app
watch /path/to/app/**/*.php # surveille les fichiers PHP dans /path/to/app et les sous-répertoires
watch /path/to/app/**/*.{php,twig} # surveille les fichiers PHP et Twig dans /path/to/app et les sous-répertoires
}
}
}
```
- Le motif `**` signifie une surveillance récursive.
- Les répertoires peuvent également être relatifs (depuis l'endroit où le processus FrankenPHP est démarré).
- Si vous avez défini plusieurs workers, ils seront tous redémarrés lorsqu'un fichier est modifié.
- Méfiez-vous des fichiers créés au moment de l'exécution (comme les logs) car ils peuvent provoquer des redémarrages intempestifs du worker.
La surveillance des fichiers est basé sur [e-dant/watcher](https://github.com/e-dant/watcher).
### Full Duplex (HTTP/1)
Lors de l'utilisation de HTTP/1.x, il peut être souhaitable d'activer le mode full-duplex pour permettre l'écriture d'une réponse avant que le corps entier
n'ait été lu. (par exemple : WebSocket, événements envoyés par le serveur, etc.)
Il s'agit d'une configuration optionnelle qui doit être ajoutée aux options globales dans le fichier `Caddyfile` :
```caddyfile
{
servers {
enable_full_duplex
}
}
```
> [!CAUTION]
>
> L'activation de cette option peut entraîner un blocage (deadlock) des anciens clients HTTP/1.x qui ne supportent pas le full-duplex.
> Cela peut aussi être configuré en utilisant la variable d'environnement `CADDY_GLOBAL_OPTIONS` :
```sh
CADDY_GLOBAL_OPTIONS="servers {
enable_full_duplex
}"
```
Vous trouverez plus d'informations sur ce paramètre dans la [documentation Caddy](https://caddyserver.com/docs/caddyfile/options#enable-full-duplex).
## Variables d'environnement
Les variables d'environnement suivantes peuvent être utilisées pour insérer des directives Caddy dans le `Caddyfile` sans le modifier :
- `SERVER_NAME` : change [les adresses sur lesquelles écouter](https://caddyserver.com/docs/caddyfile/concepts#addresses), les noms d'hôte fournis seront également utilisés pour le certificat TLS généré
- `SERVER_ROOT` : change le répertoire racine du site, par défaut `public/`
- `CADDY_GLOBAL_OPTIONS` : injecte [des options globales](https://caddyserver.com/docs/caddyfile/options)
- `FRANKENPHP_CONFIG` : insère la configuration sous la directive `frankenphp`
Comme pour les SAPI FPM et CLI, les variables d'environnement ne sont exposées par défaut dans la superglobale `$_SERVER`.
La valeur `S` de [la directive `variables_order` de PHP](https://www.php.net/manual/fr/ini.core.php#ini.variables-order) est toujours équivalente à `ES`, que `E` soit défini ailleurs dans cette directive ou non.
## Configuration PHP
Pour charger [des fichiers de configuration PHP supplémentaires](https://www.php.net/manual/fr/configuration.file.php#configuration.file.scan),
la variable d'environnement `PHP_INI_SCAN_DIR` peut être utilisée.
Lorsqu'elle est définie, PHP chargera tous les fichiers avec l'extension `.ini` présents dans les répertoires donnés.
Vous pouvez également modifier la configuration de PHP en utilisant la directive `php_ini` dans le fichier `Caddyfile` :
```caddyfile
{
frankenphp {
php_ini memory_limit 256M
# or
php_ini {
memory_limit 256M
max_execution_time 15
}
}
}
```
## Activer le mode debug
Lors de l'utilisation de l'image Docker, définissez la variable d'environnement `CADDY_GLOBAL_OPTIONS` sur `debug` pour activer le mode debug :
```console
docker run -v $PWD:/app/public \
-e CADDY_GLOBAL_OPTIONS=debug \
-p 80:80 -p 443:443 -p 443:443/udp \
dunglas/frankenphp
```

203
docs/fr/docker.md Normal file
View File

@@ -0,0 +1,203 @@
# Création d'une image Docker personnalisée
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, 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>`
- `<frankenphp-version>` et `<php-version>` sont repsectivement les numéros de version de FrankenPHP et PHP, allant de majeur (e.g. `1`), mineur (e.g. `1.2`) à des versions correctives (e.g. `1.2.3`).
- `<os>` est soit `trixie` (pour Debian Trixie), `bookworm` (pour Debian Bookworm) ou `alpine` (pour la dernière version stable d'Alpine).
[Parcourir les tags](https://hub.docker.com/r/dunglas/frankenphp/tags).
## Comment utiliser les images
Créez un `Dockerfile` dans votre projet :
```dockerfile
FROM dunglas/frankenphp
COPY . /app/public
```
Ensuite, exécutez ces commandes pour construire et exécuter l'image Docker :
```console
docker build -t my-php-app .
docker run -it --rm --name my-running-app my-php-app
```
## Comment installer plus d'extensions PHP
Le script [`docker-php-extension-installer`](https://github.com/mlocati/docker-php-extension-installer) est fourni dans l'image de base.
Il est facile d'ajouter des extensions PHP supplémentaires :
```dockerfile
FROM dunglas/frankenphp
# ajoutez des extensions supplémentaires ici :
RUN install-php-extensions \
pdo_mysql \
gd \
intl \
zip \
opcache
```
## Comment installer plus de modules Caddy
FrankenPHP est construit sur Caddy, et tous les [modules Caddy](https://caddyserver.com/docs/modules/) peuvent être utilisés avec FrankenPHP.
La manière la plus simple d'installer des modules Caddy personnalisés est d'utiliser [xcaddy](https://github.com/caddyserver/xcaddy) :
```dockerfile
FROM dunglas/frankenphp:builder AS builder
# Copier xcaddy dans l'image du constructeur
COPY --from=caddy:builder /usr/bin/xcaddy /usr/bin/xcaddy
# CGO doit être activé pour construire FrankenPHP
RUN CGO_ENABLED=1 \
XCADDY_SETCAP=1 \
XCADDY_GO_BUILD_FLAGS="-ldflags='-w -s' -tags=nobadger,nomysql,nopgx" \
CGO_CFLAGS=$(php-config --includes) \
CGO_LDFLAGS="$(php-config --ldflags) $(php-config --libs)" \
xcaddy build \
--output /usr/local/bin/frankenphp \
--with github.com/dunglas/frankenphp=./ \
--with github.com/dunglas/frankenphp/caddy=./caddy/ \
--with github.com/dunglas/caddy-cbrotli \
# Mercure et Vulcain sont inclus dans la construction officielle, mais n'hésitez pas à les retirer
--with github.com/dunglas/mercure/caddy \
--with github.com/dunglas/vulcain/caddy
# Ajoutez des modules Caddy supplémentaires ici
FROM dunglas/frankenphp AS runner
# Remplacer le binaire officiel par celui contenant vos modules personnalisés
COPY --from=builder /usr/local/bin/frankenphp /usr/local/bin/frankenphp
```
L'image builder fournie par FrankenPHP contient une version compilée de `libphp`.
[Les images builder](https://hub.docker.com/r/dunglas/frankenphp/tags?name=builder) sont fournies pour toutes les versions de FrankenPHP et PHP, à la fois pour Debian et Alpine.
> [!TIP]
>
> Si vous utilisez Alpine Linux et Symfony,
> vous devrez peut-être [augmenter la taille de pile par défaut](compile.md#utiliser-xcaddy).
## Activer le mode Worker par défaut
Définissez la variable d'environnement `FRANKENPHP_CONFIG` pour démarrer FrankenPHP avec un script worker :
```dockerfile
FROM dunglas/frankenphp
# ...
ENV FRANKENPHP_CONFIG="worker ./public/index.php"
```
## Utiliser un volume en développement
Pour développer facilement avec FrankenPHP, montez le répertoire de l'hôte contenant le code source de l'application comme un volume dans le conteneur Docker :
```console
docker run -v $PWD:/app/public -p 80:80 -p 443:443 -p 443:443/udp --tty my-php-app
```
> [!TIP]
>
> L'option --tty permet d'avoir des logs lisibles par un humain au lieu de logs JSON.
Avec Docker Compose :
```yaml
# compose.yaml
services:
php:
image: dunglas/frankenphp
# décommentez la ligne suivante si vous souhaitez utiliser un Dockerfile personnalisé
#build: .
# décommentez la ligne suivante si vous souhaitez exécuter ceci dans un environnement de production
# restart: always
ports:
- "80:80" # HTTP
- "443:443" # HTTPS
- "443:443/udp" # HTTP/3
volumes:
- ./:/app/public
- caddy_data:/data
- caddy_config:/config
# commentez la ligne suivante en production, elle permet d'avoir de beaux logs lisibles en dev
tty: true
# Volumes nécessaires pour les certificats et la configuration de Caddy
volumes:
caddy_data:
caddy_config:
```
## Exécution en tant qu'utilisateur non-root
FrankenPHP peut s'exécuter en tant qu'utilisateur non-root dans Docker.
Voici un exemple de `Dockerfile` le permettant :
```dockerfile
FROM dunglas/frankenphp
ARG USER=appuser
RUN \
# Utilisez "adduser -D ${USER}" pour les distributions basées sur Alpine
useradd ${USER}; \
# Ajouter la capacité supplémentaire de se lier aux ports 80 et 443
setcap CAP_NET_BIND_SERVICE=+eip /usr/local/bin/frankenphp; \
# Donner l'accès en écriture à /data/caddy et /config/caddy
chown -R ${USER}:${USER} /data/caddy && chown -R ${USER}:${USER} /config/caddy
USER ${USER}
```
### Exécution sans capacité
Même lorsqu'il s'exécute en tant qu'utilisateur autre que root, FrankenPHP a besoin de la capacité `CAP_NET_BIND_SERVICE`
pour que son serveur utilise les ports privilégiés (80 et 443).
Si vous exposez FrankenPHP sur un port non privilégié (à partir de 1024), il est possible de faire fonctionner le serveur web avec un utilisateur qui n'est pas root, et sans avoir besoin d'aucune capacité.
```dockerfile
FROM dunglas/frankenphp
ARG USER=appuser
RUN \
# Utiliser "adduser -D ${USER}" pour les distros basées sur Alpine
useradd ${USER}; \
# Supprimer la capacité par défaut \
setcap -r /usr/local/bin/frankenphp; \
# Donner un accès en écriture à /data/caddy et /config/caddy \
chown -R ${USER}:${USER} /data/caddy && chown -R ${USER}:${USER} /config/caddy
USER ${USER}
```
Ensuite, définissez la variable d'environnement `SERVER_NAME` pour utiliser un port non privilégié.
Exemple `:8000`
## Mises à jour
Les images Docker sont construites :
- lorsqu'une nouvelle version est taguée
- tous les jours à 4h UTC, si de nouvelles versions des images officielles PHP sont disponibles
## Versions de développement
Les versions de développement sont disponibles dans le dépôt Docker [`dunglas/frankenphp-dev`](https://hub.docker.com/repository/docker/dunglas/frankenphp-dev). Un nouveau build est déclenché chaque fois qu'un commit est poussé sur la branche principale du dépôt GitHub.
Les tags `latest*` pointent vers la tête de la branche `main`.
Les tags sous la forme `sha-<hash-du-commit-git>` sont également disponibles.

21
docs/fr/early-hints.md Normal file
View File

@@ -0,0 +1,21 @@
# Early Hints
FrankenPHP prend nativement en charge le code de statut [103 Early Hints](https://developer.chrome.com/blog/early-hints/).
L'utilisation des Early Hints peut améliorer le temps de chargement de vos pages web de 30 %.
```php
<?php
header('Link: </style.css>; rel=preload; as=style');
headers_send(103);
// vos algorithmes lents et requêtes SQL 🤪
echo <<<'HTML'
<!DOCTYPE html>
<title>Hello FrankenPHP</title>
<link rel="stylesheet" href="style.css">
HTML;
```
Les Early Hints sont pris en charge à la fois par les modes "standard" et [worker](worker.md).

151
docs/fr/embed.md Normal file
View File

@@ -0,0 +1,151 @@
# Applications PHP en tant que binaires autonomes
FrankenPHP a la capacité d'incorporer le code source et les assets des applications PHP dans un binaire statique et autonome.
Grâce à cette fonctionnalité, les applications PHP peuvent être distribuées en tant que binaires autonomes qui incluent l'application elle-même, l'interpréteur PHP et Caddy, un serveur web de qualité production.
Pour en savoir plus sur cette fonctionnalité, consultez [la présentation faite par Kévin à la SymfonyCon 2023](https://dunglas.dev/2023/12/php-and-symfony-apps-as-standalone-binaries/).
Pour embarquer des applications Laravel, [lisez ce point spécifique de la documentation](laravel.md#les-applications-laravel-en-tant-que-binaires-autonomes).
## Préparer votre application
Avant de créer le binaire autonome, assurez-vous que votre application est prête à être intégrée.
Vous devrez probablement :
- Installer les dépendances de production de l'application
- Dumper l'autoloader
- Activer le mode production de votre application (si disponible)
- Supprimer les fichiers inutiles tels que `.git` ou les tests pour réduire la taille de votre binaire final
Par exemple, pour une application Symfony, lancez les commandes suivantes :
```console
# Exporter le projet pour se débarrasser de .git/, etc.
mkdir $TMPDIR/my-prepared-app
git archive HEAD | tar -x -C $TMPDIR/my-prepared-app
cd $TMPDIR/my-prepared-app
# Définir les variables d'environnement appropriées
echo APP_ENV=prod > .env.local
echo APP_DEBUG=0 >> .env.local
# Supprimer les tests et autres fichiers inutiles pour économiser de l'espace
# Alternativement, ajoutez ces fichiers avec l'attribut export-ignore dans votre fichier .gitattributes
rm -Rf tests/
# Installer les dépendances
composer install --ignore-platform-reqs --no-dev -a
# Optimiser le .env
composer dump-env prod
```
### Personnaliser la configuration
Pour personnaliser [la configuration](config.md),
vous pouvez mettre un fichier `Caddyfile` ainsi qu'un fichier `php.ini`
dans le répertoire principal de l'application à intégrer
(`$TMPDIR/my-prepared-app` dans l'exemple précédent).
## Créer un binaire Linux
La manière la plus simple de créer un binaire Linux est d'utiliser le builder basé sur Docker que nous fournissons.
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-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 . .
WORKDIR /go/src/app/
RUN EMBED=dist/app/ ./build-static.sh
```
> [!CAUTION]
>
> Certains fichiers `.dockerignore` (par exemple celui fourni par défaut par [Symfony Docker](https://github.com/dunglas/symfony-docker/blob/main/.dockerignore))
> empêchent la copie du dossier `vendor/` et des fichiers `.env`. Assurez-vous d'ajuster ou de supprimer le fichier `.dockerignore` avant le build.
2. Construisez:
```console
docker build -t static-app -f static-build.Dockerfile .
```
3. Extrayez le binaire :
```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
```
Le binaire généré sera nommé `my-app` dans le répertoire courant.
## Créer un binaire pour d'autres systèmes d'exploitation
Si vous ne souhaitez pas utiliser Docker, ou souhaitez construire un binaire macOS, utilisez le script shell que nous fournissons :
```console
git clone https://github.com/php/frankenphp
cd frankenphp
EMBED=/path/to/your/app ./build-static.sh
```
Le binaire obtenu est le fichier nommé `frankenphp-<os>-<arch>` dans le répertoire `dist/`.
## Utiliser le binaire
C'est tout ! Le fichier `my-app` (ou `dist/frankenphp-<os>-<arch>` sur d'autres systèmes d'exploitation) contient votre application autonome !
Pour démarrer l'application web, exécutez :
```console
./my-app php-server
```
Si votre application contient un [script worker](worker.md), démarrez le worker avec quelque chose comme :
```console
./my-app php-server --worker public/index.php
```
Pour activer HTTPS (un certificat Let's Encrypt est automatiquement créé), HTTP/2 et HTTP/3, spécifiez le nom de domaine à utiliser :
```console
./my-app php-server --domain localhost
```
Vous pouvez également exécuter les scripts CLI PHP incorporés dans votre binaire :
```console
./my-app php-cli bin/console
```
## Extensions PHP
Par défaut, le script construira les extensions requises par le fichier `composer.json` de votre projet, s'il y en a.
Si le fichier `composer.json` n'existe pas, les extensions par défaut sont construites, comme documenté dans [Créer un binaire statique](static.md).
Pour personnaliser les extensions, utilisez la variable d'environnement `PHP_EXTENSIONS`.
```console
EMBED=/path/to/your/app \
PHP_EXTENSIONS=ctype,iconv,pdo_sqlite \
./build-static.sh
```
## Personnaliser la compilation
[Consultez la documentation sur la compilation statique](static.md) pour voir comment personnaliser le binaire (extensions, version PHP...).
## Distribuer le binaire
Sous Linux, le binaire est compressé par défaut à l'aide de [UPX](https://upx.github.io).
Sous Mac, pour réduire la taille du fichier avant de l'envoyer, vous pouvez le compresser.
Nous recommandons `xz`.

886
docs/fr/extensions.md Normal file
View File

@@ -0,0 +1,886 @@
# Écrire des extensions PHP en Go
Avec FrankenPHP, vous pouvez **écrire des extensions PHP en Go**, ce qui vous permet de créer des **fonctions natives haute performance** qui peuvent être appelées directement depuis PHP. Vos applications peuvent tirer parti de toute bibliothèque Go existante ou nouvelle, ainsi que du célèbre modèle de concurrence des **goroutines directement depuis votre code PHP**.
L'écriture d'extensions PHP se fait généralement en C, mais il est également possible de les écrire dans d'autres langages avec un peu de travail supplémentaire. Les extensions PHP permettent de tirer parti de la puissance des langages de bas niveau pour étendre les fonctionnalités de PHP, par exemple, en ajoutant des fonctions natives ou en optimisant des opérations spécifiques.
Grâce aux modules Caddy, vous pouvez écrire des extensions PHP en Go et les intégrer très rapidement dans FrankenPHP.
## Deux Approches
FrankenPHP offre deux façons de créer des extensions PHP en Go :
1. **Utilisation du Générateur d'Extensions** - L'approche recommandée qui génère tout le code standard nécessaire pour la plupart des cas d'usage, vous permettant de vous concentrer sur l'écriture de votre code Go
2. **Implémentation Manuelle** - Contrôle total sur la structure de l'extension pour les cas d'usage avancés
Nous commencerons par l'approche du générateur, car c'est le moyen le plus facile de commencer, puis nous montrerons l'implémentation manuelle pour ceux qui ont besoin d'un contrôle complet.
## Utilisation du Générateur d'Extensions
FrankenPHP est livré avec un outil qui vous permet de **créer une extension PHP** en utilisant uniquement Go. **Pas besoin d'écrire du code C** ou d'utiliser CGO directement : FrankenPHP inclut également une **API de types publique** pour vous aider à écrire vos extensions en Go sans avoir à vous soucier du **jonglage de types entre PHP/C et Go**.
> [!TIP]
> Si vous voulez comprendre comment les extensions peuvent être écrites en Go à partir de zéro, vous pouvez lire la section d'implémentation manuelle ci-dessous démontrant comment écrire une extension PHP en Go sans utiliser le générateur.
Gardez à l'esprit que cet outil n'est **pas un générateur d'extensions complet**. Il est destiné à vous aider à écrire des extensions simples en Go, mais il ne fournit pas les fonctionnalités les plus avancées des extensions PHP. Si vous devez écrire une extension plus **complexe et optimisée**, vous devrez peut-être écrire du code C ou utiliser CGO directement.
### Prérequis
Comme aussi couvert dans la section d'implémentation manuelle ci-dessous, vous devez [obtenir les sources PHP](https://www.php.net/downloads.php) et créer un nouveau module Go.
#### Créer un Nouveau Module et Obtenir les Sources PHP
La première étape pour écrire une extension PHP en Go est de créer un nouveau module Go. Vous pouvez utiliser la commande suivante pour cela :
```console
go mod init github.com/my-account/my-module
```
La seconde étape est [l'obtention des sources PHP](https://www.php.net/downloads.php) pour les étapes suivantes. Une fois que vous les avez, décompressez-les dans le répertoire de votre choix, mais pas à l'intérieur de votre module Go :
```console
tar xf php-*
```
### Écrire l'Extension
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 (
"strings"
"unsafe"
"github.com/dunglas/frankenphp"
)
//export_php:function repeat_this(string $str, int $count, bool $reverse): string
func repeat_this(s *C.zend_string, count int64, reverse bool) unsafe.Pointer {
str := frankenphp.GoString(unsafe.Pointer(s))
result := strings.Repeat(str, int(count))
if reverse {
runes := []rune(result)
for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
runes[i], runes[j] = runes[j], runes[i]
}
result = string(runes)
}
return frankenphp.PHPString(result, false)
}
```
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.
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.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.
>
> Pour les méthodes de classe spécifiquement, les types primitifs et les tableaux sont supportés. Les objets ne peuvent pas encore être utilisés comme paramètres de méthode ou types de retour.
Si vous vous référez à l'extrait de code de la section précédente, vous pouvez voir que des assistants sont utilisés pour convertir le premier paramètre et la valeur de retour. Les deuxième et troisième paramètres de notre fonction `repeat_this()` n'ont pas besoin d'être convertis car la représentation mémoire des types sous-jacents est la même pour C et Go.
#### Travailler avec les Tableaux
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
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
}
// 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 la conversion de tableaux :**
- **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 : Packed et Associatif
- `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
Age int
}
```
#### Que sont les Classes Opaques ?
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
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.
#### Ajouter des Méthodes aux Classes
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
Age int
}
//export_php:method User::getName(): string
func (us *UserStruct) GetUserName() unsafe.Pointer {
return frankenphp.PHPString(us.Name, false)
}
//export_php:method User::setAge(int $age): void
func (us *UserStruct) SetUserAge(age int64) {
us.Age = int(age)
}
//export_php:method User::getAge(): int
func (us *UserStruct) GetUserAge() int64 {
return int64(us.Age)
}
//export_php:method User::setNamePrefix(string $prefix = "User"): void
func (us *UserStruct) SetNamePrefix(prefix *C.zend_string) {
us.Name = frankenphp.GoString(unsafe.Pointer(prefix)) + ": " + us.Name
}
```
#### Paramètres Nullables
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) {
// Vérifier si name a été fourni (pas null)
if name != nil {
us.Name = frankenphp.GoString(unsafe.Pointer(name))
}
// Vérifier si age a été fourni (pas null)
if age != nil {
us.Age = int(*age)
}
// Vérifier si active a été fourni (pas null)
if active != nil {
us.Active = *active
}
}
```
**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`
> [!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`).
Après avoir généré l'extension, vous serez autorisé à utiliser la classe et ses méthodes en PHP. Notez que vous **ne pouvez pas accéder aux propriétés directement** :
```php
<?php
$user = new User();
// ✅ Fonctionne - utilisation des méthodes
$user->setAge(25);
echo $user->getName(); // Output : (vide, valeur par défaut)
echo $user->getAge(); // Output : 25
$user->setNamePrefix("Employee");
// ✅ Fonctionne aussi - paramètres nullables
$user->updateInfo("John", 30, true); // Tous les paramètres fournis
$user->updateInfo("Jane", null, false); // L'âge est null
$user->updateInfo(null, 25, null); // Le nom et actif sont null
// ❌ Ne fonctionnera PAS - accès direct aux propriétés
// echo $user->name; // Erreur : Impossible d'accéder à la propriété privée
// $user->age = 30; // Erreur : Impossible d'accéder à la propriété privée
```
Cette conception garantit que votre code Go a un contrôle complet sur la façon dont l'état de l'objet est accédé et modifié, fournissant une meilleure encapsulation et sécurité de type.
### 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: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
//export_php:const
const API_VERSION = "1.2.3"
//export_php:const
const STATUS_OK = iota
//export_php:const
const STATUS_ERROR = iota
```
#### Constantes de Classe
Utilisez la directive `//export_php:classconst ClassName` pour créer des constantes qui appartiennent à une classe PHP spécifique :
```go
package example
//export_php:classconst User
const STATUS_ACTIVE = 1
//export_php:classconst User
const STATUS_INACTIVE = 0
//export_php:classconst User
const ROLE_ADMIN = "admin"
//export_php:classconst Order
const STATE_PENDING = iota
//export_php:classconst Order
const STATE_PROCESSING = iota
//export_php:classconst Order
const STATE_COMPLETED = iota
```
Les constantes de classe sont accessibles en utilisant la portée du nom de classe en PHP :
```php
<?php
// Constantes globales
echo MAX_CONNECTIONS; // 100
echo API_VERSION; // "1.2.3"
// Constantes de classe
echo User::STATUS_ACTIVE; // 1
echo User::ROLE_ADMIN; // "admin"
echo Order::STATE_PENDING; // 0
```
La directive prend en charge divers types de valeurs incluant les chaînes, entiers, booléens, flottants et constantes iota. Lors de l'utilisation de `iota`, le générateur assigne automatiquement des valeurs séquentielles (0, 1, 2, etc.). Les constantes globales deviennent disponibles dans votre code PHP comme constantes globales, tandis que les constantes de classe sont déclarées dans leurs classes respectives avec la visibilité publique. Lors de l'utilisation d'entiers, différentes notations possibles (binaire, hex, octale) sont supportées et dumpées telles quelles dans le fichier stub PHP.
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 (
"strings"
"unsafe"
"github.com/dunglas/frankenphp"
)
//export_php:const
const STR_REVERSE = iota
//export_php:const
const STR_NORMAL = iota
//export_php:classconst StringProcessor
const MODE_LOWERCASE = 1
//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))
result := strings.Repeat(str, int(count))
if mode == STR_REVERSE {
// inverser la chaîne
}
if mode == STR_NORMAL {
// no-op, juste pour montrer la constante
}
return frankenphp.PHPString(result, false)
}
//export_php:class StringProcessor
type StringProcessorStruct struct {
// champs internes
}
//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)
}
```
### Utilisation des Espaces de Noms
Le générateur prend en charge l'organisation des fonctions, classes et constantes de votre extension PHP sous un espace de noms (namespace) en utilisant la directive `//export_php:namespace`. Cela aide à éviter les conflits de noms et fournit une meilleure organisation pour l'API de votre extension.
#### Déclarer un Espace de Noms
Utilisez la directive `//export_php:namespace` en haut de votre fichier Go pour placer tous les symboles exportés sous un espace de noms spécifique :
```go
//export_php:namespace My\Extension
package example
import (
"unsafe"
"github.com/dunglas/frankenphp"
)
//export_php:function hello(): string
func hello() string {
return "Bonjour depuis l'espace de noms My\\Extension !"
}
//export_php:class User
type UserStruct struct {
// champs internes
}
//export_php:method User::getName(): string
func (u *UserStruct) GetName() unsafe.Pointer {
return frankenphp.PHPString("Jean Dupont", false)
}
//export_php:const
const STATUS_ACTIVE = 1
```
#### Utilisation de l'Extension avec Espace de Noms en PHP
Quand un espace de noms est déclaré, toutes les fonctions, classes et constantes sont placées sous cet espace de noms en PHP :
```php
<?php
echo My\Extension\hello(); // "Bonjour depuis l'espace de noms My\Extension !"
$user = new My\Extension\User();
echo $user->getName(); // "Jean Dupont"
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.
### 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
```
> [!NOTE]
> N'oubliez pas de définir la variable d'environnement `GEN_STUB_SCRIPT` sur le chemin du fichier `gen_stub.php` dans les sources PHP que vous avez téléchargées plus tôt. C'est le même script `gen_stub.php` mentionné dans la section d'implémentation manuelle.
Si tout s'est bien passé, un nouveau répertoire nommé `build` devrait avoir été créé. Ce répertoire contient les fichiers générés pour votre extension, incluant le fichier `my_extension.go` avec les stubs de fonction PHP générés.
### Intégrer l'Extension Générée dans FrankenPHP
Notre extension est maintenant prête à être compilée et intégrée dans FrankenPHP. Pour ce faire, référez-vous à la [documentation de compilation](compile.md) de FrankenPHP pour apprendre comment compiler FrankenPHP. Ajoutez le module en utilisant le flag `--with`, pointant vers le chemin de votre module :
```console
CGO_ENABLED=1 \
XCADDY_GO_BUILD_FLAGS="-ldflags='-w -s' -tags=nobadger,nomysql,nopgx" \
CGO_CFLAGS=$(php-config --includes) \
CGO_LDFLAGS="$(php-config --ldflags) $(php-config --libs)" \
xcaddy build \
--output frankenphp \
--with github.com/my-account/my-module/build
```
Notez que vous pointez vers le sous-répertoire `/build` qui a été créé pendant l'étape de génération. Cependant, ce n'est pas obligatoire : vous pouvez aussi copier les fichiers générés dans le répertoire de votre module et pointer directement vers lui.
### Tester Votre Extension Générée
Vous pouvez créer un fichier PHP pour tester les fonctions et classes que vous avez créées. Par exemple, créez un fichier `index.php` avec le contenu suivant :
```php
<?php
// Utilisation des constantes globales
var_dump(repeat_this('Hello World', 5, STR_REVERSE));
// Utilisation des constantes de classe
$processor = new StringProcessor();
echo $processor->process('Hello World', StringProcessor::MODE_LOWERCASE); // "hello world"
echo $processor->process('Hello World', StringProcessor::MODE_UPPERCASE); // "HELLO WORLD"
```
Une fois que vous avez intégré votre extension dans FrankenPHP comme indiqué dans la section précédente, vous pouvez exécuter ce fichier de test en utilisant `./frankenphp php-server`, et vous devriez voir votre extension fonctionner.
## Implémentation Manuelle
Si vous voulez comprendre comment les extensions fonctionnent ou avez besoin d'un contrôle total sur votre extension, vous pouvez les écrire manuellement. Cette approche vous donne un contrôle complet mais nécessite plus de code intermédiaire.
### Fonction de Base
Nous allons voir comment écrire une extension PHP simple en Go qui définit une nouvelle fonction native. Cette fonction sera appelée depuis PHP et déclenchera une goroutine qui enregistrera un message dans les logs de Caddy. Cette fonction ne prend aucun paramètre et ne retourne rien.
#### Définir la Fonction Go
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 example
// #include "extension.h"
import "C"
import (
"log/slog"
"unsafe"
"github.com/dunglas/frankenphp"
)
func init() {
frankenphp.RegisterExtension(unsafe.Pointer(&C.ext_module_entry))
}
//export go_print_something
func go_print_something() {
go func() {
slog.Info("Hello from a goroutine!")
}()
}
```
La fonction `frankenphp.RegisterExtension()` simplifie le processus d'enregistrement d'extension en gérant la logique interne de PHP. La fonction `go_print_something` utilise la directive `//export` pour indiquer qu'elle sera accessible dans le code C que nous écrirons, grâce à CGO.
Dans cet exemple, notre nouvelle fonction déclenchera une goroutine qui enregistrera un message dans les logs de Caddy.
#### Définir la Fonction PHP
Pour permettre à PHP d'appeler notre fonction, nous devons définir une fonction PHP correspondante. Pour cela, nous créerons un fichier stub, par exemple, `extension.stub.php`, qui contiendra le code suivant :
```php
<?php
/** @generate-class-entries */
function go_print(): void {}
```
Ce fichier définit la signature de la fonction `go_print()`, qui sera appelée depuis PHP. La directive `@generate-class-entries` permet à PHP de générer automatiquement les entrées de fonction pour notre extension.
Ceci n'est pas fait manuellement mais en utilisant un script fourni dans les sources PHP (assurez-vous d'ajuster le chemin vers le script `gen_stub.php` selon l'emplacement de vos sources PHP) :
```bash
php ../php-src/build/gen_stub.php extension.stub.php
```
Ce script générera un fichier nommé `extension_arginfo.h` qui contient les informations nécessaires pour que PHP sache comment définir et appeler notre fonction.
#### Écrire le Pont entre Go et C
Maintenant, nous devons écrire le pont entre Go et C. Créez un fichier nommé `extension.h` dans le répertoire de votre module avec le contenu suivant :
```c
#ifndef _EXTENSION_H
#define _EXTENSION_H
#include <php.h>
extern zend_module_entry ext_module_entry;
#endif
```
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.
Commençons par inclure les en-têtes requis :
```c
#include <php.h>
#include "extension.h"
#include "extension_arginfo.h"
// Contient les symboles exportés par Go
#include "_cgo_export.h"
```
Nous définissons ensuite notre fonction PHP comme une fonction de langage natif :
```c
PHP_FUNCTION(go_print)
{
ZEND_PARSE_PARAMETERS_NONE();
go_print_something();
}
zend_module_entry ext_module_entry = {
STANDARD_MODULE_HEADER,
"ext_go",
ext_functions, /* Functions */
NULL, /* MINIT */
NULL, /* MSHUTDOWN */
NULL, /* RINIT */
NULL, /* RSHUTDOWN */
NULL, /* MINFO */
"0.1.1",
STANDARD_MODULE_PROPERTIES
};
```
Dans ce cas, notre fonction ne prend aucun paramètre et ne retourne rien. Elle appelle simplement la fonction Go que nous avons définie plus tôt, exportée en utilisant la directive `//export`.
Enfin, nous définissons les métadonnées de l'extension dans une structure `zend_module_entry`, telles que son nom, sa version et ses propriétés. Cette information est nécessaire pour que PHP reconnaisse et charge notre extension. Notez que `ext_functions` est un tableau de pointeurs vers les fonctions PHP que nous avons définies, et il a été automatiquement généré par le script `gen_stub.php` dans le fichier `extension_arginfo.h`.
L'enregistrement de l'extension est automatiquement géré par la fonction `RegisterExtension()` de FrankenPHP que nous appelons dans notre code Go.
### Usage Avancé
Maintenant que nous savons comment créer une extension PHP de base en Go, complexifions notre exemple. Nous allons maintenant créer une fonction PHP qui prend une chaîne comme paramètre et retourne sa version en majuscules.
#### Définir le Stub de Fonction PHP
Pour définir la nouvelle fonction PHP, nous modifierons notre fichier `extension.stub.php` pour inclure la nouvelle signature de fonction :
```php
<?php
/** @generate-class-entries */
/**
* Convertit une chaîne en majuscules.
*
* @param string $string La chaîne à convertir.
* @return string La version en majuscules de la chaîne.
*/
function go_upper(string $string): string {}
```
> [!TIP]
> Ne négligez pas la documentation de vos fonctions ! Vous êtes susceptible de partager vos stubs d'extension avec d'autres développeurs pour documenter comment utiliser votre extension et quelles fonctionnalités sont disponibles.
En régénérant le fichier stub avec le script `gen_stub.php`, le fichier `extension_arginfo.h` devrait ressembler à ceci :
```c
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_go_upper, 0, 1, IS_STRING, 0)
ZEND_ARG_TYPE_INFO(0, string, IS_STRING, 0)
ZEND_END_ARG_INFO()
ZEND_FUNCTION(go_upper);
static const zend_function_entry ext_functions[] = {
ZEND_FE(go_upper, arginfo_go_upper)
ZEND_FE_END
};
```
Nous pouvons voir que la fonction `go_upper` est définie avec un paramètre de type `string` et un type de retour `string`.
#### Jonglerie de Types entre Go et PHP/C
Votre fonction Go ne peut pas accepter directement une chaîne PHP comme paramètre. Vous devez la convertir en chaîne Go. Heureusement, FrankenPHP fournit des fonctions d'aide pour gérer la conversion entre les chaînes PHP et les chaînes Go, similaire à ce que nous avons vu dans l'approche du générateur.
Le fichier d'en-tête reste simple :
```c
#ifndef _EXTENSION_H
#define _EXTENSION_H
#include <php.h>
extern zend_module_entry ext_module_entry;
#endif
```
Nous pouvons maintenant écrire le pont entre Go et C dans notre fichier `extension.c`. Nous passerons la chaîne PHP directement à notre fonction Go :
```c
PHP_FUNCTION(go_upper)
{
zend_string *str;
ZEND_PARSE_PARAMETERS_START(1, 1)
Z_PARAM_STR(str)
ZEND_PARSE_PARAMETERS_END();
zend_string *result = go_upper(str);
RETVAL_STR(result);
}
```
Vous pouvez en apprendre plus sur `ZEND_PARSE_PARAMETERS_START` et l'analyse des paramètres dans la page dédiée du [PHP Internals Book](https://www.phpinternalsbook.com/php7/extensions_design/php_functions.html#parsing-parameters-zend-parse-parameters). Ici, nous disons à PHP que notre fonction prend un paramètre obligatoire de type `string` comme `zend_string`. Nous passons ensuite cette chaîne directement à notre fonction Go et retournons le résultat en utilisant `RETVAL_STR`.
Il ne reste qu'une chose à faire : implémenter la fonction `go_upper` en Go.
#### Implémenter la Fonction 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
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))
}
```
Cette approche est beaucoup plus propre et sûre que la gestion manuelle de la mémoire. Les fonctions d'aide de FrankenPHP gèrent la conversion entre le format `zend_string` de PHP et les chaînes Go automatiquement. Le paramètre `false` dans `PHPString()` indique que nous voulons créer une nouvelle chaîne non persistante (libérée à la fin de la requête).
> [!TIP]
> Dans cet exemple, nous n'effectuons aucune gestion d'erreur, mais vous devriez toujours vérifier que les pointeurs ne sont pas `nil` et que les données sont valides avant de les utiliser dans vos fonctions Go.
### Intégrer l'Extension dans FrankenPHP
Notre extension est maintenant prête à être compilée et intégrée dans FrankenPHP. Pour ce faire, référez-vous à la [documentation de compilation](compile.md) de FrankenPHP pour apprendre comment compiler FrankenPHP. Ajoutez le module en utilisant le flag `--with`, pointant vers le chemin de votre module :
```console
CGO_ENABLED=1 \
XCADDY_GO_BUILD_FLAGS="-ldflags='-w -s' -tags=nobadger,nomysql,nopgx" \
CGO_CFLAGS=$(php-config --includes) \
CGO_LDFLAGS="$(php-config --ldflags) $(php-config --libs)" \
xcaddy build \
--output frankenphp \
--with github.com/my-account/my-module
```
C'est tout ! Votre extension est maintenant intégrée dans FrankenPHP et peut être utilisée dans votre code PHP.
### Tester Votre Extension
Après avoir intégré votre extension dans FrankenPHP, vous pouvez créer un fichier `index.php` avec des exemples pour les fonctions que vous avez implémentées :
```php
<?php
// Tester la fonction de base
go_print();
// Tester la fonction avancée
echo go_upper("hello world") . "\n";
```
Vous pouvez maintenant exécuter FrankenPHP avec ce fichier en utilisant `./frankenphp php-server`, et vous devriez voir votre extension fonctionner.

31
docs/fr/github-actions.md Normal file
View File

@@ -0,0 +1,31 @@
# Utilisation de GitHub Actions
Ce dépôt construit et déploie l'image Docker sur [le Hub Docker](https://hub.docker.com/r/dunglas/frankenphp) pour
chaque pull request approuvée ou sur votre propre fork une fois configuré.
## Configuration de GitHub Actions
Dans les paramètres du dépôt, sous "secrets", ajoutez les secrets suivants :
- `REGISTRY_LOGIN_SERVER` : Le registre Docker à utiliser (par exemple, `docker.io`).
- `REGISTRY_USERNAME` : Le nom d'utilisateur à utiliser pour se connecter au registre (par exemple, `dunglas`).
- `REGISTRY_PASSWORD` : Le mot de passe à utiliser pour se connecter au registre (par exemple, une clé d'accès).
- `IMAGE_NAME` : Le nom de l'image (par exemple, `dunglas/frankenphp`).
## Construction et push de l'image
1. Créez une Pull Request ou poussez vers votre fork.
2. GitHub Actions va construire l'image et exécuter tous les tests.
3. Si la construction est réussie, l'image sera poussée vers le registre en utilisant le tag `pr-x`, où `x` est le numéro de la PR.
## Déploiement de l'image
1. Une fois la Pull Request fusionnée, GitHub Actions exécutera à nouveau les tests et construira une nouvelle image.
2. Si la construction est réussie, le tag `main` sera mis à jour dans le registre Docker.
## Releases
1. Créez un nouveau tag dans le dépôt.
2. GitHub Actions va construire l'image et exécuter tous les tests.
3. Si la compilation est réussie, l'image sera poussée vers le registre en utilisant le nom du tag comme tag (par exemple, `v1.2.3` et `v1.2` seront créés).
4. Le tag `latest` sera également mis à jour.

143
docs/fr/known-issues.md Normal file
View File

@@ -0,0 +1,143 @@
# Problèmes Connus
## Extensions PHP non prises en charge
Les extensions suivantes sont connues pour ne pas être compatibles avec FrankenPHP :
| Nom | Raison | Alternatives |
| ----------------------------------------------------------------------------------------------------------- | --------------- | -------------------------------------------------------------------------------------------------------------------- |
| [imap](https://www.php.net/manual/en/imap.installation.php) | Non thread-safe | [javanile/php-imap2](https://github.com/javanile/php-imap2), [webklex/php-imap](https://github.com/Webklex/php-imap) |
| [newrelic](https://docs.newrelic.com/docs/apm/agents/php-agent/getting-started/introduction-new-relic-php/) | Non thread-safe | - |
## Extensions PHP boguées
Les extensions suivantes ont des bugs connus ou des comportements inattendus lorsqu'elles sont utilisées avec FrankenPHP :
| Nom | Problème |
| ------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [ext-openssl](https://www.php.net/manual/fr/book.openssl.php) | Lors de l'utilisation d'une version statique de FrankenPHP (construite avec la libc musl), l'extension OpenSSL peut planter sous de fortes charges. Une solution consiste à utiliser une version liée dynamiquement (comme celle utilisée dans les images Docker). Ce bogue est [suivi par PHP](https://github.com/php/php-src/issues/13648). |
## get_browser
La fonction [get_browser()](https://www.php.net/manual/fr/function.get-browser.php) semble avoir de mauvaises performances après un certain temps. Une solution est de mettre en cache (par exemple, avec [APCu](https://www.php.net/manual/en/book.apcu.php)) les résultats par agent utilisateur, car ils sont statiques.
## Binaire autonome et images Docker basées sur Alpine
Le binaire autonome et les images Docker basées sur Alpine (`dunglas/frankenphp:*-alpine`) utilisent [musl libc](https://musl.libc.org/) au lieu de [glibc et ses amis](https://www.etalabs.net/compare_libcs.html), pour garder une taille de binaire plus petite. Cela peut entraîner des problèmes de compatibilité. En particulier, le drapeau glob `GLOB_BRACE` n'est [pas disponible](https://www.php.net/manual/fr/function.glob.php).
## Utilisation de `https://127.0.0.1` avec Docker
Par défaut, FrankenPHP génère un certificat TLS pour `localhost`.
C'est l'option la plus simple et recommandée pour le développement local.
Si vous voulez vraiment utiliser `127.0.0.1` comme hôte, il est possible de configurer FrankenPHP pour générer un certificat pour cela en définissant le nom du serveur à `127.0.0.1`.
Malheureusement, cela ne suffit pas lors de l'utilisation de Docker à cause de [son système de gestion des réseaux](https://docs.docker.com/network/).
Vous obtiendrez une erreur TLS similaire à `curl: (35) LibreSSL/3.3.6: error:1404B438:SSL routines:ST_CONNECT:tlsv1 alert internal error`.
Si vous utilisez Linux, une solution est d'utiliser [le pilote de réseau "hôte"](https://docs.docker.com/network/network-tutorial-host/) :
```console
docker run \
-e SERVER_NAME="127.0.0.1" \
-v $PWD:/app/public \
--network host \
dunglas/frankenphp
```
Le pilote de réseau "hôte" n'est pas pris en charge sur Mac et Windows. Sur ces plateformes, vous devrez deviner l'adresse IP du conteneur et l'inclure dans les noms de serveur.
Exécutez la commande `docker network inspect bridge` et inpectez la clef `Containers` pour identifier la dernière adresse IP attribuée sous la clef `IPv4Address`, puis incrémentez-la d'un. Si aucun conteneur n'est en cours d'exécution, la première adresse IP attribuée est généralement `172.17.0.2`.
Ensuite, incluez ceci dans la variable d'environnement `SERVER_NAME` :
```console
docker run \
-e SERVER_NAME="127.0.0.1, 172.17.0.3" \
-v $PWD:/app/public \
-p 80:80 -p 443:443 -p 443:443/udp \
dunglas/frankenphp
```
> [!CAUTION]
>
> Assurez-vous de remplacer `172.17.0.3` par l'IP qui sera attribuée à votre conteneur.
Vous devriez maintenant pouvoir accéder à `https://127.0.0.1` depuis la machine hôte.
Si ce n'est pas le cas, lancez FrankenPHP en mode debug pour essayer de comprendre le problème :
```console
docker run \
-e CADDY_GLOBAL_OPTIONS="debug" \
-e SERVER_NAME="127.0.0.1" \
-v $PWD:/app/public \
-p 80:80 -p 443:443 -p 443:443/udp \
dunglas/frankenphp
```
## Scripts Composer Faisant Références à `@php`
Les [scripts Composer](https://getcomposer.org/doc/articles/scripts.md) peuvent vouloir exécuter un binaire PHP pour certaines tâches, par exemple dans [un projet Laravel](laravel.md) pour exécuter `@php artisan package:discover --ansi`. Cela [echoue actuellement](https://github.com/php/frankenphp/issues/483#issuecomment-1899890915) pour deux raisons :
- Composer ne sait pas comment appeler le binaire FrankenPHP ;
- Composer peut ajouter des paramètres PHP en utilisant le paramètre `-d` dans la commande, ce que FrankenPHP ne supporte pas encore.
Comme solution de contournement, nous pouvons créer un script shell dans `/usr/local/bin/php` qui supprime les paramètres non supportés et appelle ensuite FrankenPHP :
```bash
#!/usr/bin/env bash
args=("$@")
index=0
for i in "$@"
do
if [ "$i" == "-d" ]; then
unset 'args[$index]'
unset 'args[$index+1]'
fi
index=$((index+1))
done
/usr/local/bin/frankenphp php-cli ${args[@]}
```
Ensuite, mettez la variable d'environnement `PHP_BINARY` au chemin de notre script `php` et lancez Composer :
```console
export PHP_BINARY=/usr/local/bin/php
composer install
```
## Résolution des problèmes TLS/SSL avec les binaires statiques
Lorsque vous utilisez les binaires statiques, vous pouvez rencontrer les erreurs suivantes liées à TLS, par exemple lors de l'envoi de courriels utilisant STARTTLS :
```text
Unable to connect with STARTTLS: stream_socket_enable_crypto(): SSL operation failed with code 5. OpenSSL Error messages:
error:80000002:system library::No such file or directory
error:80000002:system library::No such file or directory
error:80000002:system library::No such file or directory
error:0A000086:SSL routines::certificate verify failed
```
Comme le binaire statique ne contient pas de certificats TLS, vous devez indiquer à OpenSSL l'installation de vos certificats CA locaux.
Inspectez la sortie de [`openssl_get_cert_locations()`](https://www.php.net/manual/en/function.openssl-get-cert-locations.php),
pour trouver l'endroit où les certificats CA doivent être installés et stockez-les à cet endroit.
> [!WARNING]
>
> Les contextes Web et CLI peuvent avoir des paramètres différents.
> Assurez-vous d'exécuter `openssl_get_cert_locations()` dans le bon contexte.
[Les certificats CA extraits de Mozilla peuvent être téléchargés sur le site de cURL](https://curl.se/docs/caextract.html).
Alternativement, de nombreuses distributions, y compris Debian, Ubuntu, et Alpine fournissent des paquets nommés `ca-certificates` qui contiennent ces certificats.
Il est également possible d'utiliser `SSL_CERT_FILE` et `SSL_CERT_DIR` pour indiquer à OpenSSL où chercher les certificats CA :
```console
# Définir les variables d'environnement des certificats TLS
export SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt
export SSL_CERT_DIR=/etc/ssl/certs
```

185
docs/fr/laravel.md Normal file
View File

@@ -0,0 +1,185 @@
# Laravel
## Docker
Déployer une application web [Laravel](https://laravel.com) avec FrankenPHP est très facile. Il suffit de monter le projet dans le répertoire `/app` de l'image Docker officielle.
Exécutez cette commande depuis le répertoire principal de votre application Laravel :
```console
docker run -p 80:80 -p 443:443 -p 443:443/udp -v $PWD:/app dunglas/frankenphp
```
Et profitez !
## Installation Locale
Vous pouvez également exécuter vos projets Laravel avec FrankenPHP depuis votre machine locale :
1. [Téléchargez le binaire correspondant à votre système](README.md#binaire-autonome)
2. Ajoutez la configuration suivante dans un fichier nommé `Caddyfile` placé dans le répertoire racine de votre projet Laravel :
```caddyfile
{
frankenphp
}
# Le nom de domaine de votre serveur
localhost {
# Définir le répertoire racine sur le dossier public/
root public/
# Autoriser la compression (optionnel)
encode zstd br gzip
# Exécuter les scripts PHP du dossier public/ et servir les assets
php_server {
try_files {path} index.php
}
}
```
3. Démarrez FrankenPHP depuis le répertoire racine de votre projet Laravel : `frankenphp run`
## Laravel Octane
Octane peut être installé via le gestionnaire de paquets Composer :
```console
composer require laravel/octane
```
Après avoir installé Octane, vous pouvez exécuter la commande Artisan `octane:install`, qui installera le fichier de configuration d'Octane dans votre application :
```console
php artisan octane:install --server=frankenphp
```
Le serveur Octane peut être démarré via la commande Artisan `octane:frankenphp`.
```console
php artisan octane:frankenphp
```
La commande `octane:frankenphp` peut prendre les options suivantes :
- `--host` : L'adresse IP à laquelle le serveur doit se lier (par défaut : `127.0.0.1`)
- `--port` : Le port sur lequel le serveur doit être disponible (par défaut : `8000`)
- `--admin-port` : Le port sur lequel le serveur administratif doit être disponible (par défaut : `2019`)
- `--workers` : Le nombre de workers qui doivent être disponibles pour traiter les requêtes (par défaut : `auto`)
- `--max-requests` : Le nombre de requêtes à traiter avant de recharger le serveur (par défaut : `500`)
- `--caddyfile` : Le chemin vers le fichier `Caddyfile` de FrankenPHP
- `--https` : Activer HTTPS, HTTP/2, et HTTP/3, et générer automatiquement et renouveler les certificats
- `--http-redirect` : Activer la redirection HTTP vers HTTPS (uniquement activé si --https est passé)
- `--watch` : Recharger automatiquement le serveur lorsque l'application est modifiée
- `--poll` : Utiliser le sondage du système de fichiers pendant la surveillance pour surveiller les fichiers sur un réseau
- `--log-level` : Enregistrer les messages au niveau de journalisation spécifié ou au-dessus, en utilisant le logger natif de Caddy
> [!TIP]
> Pour obtenir des logs structurés en JSON logs (utile quand vous utilisez des solutions d'analyse de logs), passez explicitement l'option `--log-level`.
En savoir plus sur Laravel Octane [dans sa documentation officielle](https://laravel.com/docs/octane).
## Les Applications Laravel En Tant Que Binaires Autonomes
En utilisant la [fonctionnalité d'intégration d'applications de FrankenPHP](embed.md), il est possible de distribuer
les applications Laravel sous forme de binaires autonomes.
Suivez ces étapes pour empaqueter votre application Laravel en tant que binaire autonome pour Linux :
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-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
COPY . .
# Supprimez les tests et autres fichiers inutiles pour gagner de la place
# Alternativement, ajoutez ces fichiers à un fichier .dockerignore
RUN rm -Rf tests/
# Copiez le fichier .env
RUN cp .env.example .env
# Modifier APP_ENV et APP_DEBUG pour qu'ils soient prêts pour la production
RUN sed -i'' -e 's/^APP_ENV=.*/APP_ENV=production/' -e 's/^APP_DEBUG=.*/APP_DEBUG=false/' .env
# Apportez d'autres modifications à votre fichier .env si nécessaire
# Installez les dépendances
RUN composer install --ignore-platform-reqs --no-dev -a
# Construire le binaire statique
WORKDIR /go/src/app/
RUN EMBED=dist/app/ ./build-static.sh
```
> [!CAUTION]
>
> Certains fichiers `.dockerignore` ignoreront le répertoire `vendor/`
> et les fichiers `.env`. Assurez-vous d'ajuster ou de supprimer le fichier `.dockerignore` avant la construction.
2. Build:
```console
docker build -t static-laravel-app -f static-build.Dockerfile .
```
3. Extraire le binaire
```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. Remplir les caches :
```console
frankenphp php-cli artisan optimize
```
5. Exécutez les migrations de base de données (s'il y en a) :
```console
frankenphp php-cli artisan migrate
```
6. Générer la clé secrète de l'application :
```console
frankenphp php-cli artisan key:generate
```
7. Démarrez le serveur:
```console
frankenphp php-server
```
Votre application est maintenant prête !
Pour en savoir plus sur les options disponibles et sur la construction de binaires pour d'autres systèmes d'exploitation,
consultez la documentation [Applications PHP en tant que binaires autonomes](embed.md).
### Changer le chemin de stockage
Par défaut, Laravel stocke les fichiers téléchargés, les caches, les logs, etc. dans le répertoire `storage/` de l'application.
Ceci n'est pas adapté aux applications embarquées, car chaque nouvelle version sera extraite dans un répertoire temporaire différent.
Définissez la variable d'environnement `LARAVEL_STORAGE_PATH` (par exemple, dans votre fichier `.env`) ou appelez la méthode `Illuminate\Foundation\Application::useStoragePath()` pour utiliser un répertoire en dehors du répertoire temporaire.
### Exécuter Octane avec des binaires autonomes
Il est même possible d'empaqueter les applications Laravel Octane en tant que binaires autonomes !
Pour ce faire, [installez Octane correctement](#laravel-octane) et suivez les étapes décrites dans [la section précédente](#les-applications-laravel-en-tant-que-binaires-autonomes).
Ensuite, pour démarrer FrankenPHP en mode worker via Octane, exécutez :
```console
PATH="$PWD:$PATH" frankenphp php-cli artisan octane:frankenphp
```
> [!CAUTION]
>
> Pour que la commande fonctionne, le binaire autonome **doit** être nommé `frankenphp`
> car Octane a besoin d'un programme nommé `frankenphp` disponible dans le chemin

12
docs/fr/mercure.md Normal file
View File

@@ -0,0 +1,12 @@
# Temps Réel
FrankenPHP est livré avec un hub [Mercure](https://mercure.rocks) intégré.
Mercure permet de pousser des événements en temps réel vers tous les appareils connectés : ils recevront un événement JavaScript instantanément.
Aucune bibliothèque JS ou SDK requis !
![Mercure](../mercure-hub.png)
Pour activer le hub Mercure, mettez à jour le `Caddyfile` comme décrit [sur le site de Mercure](https://mercure.rocks/docs/hub/config).
Pour pousser des mises à jour Mercure depuis votre code, nous recommandons le [Composant Mercure de Symfony](https://symfony.com/components/Mercure) (vous n'avez pas besoin du framework full stack Symfony pour l'utiliser).

17
docs/fr/metrics.md Normal file
View File

@@ -0,0 +1,17 @@
# Métriques
Lorsque les [métriques Caddy](https://caddyserver.com/docs/metrics) sont activées, FrankenPHP expose les métriques suivantes :
- `frankenphp_total_threads` : Le nombre total de threads PHP.
- `frankenphp_busy_threads` : Le nombre de threads PHP en cours de traitement d'une requête (les workers en cours d'exécution consomment toujours un thread).
- `frankenphp_queue_depth` : Le nombre de requêtes régulières en file d'attente
- `frankenphp_total_workers{worker=« [nom_du_worker] »}` : Le nombre total de workers.
- `frankenphp_busy_workers{worker=« [nom_du_worker] »}` : Le nombre de workers qui traitent actuellement une requête.
- `frankenphp_worker_request_time{worker=« [nom_du_worker] »}` : Le temps passé à traiter les requêtes par tous les workers.
- `frankenphp_worker_request_count{worker=« [nom_du_worker] »}` : Le nombre de requêtes traitées par tous les workers.
- `frankenphp_ready_workers{worker=« [nom_du_worker] »}` : Le nombre de workers qui ont appelé `frankenphp_handle_request` au moins une fois.
- `frankenphp_worker_crashes{worker=« [nom_du_worker] »}` : Le nombre de fois où un worker s'est arrêté de manière inattendue.
- `frankenphp_worker_restarts{worker=« [nom_du_worker] »}` : Le nombre de fois où un worker a été délibérément redémarré.
- `frankenphp_worker_queue_depth{worker=« [nom_du_worker] »}` : Le nombre de requêtes en file d'attente.
Pour les métriques de worker, le placeholder `[nom_du_worker]` est remplacé par le nom du worker dans le Caddyfile, sinon le chemin absolu du fichier du worker sera utilisé.

157
docs/fr/performance.md Normal file
View File

@@ -0,0 +1,157 @@
# Performance
Par défaut, FrankenPHP essaie d'offrir un bon compromis entre performance et facilité d'utilisation.
Cependant, il est possible d'améliorer considérablement les performances en utilisant une configuration appropriée.
## Nombre de threads et de workers
Par défaut, FrankenPHP démarre deux fois plus de threads et de workers (en mode worker) que le nombre de CPU disponibles.
Les valeurs appropriées dépendent fortement de la manière dont votre application est écrite, de ce qu'elle fait et de votre matériel.
Nous recommandons vivement de modifier ces valeurs.
Pour trouver les bonnes valeurs, il est souhaitable d'effectuer des tests de charge simulant le trafic réel.
[k6](https://k6.io) et [Gatling](https://gatling.io) sont de bons outils pour cela.
Pour configurer le nombre de threads, utilisez l'option `num_threads` des directives `php_server` et `php`.
Pour changer le nombre de travailleurs, utilisez l'option `num` de la section `worker` de la directive `frankenphp`.
### `max_threads`
Bien qu'il soit toujours préférable de savoir exactement à quoi ressemblera votre trafic, les applications réelles
ont tendance à être plus imprévisibles. Le paramètre `max_threads` permet à FrankenPHP de créer automatiquement des threads supplémentaires au moment de l'exécution, jusqu'à la limite spécifiée.
`max_threads` peut vous aider à déterminer le nombre de threads dont vous avez besoin pour gérer votre trafic et peut rendre le serveur plus résistant aux pics de latence.
Si elle est fixée à `auto`, la limite sera estimée en fonction de la valeur de `memory_limit` dans votre `php.ini`. Si ce n'est pas possible,
`auto` prendra par défaut 2x `num_threads`. Gardez à l'esprit que `auto` peut fortement sous-estimer le nombre de threads nécessaires.
`max_threads` est similaire à [pm.max_children](https://www.php.net/manual/en/install.fpm.configuration.php#pm.max-children) de PHP FPM. La principale différence est que FrankenPHP utilise des threads au lieu de
processus et les délègue automatiquement à différents scripts de travail et au `mode classique` selon les besoins.
## Mode worker
Activer [le mode worker](worker.md) améliore considérablement les performances,
mais votre application doit être adaptée pour être compatible avec ce mode :
vous devez créer un script worker et vous assurer que l'application n'a pas de fuite de mémoire.
## Ne pas utiliser musl
Les binaires statiques que nous fournissons, ainsi que la variante Alpine Linux des images Docker officielles, utilisent [la bibliothèque musl](https://musl.libc.org).
PHP est connu pour être [significativement plus lent](https://gitlab.alpinelinux.org/alpine/aports/-/issues/14381) lorsqu'il utilise cette bibliothèque C alternative au lieu de la bibliothèque GNU traditionnelle,
surtout lorsqu'il est compilé en mode ZTS (_thread-safe_), ce qui est nécessaire pour FrankenPHP.
En outre, [certains bogues ne se produisent que lors de l'utilisation de musl](https://github.com/php/php-src/issues?q=sort%3Aupdated-desc+is%3Aissue+is%3Aopen+label%3ABug+musl).
Dans les environnements de production, nous recommandons fortement d'utiliser la glibc.
Cela peut être réalisé en utilisant les images Docker Debian (par défaut) et [en compilant FrankenPHP à partir des sources](compile.md).
Alternativement, nous fournissons des binaires statiques compilés avec [l'allocateur mimalloc](https://github.com/microsoft/mimalloc), ce qui rend FrankenPHP+musl plus rapide (mais toujours plus lent que FrankenPHP+glibc).
## Configuration du runtime Go
FrankenPHP est écrit en Go.
En général, le runtime Go ne nécessite pas de configuration particulière, mais dans certaines circonstances,
une configuration spécifique améliore les performances.
Vous voudrez probablement mettre la variable d'environnement `GODEBUG` à `cgocheck=0` (la valeur par défaut dans les images Docker de FrankenPHP).
Si vous exécutez FrankenPHP dans des conteneurs (Docker, Kubernetes, LXC...) et que vous limitez la mémoire disponible pour les conteneurs,
mettez la variable d'environnement `GOMEMLIMIT` à la quantité de mémoire disponible.
Pour plus de détails, [la page de documentation Go dédiée à ce sujet](https://pkg.go.dev/runtime#hdr-Environment_Variables) est à lire absolument pour tirer le meilleur parti du runtime.
## `file_server`
Par défaut, la directive `php_server` met automatiquement en place un serveur de fichiers
pour servir les fichiers statiques (assets) stockés dans le répertoire racine.
Cette fonctionnalité est pratique, mais a un coût.
Pour la désactiver, utilisez la configuration suivante :
```caddyfile
php_server {
file_server off
}
```
## `try_files`
En plus des fichiers statiques et des fichiers PHP, `php_server` essaiera aussi de servir les fichiers d'index
et d'index de répertoire de votre application (`/path/` -> `/path/index.php`). Si vous n'avez pas besoin des index de répertoires,
vous pouvez les désactiver en définissant explicitement `try_files` comme ceci :
```caddyfile
php_server {
try_files {path} index.php
root /root/to/your/app # l'ajout explicite de la racine ici permet une meilleure mise en cache
}
```
Cela permet de réduire considérablement le nombre d'opérations inutiles sur les fichiers.
Une approche alternative avec 0 opérations inutiles sur le système de fichiers serait d'utiliser la directive `php`
et de diviser les fichiers de PHP par chemin. Cette approche fonctionne bien si votre application entière est servie par un seul fichier d'entrée.
Un exemple de [configuration](config.md#configuration-du-caddyfile) qui sert des fichiers statiques derrière un dossier `/assets` pourrait ressembler à ceci :
```caddyfile
route {
@assets {
path /assets/*
}
# tout ce qui se trouve derrière /assets est géré par le serveur de fichiers
file_server @assets {
root /root/to/your/app
}
# tout ce qui n'est pas dans /assets est géré par votre index ou votre fichier PHP worker
rewrite index.php
php {
root /root/to/your/app # l'ajout explicite de la racine ici permet une meilleure mise en cache
}
}
```
## _Placeholders_
Vous pouvez utiliser des [_placeholders_](https://caddyserver.com/docs/conventions#placeholders) dans les directives `root` et `env`.
Cependant, cela empêche la mise en cache de ces valeurs et a un coût important en termes de performances.
Si possible, évitez les _placeholders_ dans ces directives.
## `resolve_root_symlink`
Par défaut, si le _document root_ est un lien symbolique, il est automatiquement résolu par FrankenPHP (c'est nécessaire pour le bon fonctionnement de PHP).
Si la racine du document n'est pas un lien symbolique, vous pouvez désactiver cette fonctionnalité.
```caddyfile
php_server {
resolve_root_symlink false
}
```
Cela améliorera les performances si la directive `root` contient des [_placeholders_](https://caddyserver.com/docs/conventions#placeholders).
Le gain sera négligeable dans les autres cas.
## Journaux
La journalisation est évidemment très utile, mais, par définition, elle nécessite des opérations d'_I/O_ et des allocations de mémoire,
ce qui réduit considérablement les performances.
Assurez-vous de [définir le niveau de journalisation](https://caddyserver.com/docs/caddyfile/options#log) correctement,
et de ne journaliser que ce qui est nécessaire.
## Performances de PHP
FrankenPHP utilise l'interpréteur PHP officiel.
Toutes les optimisations de performances habituelles liées à PHP s'appliquent à FrankenPHP.
En particulier :
- vérifiez que [OPcache](https://www.php.net/manual/en/book.opcache.php) est installé, activé et correctement configuré
- activez [les optimisations de l'autoloader de Composer](https://getcomposer.org/doc/articles/autoloader-optimization.md)
- assurez-vous que le cache `realpath` est suffisamment grand pour les besoins de votre application
- utilisez le [pré-chargement](https://www.php.net/manual/en/opcache.preloading.php)
Pour plus de détails, lisez [l'entrée de la documentation dédiée de Symfony](https://symfony.com/doc/current/performance.html)
(la plupart des conseils sont utiles même si vous n'utilisez pas Symfony).

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