Compare commits

...

374 Commits

Author SHA1 Message Date
Kévin Dunglas
79e3a98a11 fix 2025-06-05 18:08:52 +02:00
Kévin Dunglas
afc4463ab3 feat: use frankenphp.dev as Go module path 2025-06-05 18:01:54 +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
219 changed files with 17419 additions and 5694 deletions

View File

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

View File

@@ -74,7 +74,7 @@ body:
attributes:
label: PHP configuration
description: |
Please copy and paste the output of the `phpinfo()` function.
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

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,33 @@
---
version: 2
updates:
-
package-ecosystem: gomod
- package-ecosystem: gomod
directory: /
schedule:
interval: weekly
commit-message:
prefix: chore
-
package-ecosystem: gomod
groups:
go-modules:
patterns:
- "*"
- 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:
- "*"
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
commit-message:
prefix: ci
groups:
github-actions:
patterns:
- "*"

View File

@@ -1,11 +1,14 @@
---
name: Build Docker images
concurrency:
cancel-in-progress: true
group: ${{ github.workflow }}-${{ github.ref }}
on:
pull_request:
branches:
- main
paths-ignore:
- 'docs/**'
- "docs/**"
push:
branches:
- main
@@ -13,18 +16,20 @@ on:
- v*.*.*
workflow_dispatch:
inputs:
#checkov:skip=CKV_GHA_7
version:
description: 'FrankenPHP 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' }}
LATEST: ${{ (github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && inputs.version) || startsWith(github.ref, 'refs/tags/')) && '0' || '1' }}
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: ${{ (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 }}
@@ -37,18 +42,19 @@ jobs:
skip: ${{ steps.check.outputs.skip }}
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="; "")')
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="; "")')
{
echo php_version="${PHP_83_LATEST},${PHP_82_LATEST}"
echo php_version="${PHP_82_LATEST},${PHP_83_LATEST},${PHP_84_LATEST}"
echo php82_version="${PHP_82_LATEST//./-}"
echo php83_version="${PHP_83_LATEST//./-}"
echo php84_version="${PHP_84_LATEST//./-}"
} >> "${GITHUB_OUTPUT}"
# Check if the Docker images must be rebuilt
@@ -57,10 +63,11 @@ jobs:
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="; "")')
FRANKENPHP_82_LATEST=$(skopeo inspect docker://docker.io/dunglas/frankenphp: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: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:php8.4 --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
if [[ "${FRANKENPHP_82_LATEST}" == "${PHP_82_LATEST}" ]] && [[ "${FRANKENPHP_83_LATEST}" == "${PHP_83_LATEST}" ]] && [[ "${FRANKENPHP_84_LATEST}" == "${PHP_84_LATEST}" ]]; then
echo skip=true >> "${GITHUB_OUTPUT}"
exit 0
fi
@@ -69,21 +76,19 @@ jobs:
echo ref="$(gh release view --repo dunglas/frankenphp --json tagName --jq '.tagName')"
echo skip=false
} >> "${GITHUB_OUTPUT}"
-
uses: actions/checkout@v4
- uses: actions/checkout@v4
if: ${{ !fromJson(steps.check.outputs.skip) }}
with:
ref: ${{ steps.check.outputs.ref }}
-
name: Set up Docker Buildx
- 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}"
@@ -92,10 +97,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) }}
@@ -105,52 +110,37 @@ jobs:
variant: ${{ fromJson(needs.prepare.outputs.variants) }}
platform: ${{ fromJson(needs.prepare.outputs.platforms) }}
include:
-
race: ""
qemu: true
-
platform: linux/amd64
qemu: false
- race: ""
- platform: linux/amd64
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 }}-bookworm
- variant: php-${{ needs.prepare.outputs.php82_version }}-bookworm
platform: linux/arm/v6
-
variant: php-${{ needs.prepare.outputs.php83_version }}-bookworm
- variant: php-${{ needs.prepare.outputs.php83_version }}-bookworm
platform: linux/arm/v6
steps:
-
uses: actions/checkout@v4
- name: Prepare
id: prepare
run: |
platform=${{ matrix.platform }}
echo "sanitized_platform=${platform//\//-}" >> "${GITHUB_OUTPUT}"
- uses: actions/checkout@v4
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
- 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) }}
@@ -159,18 +149,21 @@ jobs:
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=${{ needs.prepare.outputs.ref || github.ref }}-${{ matrix.platform }}
*.cache-from=type=gha,scope=refs/heads/main-${{ matrix.platform }}
*.cache-to=type=gha,scope=${{ needs.prepare.outputs.ref || github.ref }}-${{ matrix.platform }},ignore-error=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: |
@@ -183,36 +176,33 @@ jobs:
touch "/tmp/metadata/runner/${runnerDigest#sha256:}"
env:
METADATA: ${{ steps.build.outputs.metadata }}
-
name: Upload builder metadata
- name: Upload builder metadata
if: fromJson(needs.prepare.outputs.push)
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
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@v4
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 && !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 ./...'
sh -c 'go test -tags ${{ matrix.race }} -v ./... && cd caddy && go test -tags nobadger,nomysql,nopgx ${{ matrix.race }} -v ./...'
env:
METADATA: ${{ steps.build.outputs.metadata }}
# 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
@@ -221,28 +211,23 @@ 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@v3
- name: Download metadata
uses: actions/download-artifact@v4
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:
# Temporary fix for https://github.com/docker/buildx/issues/2229
version: "https://github.com/docker/buildx.git#master"
-
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
@@ -251,10 +236,9 @@ jobs:
$(printf "${IMAGE_NAME}@sha256:%s " *)
env:
METADATA: ${{ needs.prepare.outputs.metadata }}
-
name: Inspect image
- 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."${{ matrix.target }}-${{ matrix.variant }}".tags | first' <<< ${METADATA})
env:
METADATA: ${{ needs.prepare.outputs.metadata }}

View File

@@ -7,37 +7,36 @@ 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
- name: Checkout Code
uses: actions/checkout@v4
with:
fetch-depth: 0
-
name: Lint Code Base
uses: super-linter/super-linter@v5
- name: Lint Code Base
uses: super-linter/super-linter/slim@v7.4.0
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
# Prettier and StandardJS are incompatible
VALIDATE_JAVASCRIPT_PRETTIER: false
VALIDATE_TYPESCRIPT_PRETTIER: false
# Conflicts with MARKDOWN
VALIDATE_MARKDOWN_PRETTIER: false

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

@@ -0,0 +1,104 @@
---
name: Sanitizers
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
steps:
- name: Remove local PHP
run: sudo apt-get remove --purge --autoremove 'php*' 'libmemcached*'
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: "1.24"
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.4' -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 "${{ steps.determine-php-version.outputs.archive }}" | 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
- 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.dev.test -test.v

View File

@@ -1,11 +1,14 @@
---
name: Build binary releases
concurrency:
cancel-in-progress: true
group: ${{ github.workflow }}-${{ github.ref }}
on:
pull_request:
branches:
- main
paths-ignore:
- 'docs/**'
- "docs/**"
push:
branches:
- main
@@ -13,26 +16,31 @@ on:
- v*.*.*
workflow_dispatch:
inputs:
#checkov:skip=CKV_GHA_7
version:
description: 'FrankenPHP version'
description: "FrankenPHP version"
required: false
type: string
schedule:
- cron: '0 0 * * *'
- cron: "0 0 * * *"
permissions:
contents: write
id-token: write
attestations: write
env:
IMAGE_NAME: ${{ (github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && inputs.version) || startsWith(github.ref, 'refs/tags/')) && 'dunglas/frankenphp' || 'dunglas/frankenphp-dev' }}
LATEST: ${{ (github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && inputs.version) || startsWith(github.ref, 'refs/tags/')) && '0' || '1' }}
IMAGE_NAME: ${{ (github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && inputs.version) || startsWith(github.ref, 'refs/tags/')) && 'dunglas/frankenphp' || 'dunglas/frankenphp-dev' }}
GOTOOLCHAIN: local
jobs:
prepare:
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
outputs:
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 version
- name: Get version
id: check
if: github.event_name == 'schedule'
run: |
@@ -44,215 +52,323 @@ jobs:
echo "ref=${ref}" >> "${GITHUB_OUTPUT}"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-
uses: actions/checkout@v4
- uses: actions/checkout@v4
with:
ref: ${{ steps.check.outputs.ref }}
-
name: Set up Docker Buildx
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
version: latest
-
name: Create platforms matrix
- name: Create platforms matrix
id: matrix
run: |
METADATA="$(docker buildx bake --print static-builder | jq -c)"
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 || github.sha }}
build-linux:
VERSION: ${{ steps.check.outputs.ref || 'dev' }}
build-linux-musl:
strategy:
fail-fast: false
matrix:
platform: ${{ fromJson(needs.prepare.outputs.platforms) }}
debug: [false]
mimalloc: [false]
include:
-
qemu: true
-
platform: linux/amd64
qemu: false
-
platform: linux/amd64
qemu: false
- platform: linux/amd64
- platform: linux/amd64
debug: true
name: Build ${{ matrix.platform }} static binary${{ matrix.debug && ' (debug)' || '' }}
runs-on: ubuntu-latest
needs: [ prepare ]
- 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: |
platform=${{ matrix.platform }}
echo "sanitized_platform=${platform//\//-}" >> "${GITHUB_OUTPUT}"
- uses: actions/checkout@v4
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
- 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) && !matrix.debug }}
- 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
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Build
id: build
uses: docker/bake-action@v4
uses: docker/bake-action@v6
with:
pull: true
load: ${{ !fromJson(needs.prepare.outputs.push) || matrix.debug }}
targets: static-builder
load: ${{ !fromJson(needs.prepare.outputs.push) || matrix.debug || matrix.mimalloc }}
targets: static-builder-musl
set: |
${{ matrix.debug && 'static-builder.args.DEBUG_SYMBOLS=1' || '' }}
${{ 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${{ matrix.debug && '-debug' && '' }}
*.cache-from=type=gha,scope=refs/heads/main-static-builder${{ matrix.debug && '-debug' && '' }}
*.cache-to=type=gha,scope=${{ needs.prepare.outputs.ref || github.ref }}-static-builder${{ matrix.debug && '-debug' && '' }},ignore-error=true
${{ (fromJson(needs.prepare.outputs.push) && !matrix.debug) && format('*.output=type=image,name={0},push-by-digest=true,name-canonical=true,push=true', env.IMAGE_NAME) || '' }}
*.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 }}
VERSION: ${{ (github.ref_type == 'tag' && github.ref_name) || needs.prepare.outputs.ref || 'dev' }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-
# Workaround for https://github.com/actions/runner/pull/2477#issuecomment-1501003600
- # Workaround for https://github.com/actions/runner/pull/2477#issuecomment-1501003600
name: Export metadata
if: fromJson(needs.prepare.outputs.push) && !matrix.debug
if: fromJson(needs.prepare.outputs.push) && !matrix.debug && !matrix.mimalloc
run: |
mkdir -p /tmp/metadata
# shellcheck disable=SC2086
digest=$(jq -r '."static-builder"."containerimage.digest"' <<< ${METADATA})
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
uses: actions/upload-artifact@v3
- name: Upload metadata
if: fromJson(needs.prepare.outputs.push) && !matrix.debug && !matrix.mimalloc
uses: actions/upload-artifact@v4
with:
name: metadata-static-builder
name: metadata-static-builder-musl-${{ steps.prepare.outputs.sanitized_platform }}
path: /tmp/metadata/*
if-no-files-found: error
retention-days: 1
-
name: Copy binary
if: ${{ !fromJson(needs.prepare.outputs.push) || matrix.debug }}
- name: Copy binary
run: |
digest=$(jq -r '."static-builder"."containerimage.config.digest"' <<< "${METADATA}")
docker create --platform=${{ matrix.platform }} --name static-builder "${digest}"
docker cp "static-builder:/go/src/app/dist/${BINARY}" "${BINARY}${{ matrix.debug && '-debug' || '' }}"
# 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=${{ matrix.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' }}
-
name: Upload artifact
- name: Upload artifact
if: ${{ !fromJson(needs.prepare.outputs.push) }}
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: frankenphp-linux-${{ matrix.platform == 'linux/amd64' && 'x86_64' || 'aarch64' }}${{ matrix.debug && '-debug' || '' }}
path: frankenphp-linux-${{ matrix.platform == 'linux/amd64' && 'x86_64' || 'aarch64' }}${{ matrix.debug && '-debug' || '' }}
-
name: Upload debug asset
if: fromJson(needs.prepare.outputs.push) && matrix.debug && (needs.prepare.outputs.ref || github.ref_type == 'tag')
run: gh release upload "${{ (github.ref_type == 'tag' && github.ref_name) || needs.prepare.outputs.ref }}" frankenphp-linux-x86_64-debug --repo dunglas/frankenphp --clobber
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 "${{ (github.ref_type == 'tag' && github.ref_name) || needs.prepare.outputs.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 }}
# Adapted from https://docs.docker.com/build/ci/github-actions/multi-platform/
push:
runs-on: ubuntu-latest
needs:
- prepare
- build-linux
if: fromJson(needs.prepare.outputs.push)
#if: fromJson(needs.prepare.outputs.push) && (needs.prepare.outputs.ref || github.ref_type == 'tag')
steps:
-
name: Download metadata
uses: actions/download-artifact@v3
- if: fromJson(needs.prepare.outputs.push) && (needs.prepare.outputs.ref || github.ref_type == 'tag')
uses: actions/attest-build-provenance@v2
with:
name: metadata-static-builder
path: /tmp/metadata
-
name: Set up Docker Buildx
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:
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:
- name: Prepare
id: prepare
run: |
platform=${{ matrix.platform }}
echo "sanitized_platform=${platform//\//-}" >> "${GITHUB_OUTPUT}"
- uses: actions/checkout@v4
with:
ref: ${{ needs.prepare.outputs.ref }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
version: latest
-
name: Login to DockerHub
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: Create manifest list and push
- name: Build
id: build
uses: docker/bake-action@v6
with:
pull: true
load: ${{ !fromJson(needs.prepare.outputs.push) }}
targets: static-builder-gnu
set: |
${{ (github.event_name == 'pull_request' || matrix.platform == 'linux/arm64') && 'static-builder-gnu.args.NO_COMPRESS=1' || '' }}
static-builder-gnu.args.BUILD_PACKAGES=1
*.tags=
*.platform=${{ matrix.platform }}
*.cache-from=type=gha,scope=${{ needs.prepare.outputs.ref || github.ref }}-static-builder-gnu
*.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 }}
VERSION: ${{ (github.ref_type == 'tag' && github.ref_name) || needs.prepare.outputs.ref || 'dev' }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- # Workaround for https://github.com/actions/runner/pull/2477#issuecomment-1501003600
name: Export metadata
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@v4
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=${{ matrix.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' }}
- name: Upload artifact
if: ${{ !fromJson(needs.prepare.outputs.push) }}
uses: actions/upload-artifact@v4
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 "${{ (github.ref_type == 'tag' && github.ref_name) || needs.prepare.outputs.ref }}" gh-output/* --repo dunglas/frankenphp --clobber
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- if: fromJson(needs.prepare.outputs.push) && (needs.prepare.outputs.ref || github.ref_type == 'tag')
uses: actions/attest-build-provenance@v2
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@v4
with:
pattern: metadata-static-builder-musl-*
path: /tmp/metadata
merge-multiple: true
- name: Download GNU metadata
uses: actions/download-artifact@v4
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".tags | map("-t " + .) | join(" ")' <<< "${METADATA}") \
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: Inspect image
- name: Create GNU manifest list and push
working-directory: /tmp/metadata-gnu
run: |
# shellcheck disable=SC2046,SC2086
docker buildx imagetools inspect "$(jq -cr '.target."static-builder".tags | first' <<< "${METADATA}")"
docker buildx imagetools create $(jq -cr '.target."static-builder-gnu".tags | map("-t " + .) | join(" ")' <<< "${GNU_METADATA}") \
$(printf "${IMAGE_NAME}@sha256:%s " *)
env:
METADATA: ${{ needs.prepare.outputs.metadata }}
-
name: Copy binary
GNU_METADATA: ${{ needs.prepare.outputs.gnu_metadata }}
- name: Inspect image
run: |
tag=$(jq -cr '.target."static-builder".tags | first' <<< "${METADATA}")
docker cp "$(docker create --platform=linux/amd64 --name static-builder "${tag}"):/go/src/app/dist/frankenphp-linux-x86_64" frankenphp-linux-x86_64 ; docker rm static-builder
docker cp "$(docker create --platform=linux/arm64 --name static-builder "${tag}"):/go/src/app/dist/frankenphp-linux-aarch64" frankenphp-linux-aarch64 ; docker rm static-builder
# shellcheck disable=SC2046,SC2086
docker buildx imagetools inspect "$(jq -cr '.target."static-builder-musl".tags | first' <<< "${METADATA}")"
env:
METADATA: ${{ needs.prepare.outputs.metadata }}
-
name: Upload asset
if: needs.prepare.outputs.ref || github.ref_type == 'tag'
run: gh release upload "${{ (github.ref_type == 'tag' && github.ref_name) || needs.prepare.outputs.ref }}" frankenphp-linux-x86_64 frankenphp-linux-aarch64 --repo dunglas/frankenphp --clobber
- 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:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GNU_METADATA: ${{ needs.prepare.outputs.gnu_metadata }}
build-mac:
strategy:
fail-fast: false
matrix:
platform: ['arm64', 'x86_64']
platform: ["arm64", "x86_64"]
name: Build macOS ${{ matrix.platform }} binaries
runs-on: ${{ matrix.platform == 'arm64' && 'macos-14' || 'macos-13' }}
needs: [ prepare ]
needs: [prepare]
env:
HOMEBREW_NO_AUTO_UPDATE: 1
steps:
-
uses: actions/checkout@v4
- uses: actions/checkout@v4
with:
ref: ${{ needs.prepare.outputs.ref }}
-
uses: actions/setup-go@v5
- uses: actions/setup-go@v5
with:
go-version: '1.22'
go-version: "1.24"
cache-dependency-path: |
go.sum
go.sum
caddy/go.sum
-
name: Set FRANKENPHP_VERSION
- name: Set FRANKENPHP_VERSION
run: |
if [ "${GITHUB_REF_TYPE}" == "tag" ]; then
export FRANKENPHP_VERSION=${GITHUB_REF_NAME:1}
@@ -263,16 +379,31 @@ jobs:
fi
echo "FRANKENPHP_VERSION=${FRANKENPHP_VERSION}" >> "${GITHUB_ENV}"
-
name: Build FrankenPHP
- name: Build FrankenPHP
run: ./build-static.sh
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE: ${{ (needs.prepare.outputs.ref || github.ref_type == 'tag') && '1' || '' }}
-
name: Upload artifact
NO_COMPRESS: ${{ github.event_name == 'pull_request' && '1' || '' }}
- if: needs.prepare.outputs.ref || github.ref_type == 'tag'
uses: actions/attest-build-provenance@v2
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@v4
with:
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

@@ -5,33 +5,37 @@ on:
branches:
- main
paths-ignore:
- 'docs/**'
- "docs/**"
push:
branches:
- main
paths-ignore:
- 'docs/**'
- "docs/**"
permissions:
contents: read
env:
GOTOOLCHAIN: local
GOEXPERIMENT: cgocheck2
jobs:
tests:
tests-linux:
name: Tests (Linux, PHP ${{ matrix.php-versions }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php-versions: ['8.2', '8.3']
php-versions: ["8.2", "8.3", "8.4"]
env:
GOEXPERIMENT: cgocheck2
GOMAXPROCS: 10
LIBRARY_PATH: ${{ github.workspace }}/watcher/target/lib
steps:
-
uses: actions/checkout@v4
-
uses: actions/setup-go@v5
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.22'
go-version: "1.24"
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
@@ -39,37 +43,69 @@ jobs:
tools: none
env:
phpts: ts
-
name: Set CGO flags
run: |
echo "CGO_CFLAGS=$(php-config --includes)" >> "$GITHUB_ENV"
-
name: Build
run: go build
-
name: Build testcli binary
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
- name: Build testcli binary
working-directory: internal/testcli/
run: go build
-
name: Run library tests
- name: Run library tests
run: go test -race -v ./...
-
name: Run Caddy module tests
- 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@v4
- name: Lint Go code
uses: golangci/golangci-lint-action@v8
if: matrix.php-versions == '8.4'
with:
version: latest
tests-mac:
name: Tests (macOS, PHP 8.4)
runs-on: macos-latest
env:
HOMEBREW_NO_AUTO_UPDATE: 1
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: "1.24"
cache-dependency-path: |
go.sum
caddy/go.sum
- uses: shivammathur/setup-php@v2
with:
php-version: 8.4
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 ./...

6
.gitignore vendored
View File

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

7
.golangci.yaml Normal file
View File

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

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,70 +0,0 @@
[![GitHub Actions](https://github.com/Pithikos/C-Thread-Pool/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/Pithikos/C-Thread-Pool/actions?query=workflow%3Atests+branch%3Amaster)
# 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,571 +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
#ifndef _XOPEN_SOURCE
#define _XOPEN_SOURCE 500
#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
#if defined(__FreeBSD__) || defined(__OpenBSD__)
#include <pthread_np.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 interrupted 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);
#elif defined(__FreeBSD__) || defined(__OpenBSD__)
pthread_set_name_np(thread_p->pthread, 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

@@ -11,15 +11,18 @@ 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
```
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
@@ -107,24 +113,24 @@ 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 inlcuidng debug symbols:
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 dunglas/frankenphp:static-builder):/go/src/app/dist/frankenphp-linux-$(uname -m) frankenphp
```
```console
docker buildx bake \
--load \
--set static-builder.args.DEBUG_SYMBOLS=1 \
--set "static-builder.platform=linux/amd64" \
static-builder
docker cp $(docker create --name static-builder-musl dunglas/frankenphp:static-builder-musl):/go/src/app/dist/frankenphp-linux-$(uname -m) frankenphp
```
2. 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`)
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`
```
```console
gdb -p `pidof frankenphp`
```
5. If necessary, type `continue` in the GDB shell
6. Make FrankenPHP crash
@@ -136,64 +142,60 @@ docker buildx bake -f docker-bake.hcl --pull --no-cache --push
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.dev.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
@@ -210,8 +212,8 @@ 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 translates strings starting with `> [!` (it's special markup for GitHub)
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
6. In the [site repository](https://github.com/dunglas/frankenphp-website), 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,4 +1,7 @@
# 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
@@ -16,19 +19,24 @@ 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/
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
@@ -50,11 +58,14 @@ 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 \
cmake \
git \
libargon2-dev \
libbrotli-dev \
libcurl4-openssl-dev \
@@ -68,31 +79,45 @@ RUN apt-get update && \
&& \
apt-get clean
# Install e-dant/watcher (necessary for file watching)
WORKDIR /usr/local/src/watcher
RUN curl -s https://api.github.com/repos/e-dant/watcher/releases/latest | \
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.3/bookworm/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"
RUN echo $CGO_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'" && \
RUN GOBIN=/usr/local/bin go install -tags 'nobadger,nomysql,nopgx' -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/caddy/Caddyfile && \
frankenphp version
cp Caddyfile /etc/frankenphp/Caddyfile && \
frankenphp version && \
frankenphp build-info
WORKDIR /go/src/app
@@ -101,6 +126,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

105
README.md
View File

@@ -4,22 +4,51 @@
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 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
### Standalone Binary
We provide static FrankenPHP binaries for Linux and macOS
containing [PHP 8.4](https://www.php.net/releases/8.4/en.php) and most popular PHP extensions.
On Windows, use [WSL](https://learn.microsoft.com/windows/wsl/) to run FrankenPHP.
[Download FrankenPHP](https://github.com/dunglas/frankenphp/releases) or copy this line into your
terminal to automatically install the version appropriate for your platform:
```console
docker run -v $PWD:/app/public \
curl https://frankenphp.dev/install.sh | sh
mv frankenphp /usr/local/bin/
```
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
```
### 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
```
@@ -31,47 +60,51 @@ Go to `https://localhost`, and enjoy!
> Do not attempt to use `https://127.0.0.1`. Use `https://localhost` and accept the self-signed certificate.
> Use the [`SERVER_NAME` environment variable](docs/config.md#environment-variables) to change the domain to use.
### Standalone Binary
### Homebrew
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)
FrankenPHP is also available as a [Homebrew](https://brew.sh) package for macOS and Linux.
To install it:
```console
brew install dunglas/frankenphp/frankenphp
```
To serve the content of the current directory, run:
```console
./frankenphp php-server
```
You can also run command-line scripts with:
```console
./frankenphp php-cli /path/to/your/script.php
frankenphp php-server
```
## 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/)
* [Deploy in production](docs/production.md)
* [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/)
* [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/)
- [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/)
- [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/frankenphp.dev)
- [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)
* [TYPO3](https://github.com/ochorocho/franken-typo3)
- [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/dunglas/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
@@ -43,21 +54,30 @@ 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 \
@@ -66,31 +86,42 @@ RUN apk add --no-cache --virtual .build-deps \
sqlite-dev \
upx
# Install e-dant/watcher (necessary for file watching)
WORKDIR /usr/local/src/watcher
RUN curl -s https://api.github.com/repos/e-dant/watcher/releases/latest | \
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 install -tags 'nobadger,nomysql,nopgx' -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 && \
upx --best /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
@@ -99,6 +130,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

51
backoff.go Normal file
View File

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

41
backoff_test.go Normal file
View File

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

119
build-packages.sh Executable file
View File

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

View File

@@ -1,164 +1,355 @@
#!/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
arch="$(uname -m)"
os="$(uname -s | tr '[:upper:]' '[:lower:]')"
# 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)
# - 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
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="apcu,bcmath,bz2,calendar,ctype,curl,dba,dom,exif,fileinfo,filter,ftp,gd,gmp,gettext,iconv,igbinary,imagick,intl,ldap,mbregex,mbstring,mysqli,mysqlnd,opcache,openssl,parallel,pcntl,pdo,pdo_mysql,pdo_pgsql,pdo_sqlite,pgsql,phar,posix,protobuf,readline,redis,session,shmop,simplexml,soap,sockets,sodium,sqlite3,ssh2,sysvmsg,sysvsem,sysvshm,tidy,tokenizer,xlswriter,xml,xmlreader,xmlwriter,zip,zlib,yaml,zstd"
# if [ "${os}" != "linux" ] || [ "${SPC_LIBC}" = "glibc" ]; then
# defaultExtensions="${defaultExtensions},ffi"
# fi
defaultExtensionLibs="bzip2,freetype,libavif,libjpeg,liblz4,libwebp,libzip,nghttp2"
md5binary="md5sum"
if [ "${os}" = "darwin" ]; then
os="mac"
md5binary="md5 -q"
os="mac"
md5binary="md5 -q"
fi
if [ -z "${PHP_EXTENSIONS}" ]; then
export PHP_EXTENSIONS="apcu,bcmath,bz2,calendar,ctype,curl,dba,dom,exif,fileinfo,filter,gd,iconv,igbinary,intl,ldap,mbregex,mbstring,mysqli,mysqlnd,opcache,openssl,pcntl,pdo,pdo_mysql,pdo_pgsql,pdo_sqlite,pgsql,phar,posix,readline,redis,session,simplexml,sockets,sodium,sqlite3,sysvsem,tokenizer,xml,xmlreader,xmlwriter,zip,zlib"
if [ "${os}" = "linux" ] && ! type "cmake" >/dev/null 2>&1; then
echo "The \"cmake\" command must be installed."
exit 1
fi
if [ -z "${PHP_EXTENSION_LIBS}" ]; then
export PHP_EXTENSION_LIBS="bzip2,freetype,libavif,libjpeg,liblz4,libwebp,libzip"
fi
if [ "${os}" = "linux" ] && { [[ "${arch}" =~ "aarch" ]] || [[ "${arch}" =~ "arm" ]]; }; then
fpic="-fPIC"
fpie="-fPIE"
# 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
export PHP_EXTENSION_LIBS="${PHP_EXTENSION_LIBS},brotli"
fi
if [ -z "${PHP_VERSION}" ]; then
export PHP_VERSION="8.3"
if [ -z "${DEBUG_SYMBOLS}" ]; then
export SPC_PHP_DEFAULT_OPTIMIZE_CFLAGS="-g -fstack-protector-strong -fPIC -fPIE -Os -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64"
fi
else
fpic="-fpic"
fpie="-fpie"
fi
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
if echo "${FRANKENPHP_VERSION}" | grep -F -q "."; then
# Tag
# Trim "v" prefix if any
FRANKENPHP_VERSION=${FRANKENPHP_VERSION#v}
export FRANKENPHP_VERSION
# Trim "v" prefix if any
FRANKENPHP_VERSION=${FRANKENPHP_VERSION#v}
export FRANKENPHP_VERSION
git checkout "v${FRANKENPHP_VERSION}"
else
git checkout "${FRANKENPHP_VERSION}"
fi
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
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
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
# 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
# Additional libraries to build
if [ -z "${PHP_EXTENSION_LIBS}" ]; then
PHP_EXTENSION_LIBS="${defaultExtensionLibs}"
fi
# 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
# 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
# Build libphp if necessary
if [ -f "dist/static-php-cli/buildroot/lib/libphp.a" ]; then
cd dist/static-php-cli
cache_key="${PHP_VERSION}-${PHP_EXTENSIONS}-${PHP_EXTENSION_LIBS}"
if [ -f ../cache_key ] && [ "$(cat ../cache_key)" = "${cache_key}" ] && [ -f "buildroot/lib/libphp.a" ]; then
echo "Hit cache, skipping libphp build."
else
mkdir -p dist/
cd dist/
${spcCommand} doctor --auto-fix
# shellcheck disable=SC2086
${spcCommand} download --with-php="${PHP_VERSION}" --for-extensions="${PHP_EXTENSIONS}" --for-libs="${PHP_EXTENSION_LIBS}" ${SPC_OPT_DOWNLOAD_ARGS}
# shellcheck disable=SC2086
${spcCommand} build --enable-zts --build-embed ${SPC_OPT_BUILD_ARGS} "${PHP_EXTENSIONS}" --with-libs="${PHP_EXTENSION_LIBS}"
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
if ! type "composer" > /dev/null; then
packages="composer"
fi
if ! type "go" > /dev/null; then
packages="${packages} go"
fi
if [ -n "${RELEASE}" ] && ! type "gh" > /dev/null; then
packages="${packages} gh"
fi
if [ -n "${packages}" ]; then
# shellcheck disable=SC2086
brew install --formula --quiet ${packages}
fi
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 --auto-fix
./bin/spc download --with-php="${PHP_VERSION}" --for-extensions="${PHP_EXTENSIONS}" --for-libs="${PHP_EXTENSION_LIBS}" --ignore-cache-sources=php-src
# shellcheck disable=SC2086
./bin/spc build --enable-zts --build-embed ${extraOpts} "${PHP_EXTENSIONS}" --with-libs="${PHP_EXTENSION_LIBS}"
echo -n "${cache_key}" >../cache_key
fi
CGO_CFLAGS="-DFRANKENPHP_VERSION=${FRANKENPHP_VERSION} -I${PWD}/buildroot/include/ $(./buildroot/bin/php-config --includes | sed s#-I/#-I"${PWD}"/buildroot/#g)"
if ! type "go" >/dev/null 2>&1; then
echo "The \"go\" command must be installed."
exit 1
fi
XCADDY_COMMAND="xcaddy"
if ! type "$XCADDY_COMMAND" >/dev/null 2>&1; then
go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest
XCADDY_COMMAND="$(go env GOPATH)/bin/xcaddy"
fi
curlGitHubHeaders=(--header "X-GitHub-Api-Version: 2022-11-28")
if [ "${GITHUB_TOKEN}" ]; then
curlGitHubHeaders+=(--header "Authorization: Bearer ${GITHUB_TOKEN}")
fi
# Compile e-dant/watcher as a static library
mkdir -p watcher
cd watcher
curl -f --retry 5 "${curlGitHubHeaders[@]}" https://api.github.com/repos/e-dant/watcher/releases/latest |
grep tarball_url |
awk '{ print $2 }' |
sed 's/,$//' |
sed 's/"//g' |
xargs curl -fL --retry 5 "${curlGitHubHeaders[@]}" |
tar xz --strip-components 1
cd watcher-c
if [ -z "${CC}" ]; then
watcherCC=cc
else
watcherCC="${CC}"
fi
${watcherCC} -c -o libwatcher-c.o ./src/watcher-c.cpp -I ./include -I ../include -std=c++17 -Wall -Wextra "${fpic}"
ar rcs libwatcher-c.a libwatcher-c.o
cp libwatcher-c.a ../../buildroot/lib/libwatcher-c.a
mkdir -p ../../buildroot/include/wtr
cp -R include/wtr/watcher-c.h ../../buildroot/include/wtr/watcher-c.h
cd ../../
# See https://github.com/docker-library/php/blob/master/8.3/alpine3.20/zts/Dockerfile#L53-L55
CGO_CFLAGS="-DFRANKENPHP_VERSION=${FRANKENPHP_VERSION} -I${PWD}/buildroot/include/ $(${spcCommand} spc-config "${PHP_EXTENSIONS}" --with-libs="${PHP_EXTENSION_LIBS}" --includes)"
if [ -n "${DEBUG_SYMBOLS}" ]; then
CGO_CFLAGS="-g ${CGO_CFLAGS}"
CGO_CFLAGS="-g ${CGO_CFLAGS}"
else
CGO_CFLAGS="-fstack-protector-strong ${fpic} ${fpie} -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 ${CGO_CFLAGS}"
fi
export CGO_CFLAGS
export CGO_CPPFLAGS="${CGO_CFLAGS}"
if [ "${os}" = "mac" ]; then
export CGO_LDFLAGS="-framework CoreFoundation -framework SystemConfiguration"
export CGO_LDFLAGS="-framework CoreFoundation -framework SystemConfiguration"
elif [ "${os}" = "linux" ] && [ -z "${DEBUG_SYMBOLS}" ]; then
CGO_LDFLAGS="-Wl,-O1 -pie"
fi
if [ "${os}" = "linux" ] && [ "${SPC_LIBC}" = "glibc" ]; then
CGO_LDFLAGS="${CGO_LDFLAGS} -Wl,--allow-multiple-definition -Wl,--export-dynamic"
fi
CGO_LDFLAGS="${CGO_LDFLAGS} ${PWD}/buildroot/lib/libbrotlicommon.a ${PWD}/buildroot/lib/libbrotlienc.a ${PWD}/buildroot/lib/libbrotlidec.a ${PWD}/buildroot/lib/libwatcher-c.a $(${spcCommand} spc-config "${PHP_EXTENSIONS}" --with-libs="${PHP_EXTENSION_LIBS}" --libs)"
if [[ "$CGO_LDFLAGS" == *"${PWD}/buildroot/lib/mimalloc.o"* ]]; then
CGO_LDFLAGS=${CGO_LDFLAGS//${PWD}\/buildroot\/lib\/mimalloc.o/}
CGO_LDFLAGS="${PWD}/buildroot/lib/libmimalloc.a $CGO_LDFLAGS"
fi
if [ "${os}" = "linux" ] && [ "${SPC_LIBC}" = "glibc" ]; then
CGO_LDFLAGS="${CGO_LDFLAGS//-lphp/-Wl,--whole-archive -lphp -Wl,--no-whole-archive}"
# shellcheck disable=SC2046
ar d "${PWD}/buildroot/lib/libphp.a" $(ar t "${PWD}/buildroot/lib/libphp.a" | grep '\.a$')
fi
CGO_LDFLAGS="${CGO_LDFLAGS} ${PWD}/buildroot/lib/libbrotlicommon.a ${PWD}/buildroot/lib/libbrotlienc.a ${PWD}/buildroot/lib/libbrotlidec.a $(./buildroot/bin/php-config --ldflags) $(./buildroot/bin/php-config --libs)"
export CGO_LDFLAGS
LIBPHP_VERSION="$(./buildroot/bin/php-config --version)"
export LIBPHP_VERSION
cd ../..
cd ../
if [ -z "${DEBUG_SYMBOLS}" ]; then
extraLdflags="-w -s"
fi
cd ../
# Embed PHP app, if any
if [ -n "${EMBED}" ] && [ -d "${EMBED}" ]; then
tar -cf app.tar -C "${EMBED}" .
${md5binary} app.tar > app_checksum.txt
tar -cf app.tar -C "${EMBED}" .
${md5binary} app.tar | awk '{printf $1}' >app_checksum.txt
fi
if [ "${os}" = "linux" ]; then
extraExtldflags="-Wl,-z,stack-size=0x80000"
if [ -z "${XCADDY_ARGS}" ]; then
XCADDY_ARGS="--with github.com/dunglas/caddy-cbrotli --with github.com/dunglas/mercure/caddy --with github.com/dunglas/vulcain/caddy"
fi
if [ -z "${DEBUG_SYMBOLS}" ]; then
extraLdflags="-w -s"
XCADDY_DEBUG=0
if [ -n "${DEBUG_SYMBOLS}" ]; then
XCADDY_DEBUG=1
fi
if [ "${SPC_LIBC}" = "musl" ]; then
muslStackSizeFix="-Wl,-z,stack-size=0x80000"
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 ../..
cd caddy/
if [ -z "${SPC_LIBC}" ] || [ "${SPC_LIBC}" = "musl" ]; then
xcaddyGoBuildFlags="-buildmode=pie -tags cgo,netgo,osusergo,static_build,nobadger,nomysql,nopgx -ldflags \"-linkmode=external -extldflags '-static-pie ${muslStackSizeFix}' ${extraLdflags} -X 'github.com/caddyserver/caddy/v2.CustomVersion=FrankenPHP ${FRANKENPHP_VERSION} PHP ${LIBPHP_VERSION} Caddy'\""
elif [ "${SPC_LIBC}" = "glibc" ]; then
xcaddyGoBuildFlags="-buildmode=pie -tags cgo,netgo,osusergo,nobadger,nomysql,nopgx -ldflags \"-linkmode=external -extldflags '-pie' ${extraLdflags} -X 'github.com/caddyserver/caddy/v2.CustomVersion=FrankenPHP ${FRANKENPHP_VERSION} PHP ${LIBPHP_VERSION} Caddy'\""
fi
# shellcheck disable=SC2086
CGO_ENABLED=1 \
XCADDY_GO_BUILD_FLAGS=${xcaddyGoBuildFlags} \
XCADDY_DEBUG="${XCADDY_DEBUG}" \
${XCADDY_COMMAND} build \
--output "../dist/${bin}" \
${XCADDY_ARGS} \
--with frankenphp.dev=.. \
--with frankenphp.dev/caddy=.
cd ..
if [ -d "${EMBED}" ]; then
truncate -s 0 app.tar
truncate -s 0 app_checksum.txt
truncate -s 0 app.tar
truncate -s 0 app_checksum.txt
fi
if type "upx" > /dev/null && [ -z "${DEBUG_SYMBOLS}" ]; then
upx --best "dist/${bin}"
if type "upx" >/dev/null 2>&1 && [ -z "${DEBUG_SYMBOLS}" ] && [ -z "${NO_COMPRESS}" ]; then
upx --best "dist/${bin}"
fi
"dist/${bin}" version
"dist/${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}" "dist/${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"
"frankenphp.dev"
"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}
}

293
caddy/admin_test.go Normal file
View File

@@ -0,0 +1,293 @@
package caddy_test
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"sync"
"testing"
"frankenphp.dev"
"frankenphp.dev/internal/fastabs"
"github.com/caddyserver/caddy/v2/caddytest"
"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 1
}
}
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 := len(getDebugState(t, tester).ThreadDebugStates)
// try to spawn the additional threads by spamming the server
for tries := 0; tries < maxTries; tries++ {
wg.Add(requestsPerTry)
for i := 0; i < requestsPerTry; i++ {
go func() {
tester.AssertGetResponse(endpoint, http.StatusOK, "slept for 2 ms and worked for 1000 iterations")
wg.Done()
}()
}
wg.Wait()
amountOfThreads = len(getDebugState(t, tester).ThreadDebugStates)
if amountOfThreads > 2 {
break
}
}
// assert that there are now more threads than before
assert.NotEqual(t, amountOfThreads, 2)
}
// 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 := len(getDebugState(t, tester).ThreadDebugStates)
// try to spawn the additional threads by spamming the server
for tries := 0; tries < maxTries; tries++ {
wg.Add(requestsPerTry)
for i := 0; i < requestsPerTry; i++ {
go func() {
tester.AssertGetResponse(endpoint, http.StatusOK, "slept for 2 ms and worked for 1000 iterations")
wg.Done()
}()
}
wg.Wait()
amountOfThreads = len(getDebugState(t, tester).ThreadDebugStates)
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 {
threadStates := getAdminResponseBody(t, tester, "GET", "threads")
var debugStates frankenphp.FrankenPHPDebugState
err := json.Unmarshal([]byte(threadStates), &debugStates)
assert.NoError(t, err)
return debugStates
}
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")
}

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

@@ -6,9 +6,15 @@ package caddy
import (
"encoding/json"
"errors"
"fmt"
"log/slog"
"net/http"
"path/filepath"
"strconv"
"strings"
"time"
"frankenphp.dev/internal/fastabs"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
@@ -17,170 +23,348 @@ import (
"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"
"frankenphp.dev"
)
const defaultDocumentRoot = "public"
const (
defaultDocumentRoot = "public"
defaultWatchPattern = "./**/*.{php,yaml,yml,twig,env}"
)
var iniError = errors.New("'php_ini' must be in the format: php_ini \"<key>\" \"<value>\"")
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)
}
type mainPHPinterpreterKeyType int
var mainPHPInterpreterKey mainPHPinterpreterKeyType
var phpInterpreter = caddy.NewUsagePool()
type phpInterpreterDestructor struct{}
func (phpInterpreterDestructor) Destruct() error {
frankenphp.Shutdown()
return nil
httpcaddyfile.RegisterDirectiveOrder("php_server", "before", "file_server")
}
type workerConfig struct {
// 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"`
// 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"`
}
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"`
metrics frankenphp.Metrics
logger *slog.Logger
}
// CaddyModule returns the Caddy module information.
func (a FrankenPHPApp) CaddyModule() caddy.ModuleInfo {
func (f FrankenPHPApp) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: "frankenphp",
New: func() caddy.Module { return &a },
New: func() caddy.Module { return &f },
}
}
// Provision sets up the module.
func (f *FrankenPHPApp) Provision(ctx caddy.Context) error {
f.logger = ctx.Slogger()
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
f.metrics = frankenphp.NewPrometheusMetrics(ctx.GetMetricsRegistry())
}
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()
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))
opts := []frankenphp.Option{
frankenphp.WithNumThreads(f.NumThreads),
frankenphp.WithMaxThreads(f.MaxThreads),
frankenphp.WithLogger(f.logger),
frankenphp.WithMetrics(f.metrics),
frankenphp.WithPhpIni(f.PhpIni),
frankenphp.WithMaxWaitTime(f.MaxWaitTime),
}
for _, w := range append(f.Workers) {
opts = append(opts, frankenphp.WithWorkers(w.Name, repl.ReplaceKnown(w.FileName, ""), w.Num, w.Env, w.Watch))
}
_, 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 {
frankenphp.Shutdown()
if err := frankenphp.Init(opts...); 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 🐘")
func (f *FrankenPHPApp) Stop() error {
f.logger.Info("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
return nil
}
func parseWorkerConfig(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, errors.New(`FrankenPHP: too many "worker" arguments: ` + d.Val())
}
for d.NextBlock(1) {
v := d.Val()
switch 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, err
}
wc.Num = 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":
if !d.NextArg() {
// the default if the watch directory is left empty:
wc.Watch = append(wc.Watch, defaultWatchPattern)
} else {
wc.Watch = append(wc.Watch, d.Val())
}
default:
allowedDirectives := "name, file, num, env, watch"
return wc, wrongSubDirectiveError("worker", allowedDirectives, v)
}
}
if wc.FileName == "" {
return wc, errors.New(`the "file" argument must be specified`)
}
if frankenphp.EmbeddedAppPath != "" && filepath.IsLocal(wc.FileName) {
wc.FileName = filepath.Join(frankenphp.EmbeddedAppPath, wc.FileName)
}
return wc, 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.Atoi(d.Val())
v, err := strconv.ParseUint(d.Val(), 10, 32)
if err != nil {
return err
}
f.NumThreads = v
case "worker":
wc := workerConfig{}
if d.NextArg() {
wc.FileName = d.Val()
f.NumThreads = int(v)
case "max_threads":
if !d.NextArg() {
return d.ArgErr()
}
if d.NextArg() {
v, err := strconv.Atoi(d.Val())
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 errors.New("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 iniError
}
if f.PhpIni == nil {
f.PhpIni = make(map[string]string)
}
f.PhpIni[key] = d.Val()
if d.NextArg() {
return iniError
}
return nil
}
isBlock := false
for d.NextBlock(1) {
isBlock = true
err := parseIniLine(d)
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 !isBlock {
if !d.NextArg() {
return iniError
}
if wc.FileName == "" {
return errors.New(`The "file" argument must be specified`)
err := parseIniLine(d)
if err != nil {
return err
}
}
if frankenphp.EmbeddedAppPath != "" && filepath.IsLocal(wc.FileName) {
wc.FileName = filepath.Join(frankenphp.EmbeddedAppPath, wc.FileName)
case "worker":
wc, err := parseWorkerConfig(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 fmt.Errorf(`global worker names must not start with "m#": %q`, wc.Name)
}
// check for duplicate workers
for _, existingWorker := range f.Workers {
if existingWorker.FileName == wc.FileName {
return fmt.Errorf("global workers must not have duplicate filenames: %q", wc.FileName)
}
}
f.Workers = append(f.Workers, wc)
default:
allowedDirectives := "num_threads, max_threads, php_ini, worker, max_wait_time"
return wrongSubDirectiveError("frankenphp", allowedDirectives, d.Val())
}
}
}
if f.MaxThreads > 0 && f.NumThreads > 0 && f.MaxThreads < f.NumThreads {
return errors.New(`"max_threads"" must be greater than or equal to "num_threads"`)
}
return nil
}
@@ -205,8 +389,14 @@ type FrankenPHPModule struct {
// 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 frankenphp.PreparedEnv `json:"env,omitempty"`
logger *zap.Logger
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
}
// CaddyModule returns the Caddy module information.
@@ -219,7 +409,24 @@ func (FrankenPHPModule) CaddyModule() caddy.ModuleInfo {
// Provision sets up the module.
func (f *FrankenPHPModule) Provision(ctx caddy.Context) error {
f.logger = ctx.Logger(f)
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`)
}
workers, err := fapp.addModuleWorkers(f.Workers...)
if err != nil {
return err
}
f.Workers = workers
if f.Root == "" {
if frankenphp.EmbeddedAppPath == "" {
@@ -244,39 +451,98 @@ func (f *FrankenPHPModule) Provision(ctx caddy.Context) error {
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
}
}
}
return nil
}
// needReplacement checks if a string contains placeholders.
func needReplacement(s string) bool {
return strings.ContainsAny(s, "{}")
}
// 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 {
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, "")
var documentRootOption frankenphp.RequestOption
var 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 := make(map[string]string, len(f.Env)+1)
env["REQUEST_URI\x00"] = origReq.URL.RequestURI()
for k, v := range f.Env {
env[k] = repl.ReplaceKnown(v, "")
env := f.preparedEnv
if f.preparedEnvNeedsReplacement {
env = make(frankenphp.PreparedEnv, len(f.Env))
for k, v := range f.preparedEnv {
env[k] = repl.ReplaceKnown(v, "")
}
}
fullScriptPath, _ := fastabs.FastAbs(documentRoot + "/" + r.URL.Path)
workerName := ""
for _, w := range f.Workers {
if p, _ := fastabs.FastAbs(w.FileName); p == fullScriptPath {
workerName = w.Name
}
}
fr, err := frankenphp.NewRequestWithContext(
r,
frankenphp.WithRequestDocumentRoot(documentRoot, *f.ResolveRootSymlink),
documentRootOption,
frankenphp.WithRequestSplitPath(f.SplitPath),
frankenphp.WithRequestPreparedEnv(env),
frankenphp.WithOriginalRequest(&origReq),
frankenphp.WithWorkerName(workerName),
)
if err != nil {
return err
if err = frankenphp.ServeHTTP(w, fr); err != nil {
return caddyhttp.Error(http.StatusInternalServerError, err)
}
return frankenphp.ServeHTTP(w, fr)
return nil
}
// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
func (f *FrankenPHPModule) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
// First pass: Parse all directives except "worker"
for d.Next() {
for d.NextBlock(0) {
switch d.Val() {
@@ -298,24 +564,75 @@ func (f *FrankenPHPModule) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
return d.ArgErr()
}
if f.Env == nil {
f.Env = make(frankenphp.PreparedEnv)
f.Env = make(map[string]string)
f.preparedEnv = make(frankenphp.PreparedEnv)
}
f.Env[args[0]+"\x00"] = args[1]
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() {
if v, err := strconv.ParseBool(d.Val()); err == nil {
f.ResolveRootSymlink = &v
if d.NextArg() {
return d.ArgErr()
}
}
return d.ArgErr()
}
rrs := true
f.ResolveRootSymlink = &rrs
f.ResolveRootSymlink = &v
case "worker":
for d.NextBlock(1) {
}
for d.NextArg() {
}
// Skip "worker" blocks in the first pass
continue
default:
allowedDirectives := "root, split, env, resolve_root_symlink, worker"
return wrongSubDirectiveError("php or php_server", allowedDirectives, d.Val())
}
}
}
// Second pass: Parse only "worker" blocks
d.Reset()
for d.Next() {
for d.NextBlock(0) {
if d.Val() == "worker" {
wc, err := parseWorkerConfig(d)
if err != nil {
return err
}
// Inherit environment variables from the parent php_server directive
if !filepath.IsAbs(wc.FileName) && f.Root != "" {
wc.FileName = filepath.Join(f.Root, wc.FileName)
}
if f.Env != nil {
if wc.Env == nil {
wc.Env = make(map[string]string)
}
for k, v := range f.Env {
// Only set if not already defined in the worker
if _, exists := wc.Env[k]; !exists {
wc.Env[k] = v
}
}
}
// Check if a worker with this filename already exists in this module
for _, existingWorker := range f.Workers {
if existingWorker.FileName == wc.FileName {
return fmt.Errorf(`workers in a single "php_server" block must not have duplicate filenames: %q`, wc.FileName)
}
}
f.Workers = append(f.Workers, wc)
}
}
}
@@ -325,7 +642,7 @@ func (f *FrankenPHPModule) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
// parseCaddyfile unmarshals tokens from h into a new Middleware.
func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
m := FrankenPHPModule{}
m := &FrankenPHPModule{}
err := m.UnmarshalCaddyfile(h.Dispenser)
return m, err
@@ -377,7 +694,7 @@ func parsePhpServer(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error)
indexFile := "index.php"
// set up for explicitly overriding try_files
tryFiles := []string{}
var tryFiles []string
// if the user specified a matcher token, use that
// matcher in a route that wraps both of our routes;
@@ -475,37 +792,65 @@ func parsePhpServer(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error)
// 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)},
}
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 {
tryFiles = []string{"{http.request.uri.path}", "{http.request.uri.path}/" + indexFile, indexFile}
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 = ""
}
for _, tf := range tryFiles {
if tf == dirIndex {
dirRedir = true
break
}
}
}
// route to redirect to canonical path if index PHP file
if dirRedir {
redirMatcherSet := caddy.ModuleMap{
"file": h.JSON(fileserver.MatchFile{
TryFiles: []string{dirIndex},
}),
"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,
}),
}
@@ -517,12 +862,12 @@ func parsePhpServer(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error)
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(rewriteHandler, "handler", "rewrite", nil)},
}
routes = append(routes, redirRoute, rewriteRoute)
routes = append(routes, rewriteRoute)
}
// 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)
}
@@ -534,6 +879,7 @@ func parsePhpServer(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error)
// using the php directive syntax
dispenser.Next() // consume the directive name
err = phpsrv.UnmarshalCaddyfile(dispenser)
if err != nil {
return nil, err
}
@@ -585,9 +931,15 @@ func parsePhpServer(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error)
}, nil
}
// return a nice error message
func wrongSubDirectiveError(module string, allowedDriectives string, wrongValue string) error {
return fmt.Errorf("unknown '%s' subdirective: '%s' (allowed directives are: %s)", module, wrongValue, allowedDriectives)
}
// Interface guards
var (
_ caddy.App = (*FrankenPHPApp)(nil)
_ caddy.Provisioner = (*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, "./**/*.{php,yaml,yml,twig,env}", 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#")
}

View File

@@ -1,16 +1,16 @@
# 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}
@@ -19,16 +19,13 @@
# # Redact the authorization query parameter that can be set by Mercure
# format filter {
# request>uri query {
# wrap console
# fields {
# uri query {
# replace authorization REDACTED
# }
# replace authorization REDACTED
# }
# }
#}
root public/
root * public/
encode zstd br gzip
# Uncomment the following lines to enable Mercure and Vulcain modules
#mercure {
@@ -50,5 +47,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,25 +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"
_ "frankenphp.dev/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,199 +1,208 @@
module github.com/dunglas/frankenphp/caddy
module frankenphp.dev/caddy
go 1.21
go 1.24.0
toolchain go1.22.0
replace github.com/dunglas/frankenphp => ../
replace frankenphp.dev => ../
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/caddyserver/caddy/v2 v2.10.0
github.com/caddyserver/certmagic v0.23.0
github.com/dunglas/caddy-cbrotli v1.0.0
github.com/dunglas/frankenphp v1.1.1
github.com/dunglas/mercure/caddy v0.15.10
github.com/dunglas/vulcain/caddy v1.0.2
github.com/spf13/cobra v1.8.0
go.uber.org/automaxprocs v1.5.3
go.uber.org/zap v1.27.0
frankenphp.dev v1.7.0
github.com/dunglas/mercure/caddy v0.19.2
github.com/dunglas/vulcain/caddy v1.2.0
github.com/prometheus/client_golang v1.22.0
github.com/spf13/cobra v1.9.1
github.com/stretchr/testify v1.10.0
)
require github.com/smallstep/go-attestation v0.4.4-0.20240109183208-413678f90935 // indirect
require github.com/smallstep/go-attestation v0.4.4-0.20241119153605-2306d5b464ca // indirect
require (
cel.dev/expr v0.24.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.2 // 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.3.1 // 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.9.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.9.6 // indirect
github.com/MicahParks/keyfunc/v3 v3.4.0 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/RoaringBitmap/roaring v1.9.4 // indirect
github.com/alecthomas/chroma/v2 v2.18.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.13.0 // indirect
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
github.com/bits-and-blooms/bitset v1.22.0 // indirect
github.com/caddyserver/zerossl v0.1.3 // indirect
github.com/ccoveille/go-safecast v1.6.1 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // 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.14.1 // 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.11.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/dolthub/maphash v0.1.0 // indirect
github.com/dunglas/httpsfv v1.0.2 // indirect
github.com/dunglas/mercure v0.15.10 // indirect
github.com/dunglas/vulcain v1.0.2 // indirect
github.com/dunglas/httpsfv v1.1.0 // indirect
github.com/dunglas/mercure v0.19.2 // indirect
github.com/dunglas/vulcain v1.2.0 // indirect
github.com/dustin/go-humanize v1.0.1 // 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/gammazero/deque v0.2.1 // indirect
github.com/getkin/kin-openapi v0.123.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.4.1 // indirect
github.com/francoispqt/gojay v1.2.13 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/fxamacker/cbor/v2 v2.8.0 // indirect
github.com/gammazero/deque v1.0.0 // indirect
github.com/getkin/kin-openapi v0.132.0 // indirect
github.com/go-chi/chi/v5 v5.2.1 // indirect
github.com/go-jose/go-jose/v3 v3.0.4 // indirect
github.com/go-jose/go-jose/v4 v4.1.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/swag v0.23.0 // 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.21.1 // indirect
github.com/go-openapi/swag v0.23.1 // indirect
github.com/go-sql-driver/mysql v1.9.2 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
github.com/gofrs/uuid v4.4.0+incompatible // indirect
github.com/golang-jwt/jwt/v5 v5.2.1 // 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/brotli/go/cbrotli v0.0.0-20240116120200-adbc354d23af // 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/golang-jwt/jwt/v5 v5.2.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/golang/snappy v1.0.0 // indirect
github.com/google/brotli/go/cbrotli v0.0.0-20241111155135-4303850b01d6 // indirect
github.com/google/cel-go v0.25.0 // indirect
github.com/google/certificate-transparency-go v1.3.1 // indirect
github.com/google/go-tpm v0.9.5 // indirect
github.com/google/go-tspi v0.3.0 // indirect
github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7 // indirect
github.com/google/pprof v0.0.0-20250602020802-c6617b811d0e // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/handlers v1.5.2 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.1 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // 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/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.3 // indirect
github.com/jackc/pgio v1.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgproto3/v2 v2.3.3 // indirect
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect
github.com/jackc/pgtype v1.14.2 // indirect
github.com/jackc/pgx/v4 v4.18.3 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.7.5 // 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.7 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // 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.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/libdns/libdns v1.1.0 // indirect
github.com/mailru/easyjson v0.9.0 // indirect
github.com/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/maypok86/otter v1.2.0 // indirect
github.com/maypok86/otter v1.2.4 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/mholt/acmez v1.2.0 // indirect
github.com/micromdm/scep/v2 v2.1.0 // indirect
github.com/miekg/dns v1.1.58 // indirect
github.com/mholt/acmez/v3 v3.1.2 // indirect
github.com/miekg/dns v1.1.66 // 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.15.0 // indirect
github.com/pelletier/go-toml/v2 v2.1.1 // 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/onsi/ginkgo/v2 v2.23.4 // 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.19.0 // indirect
github.com/prometheus/client_model v0.6.0 // indirect
github.com/prometheus/common v0.50.0 // indirect
github.com/prometheus/procfs v0.13.0 // indirect
github.com/quic-go/qpack v0.4.0 // indirect
github.com/quic-go/quic-go v0.41.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.64.0 // indirect
github.com/prometheus/procfs v0.16.1 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.52.0 // indirect
github.com/rs/xid v1.6.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/shopspring/decimal v1.3.1 // indirect
github.com/sagikazarmark/locafero v0.9.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.2 // indirect
github.com/smallstep/certificates v0.25.0 // indirect
github.com/smallstep/nosql v0.6.0 // indirect
github.com/slackhq/nebula v1.9.5 // indirect
github.com/smallstep/certificates v0.28.3 // indirect
github.com/smallstep/cli-utils v0.12.1 // indirect
github.com/smallstep/linkedca v0.23.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.11.0 // indirect
github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/viper v1.18.2 // indirect
github.com/spf13/afero v1.14.0 // indirect
github.com/spf13/cast v1.9.2 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/spf13/viper v1.20.1 // indirect
github.com/stoewer/go-strcase v1.3.0 // 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.1 // indirect
github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53 // indirect
github.com/tidwall/gjson v1.18.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
github.com/unrolled/secure v1.14.0 // indirect
github.com/urfave/cli v1.22.14 // indirect
github.com/unrolled/secure v1.17.0 // indirect
github.com/urfave/cli v1.22.16 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
github.com/yuin/goldmark v1.7.0 // indirect
github.com/yuin/goldmark v1.7.12 // 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.9 // indirect
go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.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.23.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.23.0 // indirect
go.opentelemetry.io/otel/sdk v1.21.0 // indirect
go.opentelemetry.io/otel/trace v1.23.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.36.0 // indirect
go.step.sm/linkedca v0.20.1 // indirect
go.uber.org/mock v0.4.0 // indirect
github.com/zeebo/blake3 v0.2.4 // indirect
go.etcd.io/bbolt v1.4.0 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
go.opentelemetry.io/contrib/propagators/autoprop v0.60.0 // indirect
go.opentelemetry.io/contrib/propagators/aws v1.35.0 // indirect
go.opentelemetry.io/contrib/propagators/b3 v1.35.0 // indirect
go.opentelemetry.io/contrib/propagators/jaeger v1.35.0 // indirect
go.opentelemetry.io/contrib/propagators/ot v1.35.0 // indirect
go.opentelemetry.io/otel v1.35.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 // indirect
go.opentelemetry.io/otel/metric v1.35.0 // indirect
go.opentelemetry.io/otel/sdk v1.35.0 // indirect
go.opentelemetry.io/otel/trace v1.35.0 // indirect
go.opentelemetry.io/proto/otlp v1.6.0 // indirect
go.step.sm/crypto v0.66.0 // indirect
go.uber.org/automaxprocs v1.6.0 // indirect
go.uber.org/mock v0.5.2 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.21.0 // indirect
golang.org/x/exp v0.0.0-20240314144324-c7f7c6466f7f // indirect
golang.org/x/mod v0.15.0 // indirect
golang.org/x/net v0.22.0 // indirect
golang.org/x/sync v0.6.0 // indirect
golang.org/x/sys v0.18.0 // indirect
golang.org/x/term v0.18.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/tools v0.18.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240228224816-df926f6c8641 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240228224816-df926f6c8641 // indirect
google.golang.org/grpc v1.62.0 // indirect
google.golang.org/protobuf v1.33.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
go.uber.org/zap v1.27.0 // indirect
go.uber.org/zap/exp v0.3.0 // indirect
golang.org/x/crypto v0.38.0 // indirect
golang.org/x/crypto/x509roots/fallback v0.0.0-20250531095911-4f9f0ca9fcfb // indirect
golang.org/x/exp v0.0.0-20250531010427-b6e5de432a8b // indirect
golang.org/x/mod v0.24.0 // indirect
golang.org/x/net v0.40.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/sync v0.14.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/term v0.32.0 // indirect
golang.org/x/text v0.25.0 // indirect
golang.org/x/time v0.11.0 // indirect
golang.org/x/tools v0.33.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect
google.golang.org/grpc v1.72.2 // indirect
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 // indirect
google.golang.org/protobuf v1.36.6 // 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

View File

@@ -6,7 +6,7 @@ import (
"path/filepath"
caddycmd "github.com/caddyserver/caddy/v2/cmd"
"github.com/dunglas/frankenphp"
"frankenphp.dev"
"github.com/spf13/cobra"
)
@@ -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,9 +3,9 @@ package caddy
import (
"encoding/json"
"log"
"log/slog"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"time"
@@ -20,16 +20,14 @@ import (
"github.com/caddyserver/caddy/v2/modules/caddyhttp/fileserver"
"github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite"
"github.com/caddyserver/certmagic"
"github.com/dunglas/frankenphp"
"go.uber.org/zap"
"frankenphp.dev"
"github.com/spf13/cobra"
)
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] [--mercure]",
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,
@@ -48,10 +46,14 @@ For more advanced use cases, see https://github.com/dunglas/frankenphp/blob/main
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("mercure", "m", false, "Enable the built-in Mercure.rocks hub")
cmd.Flags().BoolP("no-compress", "", false, "Disable Zstandard, Brotli and Gzip compression")
cmd.Flags().Bool("no-compress", false, "Disable Zstandard, Brotli and Gzip compression")
cmd.Flags().Lookup("watch").NoOptDefVal = defaultWatchPattern
cmd.RunE = caddycmd.WrapCommandFuncForCobra(cmdPHPServer)
},
})
@@ -73,27 +75,35 @@ func cmdPHPServer(fs caddycmd.Flags) (int, error) {
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(filepath.Join(frankenphp.EmbeddedAppPath, "php.ini")); err == nil {
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 {
@@ -101,8 +111,8 @@ func cmdPHPServer(fs caddycmd.Flags) (int, error) {
}
}
if _, err := os.Stat(filepath.Join(frankenphp.EmbeddedAppPath, "Caddyfile")); err == nil {
config, _, err := caddycmd.LoadConfig(filepath.Join(frankenphp.EmbeddedAppPath, "Caddyfile"), "")
if _, err := os.Stat("Caddyfile"); err == nil {
config, _, err := caddycmd.LoadConfig("Caddyfile", "caddyfile")
if err != nil {
return caddy.ExitCodeFailedStartup, err
}
@@ -115,14 +125,12 @@ func cmdPHPServer(fs caddycmd.Flags) (int, error) {
}
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
@@ -173,7 +181,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)
}
@@ -213,15 +221,30 @@ func cmdPHPServer(fs caddycmd.Flags) (int, error) {
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),
"br": caddyconfig.JSON(br.New(), nil),
"gzip": caddyconfig.JSON(gzip.New(), nil),
},
Prefer: []string{"zstd", "br", "gzip"},
EncodingsRaw: encodings,
Prefer: prefer,
}, "handler", "encode", nil)},
}
@@ -295,12 +318,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{
@@ -313,7 +336,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")
}

262
cgi.go
View File

@@ -1,84 +1,63 @@
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 (
"crypto/tls"
"net"
"net/http"
"path/filepath"
"runtime"
"strings"
"unsafe"
"frankenphp.dev/internal/phpheaders"
)
type serverKey int
const (
contentLength serverKey = iota
documentRoot
documentUri
gatewayInterface
httpHost
https
pathInfo
phpSelf
remoteAddr
remoteHost
remotePort
requestScheme
scriptFilename
scriptName
serverName
serverPort
serverProtocol
serverSoftware
sslProtocol
)
var knownServerKeys = map[string]struct{}{
"CONTENT_LENGTH\x00": {},
"DOCUMENT_ROOT\x00": {},
"DOCUMENT_URI\x00": {},
"GATEWAY_INTERFACE\x00": {},
"HTTP_HOST\x00": {},
"HTTPS\x00": {},
"PATH_INFO\x00": {},
"PHP_SELF\x00": {},
"REMOTE_ADDR\x00": {},
"REMOTE_HOST\x00": {},
"REMOTE_PORT\x00": {},
"REQUEST_SCHEME\x00": {},
"SCRIPT_FILENAME\x00": {},
"SCRIPT_NAME\x00": {},
"SERVER_NAME\x00": {},
"SERVER_PORT\x00": {},
"SERVER_PROTOCOL\x00": {},
"SERVER_SOFTWARE\x00": {},
"SSL_PROTOCOL\x00": {},
}
func setKnownServerVariable(p *runtime.Pinner, cArr *[27]C.go_string, serverKey serverKey, val string) {
if val == "" {
return
}
valData := unsafe.StringData(val)
p.Pin(valData)
cArr[serverKey].len = C.size_t(len(val))
cArr[serverKey].data = (*C.char)(unsafe.Pointer(valData))
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",
"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, p *runtime.Pinner) (cArr [27]C.go_string) {
fc, fcOK := FromContext(request.Context())
if !fcOK {
panic("not a FrankenPHP request")
}
func addKnownVariablesToServer(thread *phpThread, 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 {
@@ -92,55 +71,26 @@ func computeKnownVariables(request *http.Request, p *runtime.Pinner) (cArr [27]C
ip = strings.Replace(ip, "[", "", 1)
ip = strings.Replace(ip, "]", "", 1)
ra, raOK := fc.env["REMOTE_ADDR\x00"]
if raOK {
setKnownServerVariable(p, &cArr, remoteAddr, ra)
} else {
setKnownServerVariable(p, &cArr, remoteAddr, ip)
}
if rh, ok := fc.env["REMOTE_HOST\x00"]; ok {
setKnownServerVariable(p, &cArr, remoteHost, rh) // For speed, remote host lookups disabled
} else {
if raOK {
setKnownServerVariable(p, &cArr, remoteHost, ip)
} else {
cArr[remoteHost] = cArr[remoteAddr]
}
}
setKnownServerVariable(p, &cArr, remotePort, port)
setKnownServerVariable(p, &cArr, documentRoot, fc.documentRoot)
setKnownServerVariable(p, &cArr, pathInfo, fc.pathInfo)
setKnownServerVariable(p, &cArr, phpSelf, request.URL.Path)
setKnownServerVariable(p, &cArr, documentUri, fc.docURI)
setKnownServerVariable(p, &cArr, scriptFilename, fc.scriptFilename)
setKnownServerVariable(p, &cArr, scriptName, fc.scriptName)
var https string
var sslProtocol string
var rs string
if request.TLS == nil {
rs = "http"
https = ""
sslProtocol = ""
} else {
rs = "https"
if h, ok := fc.env["HTTPS\x00"]; ok {
setKnownServerVariable(p, &cArr, https, h)
} else {
setKnownServerVariable(p, &cArr, https, "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 pr, ok := fc.env["SSL_PROTOCOL\x00"]; ok {
setKnownServerVariable(p, &cArr, sslProtocol, pr)
// (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 {
setKnownServerVariable(p, &cArr, sslProtocol, v)
}
sslProtocol = ""
}
}
setKnownServerVariable(p, &cArr, requestScheme, rs)
reqHost, reqPort, _ := net.SplitHostPort(request.Host)
if reqHost == "" {
@@ -161,23 +111,96 @@ func computeKnownVariables(request *http.Request, p *runtime.Pinner) (cArr [27]C
}
}
setKnownServerVariable(p, &cArr, serverName, reqHost)
if reqPort != "" {
setKnownServerVariable(p, &cArr, serverPort, 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),
)
// These values can not be override
setKnownServerVariable(p, &cArr, contentLength, request.Header.Get("Content-Length"))
setKnownServerVariable(p, &cArr, gatewayInterface, "CGI/1.1")
setKnownServerVariable(p, &cArr, serverProtocol, request.Proto)
setKnownServerVariable(p, &cArr, serverSoftware, "FrankenPHP")
setKnownServerVariable(p, &cArr, httpHost, request.Host) // added here, since not always part of headers
// 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"],
)
}
return
func packCgiVariable(key *C.zend_string, value string) C.ht_key_value_pair {
return C.ht_key_value_pair{key, toUnsafeChar(value), C.size_t(len(value))}
}
func addHeadersToServer(fc *frankenPHPContext, trackVarsArray *C.zval) {
for field, val := range fc.request.Header {
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(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.getRequestContext()
addKnownVariablesToServer(thread, fc, trackVarsArray)
addHeadersToServer(fc, trackVarsArray)
// The Prepared Environment is registered last and can overwrite any previous values
addPreparedEnvToServer(fc, trackVarsArray)
}
// splitPos returns the index where path should
@@ -185,7 +208,7 @@ func computeKnownVariables(request *http.Request, p *runtime.Pinner) (cArr [27]C
//
// 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 {
func splitPos(fc *frankenPHPContext, path string) int {
if len(fc.splitPath) == 0 {
return 0
}
@@ -208,8 +231,6 @@ var tlsProtocolStrings = map[uint16]string{
tls.VersionTLS13: "TLSv1.3",
}
var headerNameReplacer = strings.NewReplacer(" ", "_", "-", "_")
// SanitizedPathJoin performs filepath.Join(root, reqPath) that
// is safe against directory traversal attacks. It uses logic
// similar to that in the Go standard library, specifically
@@ -228,7 +249,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 {
@@ -239,3 +260,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))
}

166
context.go Normal file
View File

@@ -0,0 +1,166 @@
package frankenphp
import (
"context"
"log/slog"
"net/http"
"os"
"strings"
"time"
)
// frankenPHPContext provides contextual information about the Request to handle.
type frankenPHPContext struct {
documentRoot string
splitPath []string
env PreparedEnv
logger *slog.Logger
request *http.Request
originalRequest *http.Request
docURI string
pathInfo string
scriptName string
scriptFilename string
workerName string
// Whether the request is already closed by us
isDone bool
responseWriter http.ResponseWriter
done chan interface{}
startedAt time.Time
}
// fromContext extracts the frankenPHPContext from a context.
func fromContext(ctx context.Context) (fctx *frankenPHPContext, ok bool) {
fctx, ok = ctx.Value(contextKey).(*frankenPHPContext)
return
}
// NewRequestWithContext creates a new FrankenPHP request context.
func NewRequestWithContext(r *http.Request, opts ...RequestOption) (*http.Request, error) {
fc := &frankenPHPContext{
done: make(chan interface{}),
startedAt: time.Now(),
request: r,
}
for _, o := range opts {
if err := o(fc); err != nil {
return nil, err
}
}
if fc.logger == nil {
fc.logger = logger
}
if fc.documentRoot == "" {
if EmbeddedAppPath != "" {
fc.documentRoot = EmbeddedAppPath
} else {
var err error
if fc.documentRoot, err = os.Getwd(); err != nil {
return nil, err
}
}
}
if fc.splitPath == nil {
fc.splitPath = []string{".php"}
}
if fc.env == nil {
fc.env = make(map[string]string)
}
if splitPos := splitPos(fc, r.URL.Path); splitPos > -1 {
fc.docURI = r.URL.Path[:splitPos]
fc.pathInfo = r.URL.Path[splitPos:]
// Strip PATH_INFO from SCRIPT_NAME
fc.scriptName = strings.TrimSuffix(r.URL.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
}
}
// SCRIPT_FILENAME is the absolute path of SCRIPT_NAME
fc.scriptFilename = sanitizedPathJoin(fc.documentRoot, fc.scriptName)
c := context.WithValue(r.Context(), contextKey, fc)
return r.WithContext(c), nil
}
func newDummyContext(requestPath string, opts ...RequestOption) (*frankenPHPContext, error) {
r, err := http.NewRequest(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() bool {
if !strings.Contains(fc.request.URL.Path, "\x00") {
return true
}
fc.rejectBadRequest("Invalid request path")
return false
}
func (fc *frankenPHPContext) clientHasClosed() bool {
select {
case <-fc.request.Context().Done():
return true
default:
return false
}
}
// reject sends a response with the given status code and message
func (fc *frankenPHPContext) reject(statusCode int, message string) {
if fc.isDone {
return
}
rw := fc.responseWriter
if rw != nil {
rw.WriteHeader(statusCode)
_, _ = rw.Write([]byte(message))
if f, ok := rw.(http.Flusher); ok {
f.Flush()
}
}
fc.closeContext()
}
func (fc *frankenPHPContext) rejectBadRequest(message string) {
fc.reject(http.StatusBadRequest, message)
}

46
debugstate.go Normal file
View File

@@ -0,0 +1,46 @@
package frankenphp
// 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(stateReserved) {
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,8 +1,11 @@
# syntax=docker/dockerfile:1
FROM golang:1.22-alpine
#checkov:skip=CKV_DOCKER_2
#checkov:skip=CKV_DOCKER_3
FROM golang:1.24-alpine
ENV GOTOOLCHAIN=local
ENV CFLAGS="-ggdb3"
ENV PHPIZE_DEPS \
ENV PHPIZE_DEPS="\
autoconf \
dpkg-dev \
file \
@@ -11,7 +14,9 @@ ENV PHPIZE_DEPS \
libc-dev \
make \
pkgconfig \
re2c
re2c"
SHELL ["/bin/ash", "-eo", "pipefail", "-c"]
RUN apk add --no-cache \
$PHPIZE_DEPS \
@@ -27,9 +32,13 @@ RUN apk add --no-cache \
zlib-dev \
bison \
nss-tools \
# file watcher
libstdc++ \
linux-headers \
# Dev tools \
git \
clang \
cmake \
llvm \
gdb \
valgrind \
@@ -39,23 +48,33 @@ RUN apk add --no-cache \
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 . && \
RUN git clone --branch=PHP-8.4 https://github.com/php/php-src.git . && \
# --enable-embed is only necessary to generate libphp.so, we don't use this SAPI directly
./buildconf --force && \
./configure \
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 && \
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 && \
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 . .

View File

@@ -1,8 +1,11 @@
# syntax=docker/dockerfile:1
FROM golang:1.22
#checkov:skip=CKV_DOCKER_2
#checkov:skip=CKV_DOCKER_3
FROM golang:1.24
ENV GOTOOLCHAIN=local
ENV CFLAGS="-ggdb3"
ENV PHPIZE_DEPS \
ENV PHPIZE_DEPS="\
autoconf \
dpkg-dev \
file \
@@ -11,7 +14,9 @@ ENV PHPIZE_DEPS \
libc-dev \
make \
pkg-config \
re2c
re2c"
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
# hadolint ignore=DL3009
RUN apt-get update && \
@@ -32,6 +37,7 @@ RUN apt-get update && \
# Dev tools \
git \
clang \
cmake \
llvm \
gdb \
valgrind \
@@ -41,31 +47,43 @@ RUN apt-get update && \
echo 'set auto-load safe-path /' > /root/.gdbinit && \
echo '* soft core unlimited' >> /etc/security/limits.conf \
&& \
apt-get clean
apt-get clean
WORKDIR /usr/local/src/php
RUN git clone --branch=PHP-8.3 https://github.com/php/php-src.git . && \
RUN git clone --branch=PHP-8.4 https://github.com/php/php-src.git . && \
# --enable-embed is only necessary to generate libphp.so, we don't use this SAPI directly
./buildconf --force && \
./configure \
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 && \
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 && \
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 . .
WORKDIR /go/src/app/caddy/frankenphp
RUN go build
RUN go build -buildvcs=false -tags 'nobadger,nomysql,nopgx'
WORKDIR /go/src/app
CMD [ "zsh" ]

View File

@@ -7,17 +7,17 @@ variable "VERSION" {
}
variable "PHP_VERSION" {
default = "8.2,8.3"
default = "8.2,8.3,8.4"
}
variable "GO_VERSION" {
default = "1.22"
default = "1.24"
}
variable "SHA" {}
variable "LATEST" {
default = false
default = true
}
variable "CACHE" {
@@ -25,20 +25,20 @@ variable "CACHE" {
}
variable DEFAULT_PHP_VERSION {
default = "8.3"
default = "8.4"
}
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 == "bookworm" && 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 == "bookworm" && 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 +60,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" {
@@ -91,7 +91,8 @@ target "default" {
platforms = os == "alpine" ? [
"linux/amd64",
"linux/386",
"linux/arm/v6",
# FIXME: armv6 doesn't build in GitHub actions because we use a custom Go build
#"linux/arm/v6",
"linux/arm/v7",
"linux/arm64",
] : [
@@ -103,8 +104,8 @@ target "default" {
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 = {
@@ -117,20 +118,20 @@ target "default" {
}
}
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) : v == "latest" ? "${IMAGE_NAME}:static-builder": "${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()}"
@@ -142,3 +143,27 @@ target "static-builder" {
}
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
}
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 prevent that, you can use the `max_wait_time` [configuration](config.md#caddyfile-config) to limit how long a request may 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.

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

@@ -0,0 +1,219 @@
# 贡献
## 编译 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
cd ../../
```
使用 FrankenPHP Caddy 模块运行 Caddy
```console
cd testdata/
../caddy/frankenphp/frankenphp run
```
服务器正在监听 `127.0.0.1:8080`
```console
curl -vk https://localhost/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.dev.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)
- [Mac 上的 PHP 嵌入](https://gist.github.com/jonnywang/61427ffc0e8dde74fff40f479d147db4)
- [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 构建](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. 在站点存储库上打开拉取请求

View File

@@ -1,6 +1,6 @@
# FrankenPHP: 适用于 PHP 的现代应用服务器
<h1 align="center"><a href="https://frankenphp.dev"><img src="frankenphp.png" alt="FrankenPHP" width="600"></a></h1>
<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 应用程序服务器。
@@ -34,7 +34,7 @@ docker run -v $PWD:/app/public \
### 独立二进制
如果您不想使用 Docker我们为 Linux 和 macOS 提供独立的 FrankenPHP 二进制文件
,其中包含 [PHP 8.3](https://www.php.net/releases/8.3/en.php) 和最流行的 PHP 扩展:[下载 FrankenPHP](https://github.com/dunglas/frankenphp/releases)。
,其中包含 [PHP 8.4](https://www.php.net/releases/8.4/en.php) 和最流行的 PHP 扩展:[下载 FrankenPHP](https://github.com/dunglas/frankenphp/releases)。
若要启动当前目录的内容,请运行:
@@ -71,7 +71,8 @@ docker run -v $PWD:/app/public \
* [API Platform](https://api-platform.com/docs/distribution/)
* [Laravel](laravel.md)
* [Sulu](https://sulu.io/blog/running-sulu-with-frankenphp)
* [WordPress](https://github.com/dunglas/frankenphp-wordpress)
* [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)

View File

@@ -16,7 +16,9 @@ tar xf php-*
cd php-*/
```
然后,为您的平台配置 PHP
然后,为您的平台配置 PHP.
这些参数是必需的,但你也可以添加其他编译参数(例如额外的扩展)。
### Linux
@@ -28,13 +30,6 @@ cd php-*/
--enable-zend-max-execution-timers
```
最后,编译并安装 PHP
```console
make -j$(nproc)
sudo make install
```
### Mac
使用 [Homebrew](https://brew.sh/) 包管理器安装 `libiconv``bison``re2c``pkg-config`
@@ -57,12 +52,12 @@ echo 'export PATH="/opt/homebrew/opt/bison/bin:$PATH"' >> ~/.zshrc
--with-iconv=/opt/homebrew/opt/libiconv/
```
这些参数是必需的,但你也可以添加其他编译参数(例如额外的扩展)。
## 编译并安装 PHP
最后,编译并安装 PHP
```console
make -j$(sysctl -n hw.logicalcpu)
make -j"$(getconf _NPROCESSORS_ONLN)"
sudo make install
```
@@ -71,7 +66,7 @@ sudo make install
您现在可以使用 Go 库并编译我们的 Caddy 构建:
```console
curl -L https://github.com/dunglas/frankenphp/archive/refs/heads/main.tar.gz | tar x
curl -L https://github.com/dunglas/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
```
@@ -85,7 +80,8 @@ CGO_ENABLED=1 \
XCADDY_GO_BUILD_FLAGS="-ldflags '-w -s'" \
xcaddy build \
--output frankenphp \
--with github.com/dunglas/frankenphp/caddy \
--with frankenphp.dev/caddy \
--with github.com/dunglas/caddy-cbrotli \
--with github.com/dunglas/mercure/caddy \
--with github.com/dunglas/vulcain/caddy
# Add extra Caddy modules here

View File

@@ -1,37 +1,49 @@
# 配置
# 配置
FrankenPHPCaddy 以及 Mercure 和 Vulcain 模块可以使用 [Caddy 支持的格式](https://caddyserver.com/docs/getting-started#your-first-config) 进行配置。
Docker 镜像中,`Caddyfile` 位于 `/etc/caddy/Caddyfile`
[Docker 映像](docker.md) 中,`Caddyfile` 位于 `/etc/frankenphp/Caddyfile`
静态二进制文件会在启动时所在的目录中查找 `Caddyfile`
PHP 本身可以[使用 `php.ini` 文件](https://www.php.net/manual/zh/configuration.file.php)进行配置。
PHP 解释器将在以下位置查找:
您也可以像往常一样使用 `php.ini` 配置 PHP。
Docker:
在 Docker 镜像中,默认不存在 `php.ini`,您可以手动创建它或从官方模板中复制
- 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-development $PHP_INI_DIR/php.ini
# 生产:
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
```
FrankenPHP 安装 (.rpm 或 .deb):
- 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 配置
要注册 FrankenPHP 执行器,必须设置 `frankenphp` [全局选项](https://caddyserver.com/docs/caddyfile/concepts#global-options),然后可以在站点块中使用 `php_server``php` [HTTP 指令](https://caddyserver.com/docs/caddyfile/concepts#directives) 来为您的 PHP 应用程序提供服务。
可以在站点块中使用 `php_server``php` [HTTP 指令](https://caddyserver.com/docs/caddyfile/concepts#directives) 来为您的 PHP 应用程序提供服务。
最小示例:
```caddyfile
{
# 启用 FrankenPHP
frankenphp
# 配置何时必须执行指令
order php_server before file_server
}
localhost {
# 启用压缩(可选)
encode zstd br gzip
@@ -40,7 +52,8 @@ localhost {
}
```
或者,可以在全局选项下指定要创建的线程数和要从服务器启动的 [worker 脚本](worker.md)。
您也可以使用全局选项显式配置 FrankenPHP
`frankenphp` [全局选项](https://caddyserver.com/docs/caddyfile/concepts#global-options) 可用于配置 FrankenPHP。
```caddyfile
{
@@ -72,21 +85,18 @@ localhost {
如果在同一服务器上运行多个应用,还可以定义多个 worker
```caddyfile
{
frankenphp {
worker /path/to/app/public/index.php <num>
worker /path/to/other/public/index.php <num>
app.example.com {
php_server {
root /path/to/app/public
worker index.php <num>
}
}
app.example.com {
root * /path/to/app/public
php_server
}
other.example.com {
root * /path/to/other/public
php_server
php_server {
root /path/to/other/public
worker index.php <num>
}
}
# ...
```
@@ -122,9 +132,18 @@ route {
```caddyfile
php_server [<matcher>] {
root <directory> # 设置站点的根目录。默认值:`root` 指令。
split_path <delim...> # 设置用于将 URI 拆分为两部分的子字符串。第一个匹配的子字符串将用于从路径中拆分路径信息。第一个部分以匹配的子字符串为后缀,并将假定为实际资源(CGI 脚本)名称。第二部分将设置为PATH_INFO供脚本使用。默认值`.php`
split_path <delim...> # 设置用于将 URI 拆分为两部分的子字符串。第一个匹配的子字符串将用于从路径中拆分"路径信息"。第一个部分以匹配的子字符串为后缀,并将假定为实际资源(CGI 脚本)名称。第二部分将设置为PATH_INFO供脚本使用。默认值`.php`
resolve_root_symlink false # 禁用将 `root` 目录在符号链接时将其解析为实际值(默认启用)。
env <key> <value> # 设置额外的环境变量,可以设置多个环境变量。
file_server off # 禁用内置的 file_server 指令。
worker { # 创建特定于此服务器的 worker。可以为多个 worker 多次指定。
file <path> # 设置 worker 脚本的路径,可以相对于 php_server 根目录
num <num> # 设置要启动的 PHP 线程数,默认为可用 CPU 数的 2 倍
name <name> # 为 worker 设置名称用于日志和指标。默认值worker 文件的绝对路径。在 php_server 块中定义时始终以 m# 开头。
watch <path> # 设置要监视文件更改的路径。可以为多个路径多次指定。
env <key> <value> # 将额外的环境变量设置为给定值。可以为多个环境变量多次指定。此 worker 的环境变量也从 php_server 父级继承,但可以在此处覆盖。
}
worker <other_file> <num> # 也可以像在全局 frankenphp 块中一样使用简短形式。
}
```
@@ -132,9 +151,9 @@ php_server [<matcher>] {
以下环境变量可用于在 `Caddyfile` 中注入 Caddy 指令,而无需对其进行修改:
* `SERVER_NAME`: 更改 [要监听的地址](https://caddyserver.com/docs/caddyfile/concepts#addresses),提供的主机名也将用于生成的 TLS 证书
* `CADDY_GLOBAL_OPTIONS`: 注入 [全局选项](https://caddyserver.com/docs/caddyfile/options)
* `FRANKENPHP_CONFIG`: 在 `frankenphp` 指令下注入配置
- `SERVER_NAME`: 更改 [要监听的地址](https://caddyserver.com/docs/caddyfile/concepts#addresses),提供的主机名也将用于生成的 TLS 证书
- `CADDY_GLOBAL_OPTIONS`: 注入 [全局选项](https://caddyserver.com/docs/caddyfile/options)
- `FRANKENPHP_CONFIG`: 在 `frankenphp` 指令下注入配置
## PHP 配置

View File

@@ -1,7 +1,7 @@
# 构建自定义 Docker 镜像
[FrankenPHP Docker 镜像](https://hub.docker.com/r/dunglas/frankenphp) 基于 [官方 PHP 镜像](https://hub.docker.com/_/php/)。
Alpine Linux 和 Debian 衍生版适用于常见的处理器架构,支持 PHP 8.2 和 PHP 8.3。[查看 Tags](https://hub.docker.com/r/dunglas/frankenphp/tags)。
Alpine Linux 和 Debian 衍生版适用于常见的处理器架构,支持 PHP 8.2、8.3 和 8.4。。[查看 Tags](https://hub.docker.com/r/dunglas/frankenphp/tags)。
## 如何使用镜像
@@ -30,11 +30,11 @@ FROM dunglas/frankenphp
# 在此处添加其他扩展:
RUN install-php-extensions \
pdo_mysql \
gd \
intl \
zip \
opcache
pdo_mysql \
gd \
intl \
zip \
opcache
```
## 如何安装更多 Caddy 模块
@@ -44,21 +44,26 @@ FrankenPHP 建立在 Caddy 之上,所有 [Caddy 模块](https://caddyserver.co
安装自定义 Caddy 模块的最简单方法是使用 [xcaddy](https://github.com/caddyserver/xcaddy)
```dockerfile
FROM dunglas/frankenphp:latest-builder AS builder
FROM dunglas/frankenphp:builder AS builder
# 在构建器镜像中复制 xcaddy
COPY --from=caddy:builder /usr/bin/xcaddy /usr/bin/xcaddy
# 必须启用 CGO 才能构建 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 和 Vulcain 包含在官方版本中,如果不需要你可以删除它们
--with github.com/dunglas/mercure/caddy \
--with github.com/dunglas/vulcain/caddy
# 在此处添加额外的 Caddy 模块
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 frankenphp.dev=./ \
--with frankenphp.dev/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
@@ -136,15 +141,15 @@ FrankenPHP 可以在 Docker 中以非 root 用户身份运行。
```dockerfile
FROM dunglas/frankenphp
ARG USER=www-data
ARG USER=appuser
RUN \
# 在基于 alpine 的发行版使用 "adduser -D ${USER}"
useradd -D ${USER}; \
# 需要开放80和443端口的权限
setcap CAP_NET_BIND_SERVICE=+eip /usr/local/bin/frankenphp; \
# 需要 /data/caddy 和 /config/caddy 目录的写入权限
chown -R ${USER}:${USER} /data/caddy && chown -R ${USER}:${USER} /config/caddy;
# 在基于 alpine 的发行版使用 "adduser -D ${USER}"
useradd ${USER}; \
# 需要开放80和443端口的权限
setcap CAP_NET_BIND_SERVICE=+eip /usr/local/bin/frankenphp; \
# 需要 /data/caddy 和 /config/caddy 目录的写入权限
chown -R ${USER}:${USER} /data/caddy && chown -R ${USER}:${USER} /config/caddy;
USER ${USER}
```

View File

@@ -1,41 +1,11 @@
# 已知问题
## Fibers
在 [Fibers](https://www.php.net/manual/en/language.fibers.php) 中调用 PHP 的函数和代码等语言结构,这些结构内部再调用 [cgo](https://go.dev/blog/cgo) 会导致崩溃。
这个问题 [正在由 Go 项目处理](https://github.com/golang/go/issues/62130)。
一种解决方案是不要使用从 Fibers 内部委托给 Go 的构造(如 `echo`)和函数(如 `header()`)。
下面的代码可能会崩溃,因为它在 Fiber 中使用了 `echo`
```php
$fiber = new Fiber(function() {
echo 'In the Fiber'.PHP_EOL;
echo 'Still inside'.PHP_EOL;
});
$fiber->start();
```
相反,请从 Fiber 返回值并在外部使用它:
```php
$fiber = new Fiber(function() {
Fiber::suspend('In the Fiber'.PHP_EOL));
Fiber::suspend('Still inside'.PHP_EOL));
});
echo $fiber->start();
echo $fiber->resume();
$fiber->resume();
```
## 不支持的 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) |
## get_browser
@@ -91,7 +61,7 @@ docker run \
```console
docker run \
-e CADDY_GLOBAL_OPTIONS="debug"
-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 \

View File

@@ -22,13 +22,12 @@ docker run -p 80:80 -p 443:443 -p 443:443/udp -v $PWD:/app dunglas/frankenphp
```caddyfile
{
frankenphp
order php_server before file_server
}
# 服务器的域名
localhost {
# 将 webroot 设置为 public/ 目录
root * public/
root public/
# 启用压缩(可选)
encode zstd br gzip
# 执行当前目录中的 PHP 文件并提供资产
@@ -36,7 +35,7 @@ docker run -p 80:80 -p 443:443 -p 443:443/udp -v $PWD:/app dunglas/frankenphp
}
```
3. 从 Laravel 项目的根目录启动 FrankenPHP`./frankenphp run`
3. 从 Laravel 项目的根目录启动 FrankenPHP`frankenphp run`
## Laravel Octane
@@ -52,13 +51,13 @@ composer require laravel/octane
php artisan octane:install --server=frankenphp
```
Octane 服务可以通过 `octane:start` Artisan 命令启动。
Octane 服务可以通过 `octane:frankenphp` Artisan 命令启动。
```console
php artisan octane:start
php artisan octane:frankenphp
```
`octane:start` 命令可以采用以下选项:
`octane:frankenphp` 命令可以采用以下选项:
* `--host`: 服务器应绑定到的 IP 地址(默认值: `127.0.0.1`
* `--port`: 服务器应可用的端口(默认值: `8000`

View File

@@ -5,7 +5,7 @@ Mercure 允许将事件实时推送到所有连接的设备:它们将立即收
无需 JS 库或 SDK
![Mercure](https://mercure.rocks/static/main.png)
![Mercure](../mercure-hub.png)
要启用 Mercure Hub请按照 [Mercure 网站](https://mercure.rocks/docs/hub/config) 中的说明更新 `Caddyfile`

View File

@@ -13,7 +13,7 @@ FrankenPHP 还支持 [将 PHP 应用程序嵌入到静态二进制文件中](emb
```console
docker buildx bake --load static-builder
docker cp $(docker create --name static-builder dunglas/frankenphp:static-builder):/go/src/app/dist/frankenphp-linux-$(uname -m) frankenphp ; docker rm static-builder
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
```
生成的静态二进制文件名为 `frankenphp`,可在当前目录中找到。
@@ -43,6 +43,24 @@ docker buildx bake \
static-builder
```
### 额外的 Caddy 模块
要向 [xcaddy](https://github.com/caddyserver/xcaddy) 添加额外的 Caddy 模块或传递其他参数,请使用 `XCADDY_ARGS` Docker 参数:
```console
docker buildx bake \
--load \
--set static-builder.args.XCADDY_ARGS="--with github.com/darkweak/souin/plugins/caddy --with github.com/dunglas/caddy-cbrotli --with github.com/dunglas/mercure/caddy --with github.com/dunglas/vulcain/caddy" \
static-builder
```
在本例中,我们为 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
@@ -75,6 +93,7 @@ cd 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 及其所有依赖项都是重新构建的(不使用缓存)
* `DEBUG_SYMBOLS`: 设置后,调试符号将被保留在二进制文件内

View File

@@ -22,7 +22,7 @@ docker run \
使用 `php-server` 命令的 `--worker` 选项, 执行命令使当前目录的内容使用 worker
```console
./frankenphp php-server --worker /path/to/your/worker/script.php
frankenphp php-server --worker /path/to/your/worker/script.php
```
## Symfony Runtime
@@ -68,11 +68,11 @@ $myApp->boot();
// 循环外的处理程序以获得更好的性能(减少工作量)
$handler = static function () use ($myApp) {
// 收到请求时调用
// 超全局变量 php://input
echo $myApp->handle($_GET, $_POST, $_COOKIE, $_FILES, $_SERVER);
// 收到请求时调用
// 超全局变量 php://input
echo $myApp->handle($_GET, $_POST, $_COOKIE, $_FILES, $_SERVER);
};
for($nbRequests = 0, $running = true; isset($_SERVER['MAX_REQUESTS']) && ($nbRequests < ((int)$_SERVER['MAX_REQUESTS'])) && $running; ++$nbRequests) {
for ($nbRequests = 0, $running = true; isset($_SERVER['MAX_REQUESTS']) && ($nbRequests < ((int)$_SERVER['MAX_REQUESTS'])) && $running; ++$nbRequests) {
$running = \frankenphp_handle_request($handler);
// 发送 HTTP 响应后执行某些操作

View File

@@ -1,24 +1,42 @@
# Compile From Sources
This document explain how to create a FrankenPHP build that will load PHP as a dynamic 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,51 @@ 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 |
## 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 frankenphp.dev/caddy \
--with github.com/dunglas/mercure/caddy \
--with github.com/dunglas/vulcain/caddy
# Add extra Caddy modules here
# Add extra Caddy modules and FrankenPHP extensions here
```
> [!TIP]
@@ -101,4 +114,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/dunglas/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

@@ -2,36 +2,51 @@
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).
In the Docker image, the `Caddyfile` is located at `/etc/caddy/Caddyfile`.
In [the Docker images](docker.md), the `Caddyfile` is located at `/etc/frankenphp/Caddyfile`.
The static binary will also look for the `Caddyfile` in the directory where the `frankenphp run` command is executed.
You can specify a custom path with the `-c` or `--config` option.
You can also configure PHP using `php.ini` as usual.
PHP itself can be configured [using a `php.ini` file](https://www.php.net/manual/en/configuration.file.php).
In the Docker image, the `php.ini` file is not present, you can create it manually or copy an official template:
Depending on your installation method, the PHP interpreter will look for configuration files in locations described above.
## Docker
- `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
# Developement:
RUN cp $PHP_INI_DIR/php.ini-development $PHP_INI_DIR/php.ini
# Or production:
# 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
- `php.ini`: `/etc/frankenphp/php.ini` (a `php.ini` file with production presets is provided by default)
- additional configuration files: `/etc/frankenphp/php.d/*.ini`
- PHP extensions: `/usr/lib/frankenphp/modules/`
## Static binary
- `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 br gzip
@@ -40,16 +55,22 @@ localhost {
}
```
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:
The `frankenphp` [global option](https://caddyserver.com/docs/caddyfile/concepts#global-options) can be used to configure 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
}
}
}
@@ -72,28 +93,27 @@ 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 {
php_server {
root /path/to/app/public
worker index.php <num>
}
}
app.example.com {
root * /path/to/app/public
php_server
}
other.example.com {
root * /path/to/other/public
php_server
php_server {
root /path/to/other/public
worker index.php <num>
}
}
# ...
```
Using the `php_server` directive is generally what you need,
but if you need full control, you can use the lower level `php` directive:
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).
Using the `php_server` directive is equivalent to this configuration:
@@ -126,16 +146,98 @@ php_server [<matcher>] {
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.
}
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).
### Full Duplex (HTTP/1)
When using HTTP/1.x, it may be desirable to enable full-duplex mode to allow writing a response before the entire body
has been read. (for example: WebSocket, Server-Sent Events, etc.)
This is an opt-in configuration that needs to be added to the global options in the `Caddyfile`:
```caddyfile
{
servers {
enable_full_duplex
}
}
```
> [!CAUTION]
>
> Enabling this option may cause old HTTP/1.x clients that don't support full-duplex to deadlock.
> This can also be configured using the `CADDY_GLOBAL_OPTIONS` environment config:
```sh
CADDY_GLOBAL_OPTIONS="servers {
enable_full_duplex
}"
```
You can find more information about this setting in the [Caddy documentation](https://caddyserver.com/docs/caddyfile/options#enable-full-duplex).
## Environment Variables
The following environment variables can be used to inject Caddy directives in the `Caddyfile` without modifying it:
* `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
* `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
- `CADDY_GLOBAL_OPTIONS`: inject [global options](https://caddyserver.com/docs/caddyfile/options)
- `FRANKENPHP_CONFIG`: inject config under the `frankenphp` directive
As for FPM and CLI SAPIs, environment variables are exposed by default in the `$_SERVER` superglobal.
@@ -147,6 +249,23 @@ To load [additional PHP configuration files](https://www.php.net/manual/en/confi
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
}
}
}
```
## Enable the Debug Mode
When using the Docker image, set the `CADDY_GLOBAL_OPTIONS` environment variable to `debug` to enable the debug mode:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 159 KiB

After

Width:  |  Height:  |  Size: 87 KiB

View File

@@ -2,7 +2,14 @@
[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 and PHP 8.3 are provided. [Browse tags](https://hub.docker.com/r/dunglas/frankenphp/tags).
Variants for PHP 8.2, 8.3 and 8.4 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 `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
@@ -45,22 +52,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/caddy-cbrotli \
--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 frankenphp.dev=./ \
--with frankenphp.dev/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
@@ -96,7 +107,7 @@ To develop easily with FrankenPHP, mount the directory from your host containing
docker run -v $PWD:/app/public -p 80:80 -p 443:443 -p 443:443/udp --tty my-php-app
```
> ![TIP]
> [!TIP]
>
> The `--tty` option allows to have nice human-readable logs instead of JSON logs.
@@ -138,25 +149,52 @@ Here is a sample `Dockerfile` doing this:
```dockerfile
FROM dunglas/frankenphp
ARG USER=www-data
ARG USER=appuser
RUN \
# Use "adduser -D ${USER}" for alpine based distros
useradd -D ${USER}; \
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 /data/caddy and /config/caddy
chown -R ${USER}:${USER} /data/caddy && chown -R ${USER}:${USER} /config/caddy;
chown -R ${USER}:${USER} /data/caddy && chown -R ${USER}:${USER} /config/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 /data/caddy and /config/caddy
chown -R ${USER}:${USER} /data/caddy && chown -R ${USER}:${USER} /config/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
- when a new release is tagged
- daily at 4 am UTC, if new versions of the official PHP images are available
## Development Versions

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 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,42 +42,45 @@ 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
# 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.
> [!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:
```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.
@@ -85,9 +91,7 @@ If you don't want to use Docker, or want to build a macOS binary, use the shell
```console
git clone https://github.com/dunglas/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.
@@ -108,7 +112,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
@@ -120,6 +124,13 @@ 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...).

View File

@@ -11,15 +11,18 @@ 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...).
L'image contient les outils de développement habituels (Go, GDB, Valgrind, Neovim...) et utilise les emplacements de configuration PHP suivants
Si la version de Docker est inférieure à 23.0, la construction échoue à cause d'un [problème de pattern](https://github.com/moby/moby/pull/42676) dans `.dockerignore`. Ajoutez les répertoires à `.dockerignore`.
- 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
+!C-Thread-Pool
+!internal
```
@@ -30,7 +33,7 @@ Si la version de Docker est inférieure à 23.0, la construction échoue à caus
## Exécution de la suite de tests
```console
go test -race -v ./...
go test -tags watcher -race -v ./...
```
## Module Caddy
@@ -39,7 +42,7 @@ Construire Caddy avec le module FrankenPHP :
```console
cd caddy/frankenphp/
go build
go build -tags watcher,brotli,nobadger,nomysql,nopgx
cd ../../
```
@@ -50,10 +53,14 @@ cd testdata/
../caddy/frankenphp/frankenphp run
```
Le serveur est configuré pour écouter à l'adresse `127.0.0.1:8080`:
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 https://localhost/phpinfo.php
curl -vk http://127.0.0.1/phpinfo.php
```
## Serveur de test minimal
@@ -105,69 +112,92 @@ Construire à partir de zéro les images FrankenPHP pour arm64 & amd64 et les po
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
```
```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
```
```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"
```
```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 -c -ldflags=-w
gdb --args ./frankenphp.test -test.run ^MyTest$
```
```console
go test -tags watcher -c -ldflags=-w
gdb --args frankenphp.dev.test -test.run ^MyTest$
```
9. Quand le bug est corrigé, annulez tous les changements
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)
- [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/)
- [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
@@ -175,3 +205,16 @@ 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
```
## 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.

View File

@@ -4,7 +4,7 @@
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 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.
@@ -16,10 +16,38 @@ Découvrez plus de détails sur ce serveur dapplication dans le replay de cet
## Pour Commencer
### Docker
### Binaire autonome
Si vous préférez ne pas utiliser Docker, nous fournissons des binaires autonomes de FrankenPHP pour Linux et macOS
contenant [PHP 8.4](https://www.php.net/releases/8.4/fr.php) et la plupart des extensions PHP populaires.
Sous Windows, utilisez [WSL](https://learn.microsoft.com/windows/wsl/) pour exécuter FrankenPHP.
[Téléchargez FrankenPHP](https://github.com/dunglas/frankenphp/releases) ou copiez cette ligne dans votre terminal pour installer automatiquement la version appropriée à votre plateforme :
```console
docker run -v $PWD:/app/public \
curl https://frankenphp.dev/install.sh | sh
mv frankenphp /usr/local/bin/
```
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
```
### 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
```
@@ -29,49 +57,53 @@ 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#environment-variables) pour changer le domaine à utiliser.
> Utilisez [la variable d'environnement `SERVER_NAME`](config.md#variables-denvironnement) pour changer le domaine à utiliser.
### Binaire autonome
### Homebrew
Si vous préférez ne pas utiliser Docker, nous fournissons des binaires autonomes de FrankenPHP pour Linux et macOS
contenant [PHP 8.3](https://www.php.net/releases/8.3/fr.php) et la plupart des extensions PHP populaires : [Télécharger FrankenPHP](https://github.com/dunglas/frankenphp/releases)
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
```
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
frankenphp php-server
```
## Documentation
* [Le mode worker](worker.md)
* [Le support des Early Hints (code de statut HTTP 103)](early-hints.md)
* [Temps réel](mercure.md)
* [Configuration](config.md)
* [Images Docker](docker.md)
* [Déploiement en production](production.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)
* [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)
- [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)
- [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/dunglas/frankenphp-wordpress)
* [Drupal](https://github.com/dunglas/frankenphp-drupal)
* [Joomla](https://github.com/alexandreelise/frankenphp-joomla)
* [TYPO3](https://github.com/ochorocho/franken-typo3)
- [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` 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`.

View File

@@ -1,13 +1,31 @@
# Compiler depuis les sources
Ce document explique comment créer un build FrankenPHP qui chargera PHP comme une bibliothèque dynamique. C'est la méthode recommandée.
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.
En alternative, il est aussi possible de [créer des builds statiques](static.md).
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
@@ -15,7 +33,9 @@ tar xf php-*
cd php-*/
```
Ensuite, configurez PHP pour votre système d'exploitation :
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
@@ -27,19 +47,12 @@ Ensuite, configurez PHP pour votre système d'exploitation :
--enable-zend-max-execution-timers
```
Finalement, compilez et installez PHP :
```console
make -j$(nproc)
sudo make install
```
### Mac
Utilisez le gestionnaire de paquets [Homebrew](https://brew.sh/) pour installer `libiconv`, `bison`, `re2c` et `pkg-config` :
Utilisez le gestionnaire de paquets [Homebrew](https://brew.sh/) pour installer les dépendances obligatoires et optionnelles :
```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
```
@@ -47,47 +60,51 @@ Puis exécutez le script de configuration :
```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/
```
Les options de configuration spécifiées sont nécessaires pour la compilation, mais vous pouvez également inclure d'autres options selon vos besoins, par exemple pour ajouter des extensions supplémentaires.
### Compilez PHP
Finalement, compilez et installez PHP :
```console
make -j$(sysctl -n hw.logicalcpu)
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
Vous pouvez maintenant utiliser la bibliothèque Go et compiler notre build de Caddy :
```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
```
### Utiliser xcaddy
Vous pouvez utiliser à la place [xcaddy](https://github.com/caddyserver/xcaddy) pour compiler FrankenPHP avec [des modules Caddy additionnels](https://caddyserver.com/docs/modules/):
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'" \
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 frankenphp.dev/caddy \
--with github.com/dunglas/caddy-cbrotli \
--with github.com/dunglas/mercure/caddy \
--with github.com/dunglas/vulcain/caddy
# Add extra Caddy modules here
# Ajoutez les modules Caddy supplémentaires et les extensions FrankenPHP ici
```
> [!TIP]
@@ -99,3 +116,13 @@ xcaddy build \
> 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/dunglas/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,57 +1,77 @@
# Configuration
# Configuration
FrankenPHP, Caddy ainsi que les modules Mercure et Vulcain peuvent être configurés en utilisant [les formats pris en charge par Caddy](https://caddyserver.com/docs/getting-started#your-first-config).
Dans l'image Docker, le chemin du `Caddyfile` est `/etc/caddy/Caddyfile`.
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é.
Vous pouvez également configurer PHP en utilisant `php.ini` comme d'habitude.
PHP lui-même peut être configuré [en utilisant un fichier `php.ini`](https://www.php.net/manual/fr/configuration.file.php).
Dans l'image Docker, le fichier `php.ini` n'est pas présent, vous pouvez le créer manuellement ou copier un template officiel :
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
# Développement :
RUN cp $PHP_INI_DIR/php.ini-development $PHP_INI_DIR/php.ini
# Ou production :
# 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
Pour enregistrer l'exécutable de FrankenPHP, l'[option globale](https://caddyserver.com/docs/caddyfile/concepts#global-options) `frankenphp` doit être définie, puis 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.
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
{
# Activer FrankenPHP
frankenphp
# Configurer l'ordre d'exécution de la directive
order php_server before file_server
}
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
# Activer la compression (optionnel)
encode zstd br gzip
# Exécuter les fichiers PHP dans le répertoire courant et servir les assets
php_server
}
```
En option, le nombre de threads à créer et les [workers](worker.md) à démarrer avec le serveur peuvent être spécifiés sous l'option globale.
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.
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.
}
}
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
}
}
}
# ...
@@ -61,9 +81,9 @@ Vous pouvez également utiliser la forme courte de l'option worker en une seule
```caddyfile
{
frankenphp {
worker <file> <num>
}
frankenphp {
worker <file> <num>
}
}
# ...
@@ -72,21 +92,18 @@ Vous pouvez également utiliser la forme courte de l'option worker en une seule
Vous pouvez aussi définir plusieurs workers si vous servez plusieurs applications sur le même serveur :
```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
php_server {
root /path/to/app/public
worker index.php <num>
}
}
other.example.com {
root * /path/to/other/public
php_server
php_server {
root /path/to/other/public
worker index.php <num>
}
}
# ...
@@ -94,27 +111,29 @@ other.example.com {
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).
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
# 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
}
```
@@ -122,20 +141,102 @@ 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.
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é
* `CADDY_GLOBAL_OPTIONS` : injecte [des options globales](https://caddyserver.com/docs/caddyfile/options)
* `FRANKENPHP_CONFIG` : insère la configuration sous la directive `frankenphp`
- `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é
- `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`.
@@ -143,9 +244,27 @@ La valeur `S` de [la directive `variables_order` de PHP](https://www.php.net/man
## 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.
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 :

View File

@@ -2,7 +2,14 @@
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 et PHP 8.3 sont disponibles. [Parcourir les tags](https://hub.docker.com/r/dunglas/frankenphp/tags).
Des variantes pour PHP 8.2, 8.3 et 8.4 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 `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
@@ -31,35 +38,40 @@ FROM dunglas/frankenphp
# ajoutez des extensions supplémentaires ici :
RUN install-php-extensions \
pdo_mysql \
gd \
intl \
zip \
opcache
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):
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:latest-builder AS builder
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
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 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
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 frankenphp.dev=./ \
--with frankenphp.dev/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
@@ -73,7 +85,7 @@ L'image builder fournie par FrankenPHP contient une version compilée de `libphp
> [!TIP]
>
> Si vous utilisez Alpine Linux et Symfony,
> vous devrez peut-être [augmenter la taille de pile par défaut](compile.md#using-xcaddy).
> vous devrez peut-être [augmenter la taille de pile par défaut](compile.md#utiliser-xcaddy).
## Activer le mode Worker par défaut
@@ -95,7 +107,7 @@ Pour développer facilement avec FrankenPHP, montez le répertoire de l'hôte co
docker run -v $PWD:/app/public -p 80:80 -p 443:443 -p 443:443/udp --tty my-php-app
```
> ![TIP]
> [!TIP]
>
> L'option --tty permet d'avoir des logs lisibles par un humain au lieu de logs JSON.
@@ -137,25 +149,51 @@ Voici un exemple de `Dockerfile` le permettant :
```dockerfile
FROM dunglas/frankenphp
ARG USER=www-data
ARG USER=appuser
RUN \
# Utilisez "adduser -D ${USER}" pour les distributions basées sur Alpine
useradd -D ${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;
# 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
- 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

View File

@@ -6,16 +6,18 @@ Grâce à cette fonctionnalité, les applications PHP peuvent être distribuées
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
- 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 :
@@ -29,7 +31,8 @@ cd $TMPDIR/my-prepared-app
echo APP_ENV=prod > .env.local
echo APP_DEBUG=0 >> .env.local
# Supprimer les tests
# 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
@@ -39,42 +42,47 @@ composer install --ignore-platform-reqs --no-dev -a
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
```dockerfile
FROM --platform=linux/amd64 dunglas/frankenphp:static-builder
# 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, be sure to select only the PHP extensions you want
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.
> [!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 .
```
```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
```
```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.
@@ -85,9 +93,7 @@ Si vous ne souhaitez pas utiliser Docker, ou souhaitez construire un binaire mac
```console
git clone https://github.com/dunglas/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
```
Le binaire obtenu est le fichier nommé `frankenphp-<os>-<arch>` dans le répertoire `dist/`.
@@ -120,6 +126,19 @@ Vous pouvez également exécuter les scripts CLI PHP incorporés dans votre bina
./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...).

View File

@@ -1,53 +1,21 @@
# Problèmes Connus
## Fibres
Appeller de fonctions et mots clefs PHP qui eux-mêmes appellent [cgo](https://go.dev/blog/cgo) dans des [Fibres](https://www.php.net/manual/fr/language.fibers.php) est connu pour provoquer des plantages.
Ce problème est [en cours de correction par le projet Go](https://github.com/golang/go/issues/62130).
En attendant, une solution consiste à ne pas utiliser de mots clefs (comme `echo`) et de fonctions (comme `header()`) qui délèguent à Go depuis l'intérieur de fibres.
Ce code risque de planter car il utilise `echo` dans une fibre :
```php
$fiber = new Fiber(function() {
echo 'Dans la fibre'.PHP_EOL;
echo 'Toujours dedans'.PHP_EOL;
});
$fiber->start();
```
A la place, retournez la valeur de la Fibre et utilisez-la à l'extérieur :
```php
$fiber = new Fiber(function() {
Fiber::suspend('Dans la fibre'.PHP_EOL));
Fiber::suspend('Toujours dedans'.PHP_EOL));
});
echo $fiber->start();
echo $fiber->resume();
$fiber->resume();
```
## Extensions PHP non prises en charge
Les extensions suivantes sont connues pour ne pas être compatibles avec FrankenPHP :
| Nom | Problème |
|---------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [XDebug](https://xdebug.org) | XDebug peut planter, ou se bloquer quand utilisé avec PHP 8.3. Une solution consiste à dwongrader vers PHP 8.2. Ce problème est [suivi par XDebug](https://github.com/dunglas/frankenphp/issues/563#issuecomment-1952226212). |
| [Tideways](https://tideways.com/) | En mode worker, l'extension Tideways [empêche les scripts workers de se terminer correctement ou consomme 100% du CPU](https://github.com/dunglas/frankenphp/issues/578#issuecomment-1966620351). Ce problème a été signalé à Tideways. |
| [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). |
| 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
[XDebug](https://xdebug.org/) | XDebug peut planter, ou se bloquer quand utilisé avec PHP 8.3. Une solution consiste à dwongrader vers PHP 8.2. Ce problème est [suivi par XDebug](https://github.com/dunglas/frankenphp/issues/563#issuecomment-1952226212).
[Tideways](https://tideways.com/) | En mode worker, l'extension Tideways [empêche les scripts workers de se terminer correctement ou consomme 100% du CPU](https://github.com/dunglas/frankenphp/issues/578#issuecomment-1966620351). Ce problème a été signalé à Tideways.
[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).
| 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
@@ -60,9 +28,9 @@ Le binaire autonome et les images docker basées sur Alpine (`dunglas/frankenphp
## 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 est la plus simple et est recommandée pour le développement local.
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 configure FrankenPHP pour générer un certificat pour cela en définissant le nom du serveur à `127.0.0.1`.
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`.
@@ -79,7 +47,7 @@ docker run \
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 de 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`.
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` :
@@ -91,7 +59,7 @@ docker run \
dunglas/frankenphp
```
> ![CAUTION]
> [!CAUTION]
>
> Assurez-vous de remplacer `172.17.0.3` par l'IP qui sera attribuée à votre conteneur.
@@ -101,9 +69,75 @@ Si ce n'est pas le cas, lancez FrankenPHP en mode debug pour essayer de comprend
```console
docker run \
-e CADDY_GLOBAL_OPTIONS="debug"
-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/dunglas/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 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
```

View File

@@ -2,8 +2,7 @@
## 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.
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 :
@@ -17,27 +16,28 @@ Et profitez !
Vous pouvez également exécuter vos projets Laravel avec FrankenPHP depuis votre machine locale :
1. [Téléchargez le binaire correspondant à votre système](https://github.com/dunglas/frankenphp/releases)
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
order php_server before file_server
}
```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
}
```
# 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`
3. Démarrez FrankenPHP depuis le répertoire racine de votre projet Laravel : `frankenphp run`
## Laravel Octane
@@ -53,24 +53,132 @@ Après avoir installé Octane, vous pouvez exécuter la commande Artisan `octane
php artisan octane:install --server=frankenphp
```
Le serveur Octane peut être démarré via la commande Artisan `octane:start`.
Le serveur Octane peut être démarré via la commande Artisan `octane:frankenphp`.
```console
php artisan octane:start
php artisan octane:frankenphp
```
La commande `octane:start` peut prendre les options suivantes :
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
- `--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
# 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

View File

@@ -5,7 +5,7 @@ Mercure permet de pousser des événements en temps réel vers tous les appareil
Aucune bibliothèque JS ou SDK requis !
![Mercure](https://mercure.rocks/static/main.png)
![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).

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).

View File

@@ -4,20 +4,49 @@ Au lieu d'utiliser une installation locale de la bibliothèque PHP, il est possi
Avec cette méthode, un binaire portable unique contiendra l'interpréteur PHP, le serveur web Caddy et FrankenPHP !
Les exécutables natifs entièrement statiques ne nécessitent aucune dépendance et peuvent même être exécutés sur une [image Docker `scratch`](https://docs.docker.com/build/building/base-images/#create-a-minimal-base-image-using-scratch).
Cependant, ils ne peuvent pas charger les extensions dynamiques de PHP (comme Xdebug) et ont quelques limitations parce qu'ils utilisent la librairie musl.
La plupart des binaires statiques ne nécessitent que la `glibc` et peuvent charger des extensions dynamiques.
Lorsque c'est possible, nous recommandons d'utiliser des binaires statiques basés sur la glibc.
FrankenPHP permet également [d'embarquer l'application PHP dans le binaire statique](embed.md).
## Linux
Nous fournissons une image Docker pour créer un binaire statique pour Linux :
Nous fournissons des images Docker pour créer des binaires statiques pour Linux :
### Build entièrement statique, basé sur musl
Pour un binaire entièrement statique qui fonctionne sur n'importe quelle distribution Linux sans dépendances,
mais qui ne prend pas en charge le chargement dynamique des extensions :
```console
docker buildx bake --load static-builder
docker cp $(docker create --name static-builder dunglas/frankenphp:static-builder):/go/src/app/dist/frankenphp-linux-$(uname -m) frankenphp ; docker rm static-builder
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
```
Le binaire statique résultant est nommé `frankenphp`, et il est disponible dans le répertoire courant.
Pour améliorer les performances dans les scénarios fortement concurrents, envisagez d'utiliser l'allocateur [mimalloc](https://github.com/microsoft/mimalloc).
Si vous souhaitez construire le binaire statique sans Docker, regardez les instructions pour macOS, qui fonctionnent également pour Linux.
```console
docker buildx bake --load --set static-builder-musl.args.MIMALLOC=1 static-builder-musl
```
### Construction principalement statique (avec prise en charge des extensions dynamiques), basé sur la glibc
Pour un binaire qui supporte le chargement dynamique des extensions PHP tout en ayant les extensions sélectionnées compilées statiquement :
```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
```
Ce binaire supporte toutes les versions 2.17 et supérieures de la glibc mais ne fonctionne pas sur les systèmes basés sur musl (comme Alpine Linux).
Le binaire résultant, principalement statique (à l'exception de `glibc`), est nommé `frankenphp` et est disponible dans le répertoire courant.
Si vous voulez construire le binaire statique sans Docker, jetez un coup d'œil aux instructions pour macOS, qui fonctionnent aussi pour Linux.
### Extensions personnalisées
@@ -28,7 +57,7 @@ Pour réduire la taille du binaire et diminuer la surface d'attaque, vous pouvez
Par exemple, exécutez la commande suivante pour ne construire que l'extension `opcache` :
```console
docker buildx bake --load --set static-builder.args.PHP_EXTENSIONS=opcache,pdo_sqlite static-builder
docker buildx bake --load --set static-builder-musl.args.PHP_EXTENSIONS=opcache,pdo_sqlite static-builder-musl
# ...
```
@@ -37,11 +66,29 @@ Pour ajouter des bibliothèques permettant des fonctionnalités supplémentaires
```console
docker buildx bake \
--load \
--set static-builder.args.PHP_EXTENSIONS=gd \
--set static-builder.args.PHP_EXTENSION_LIBS=libjpeg,libwebp \
static-builder
--set static-builder-musl.args.PHP_EXTENSIONS=gd \
--set static-builder-musl.args.PHP_EXTENSION_LIBS=libjpeg,libwebp \
static-builder-musl
```
### Modules supplémentaires de Caddy
Pour ajouter des modules Caddy supplémentaires ou passer d'autres arguments à [xcaddy](https://github.com/caddyserver/xcaddy), utilisez l'argument Docker `XCADDY_ARGS` :
```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
```
Dans cet exemple, nous ajoutons le module de cache HTTP [Souin](https://souin.io) pour Caddy ainsi que les modules [cbrotli](https://github.com/dunglas/caddy-cbrotli), [Mercure](https://mercure.rocks) et [Vulcain](https://vulcain.rocks).
> [!TIP]
>
> Les modules cbrotli, Mercure et Vulcain sont inclus par défaut si `XCADDY_ARGS` est vide ou n'est pas défini.
> Si vous personnalisez la valeur de `XCADDY_ARGS`, vous devez les inclure explicitement si vous voulez qu'ils soient inclus.
Voir aussi comment [personnaliser la construction](#personnalisation-de-la-construction)
### Jeton GitHub
@@ -49,7 +96,7 @@ Voir aussi comment [personnaliser la construction](#personnalisation-de-la-const
Si vous atteignez la limite de taux d'appels de l'API GitHub, définissez un jeton d'accès personnel GitHub dans une variable d'environnement nommée `GITHUB_TOKEN` :
```console
GITHUB_TOKEN="xxx" docker --load buildx bake static-builder
GITHUB_TOKEN="xxx" docker --load buildx bake static-builder-musl
# ...
```
@@ -69,11 +116,46 @@ Note : ce script fonctionne également sur Linux (et probablement sur d'autres U
Les variables d'environnement suivantes peuvent être transmises à `docker build` et au script `build-static.sh` pour personnaliser la construction statique :
* `FRANKENPHP_VERSION` : la version de FrankenPHP à utiliser
* `PHP_VERSION` : la version de PHP à utiliser
* `PHP_EXTENSIONS` : les extensions PHP à construire ([liste des extensions prises en charge](https://static-php.dev/en/guide/extensions.html))
* `PHP_EXTENSION_LIBS` : bibliothèques supplémentaires à construire qui ajoutent des fonctionnalités aux extensions
* `EMBED` : chemin de l'application PHP à intégrer dans le binaire
* `CLEAN` : lorsque défini, `libphp` et toutes ses dépendances sont construites à partir de zéro (pas de cache)
* `DEBUG_SYMBOLS` : lorsque défini, les symboles de débogage ne seront pas supprimés et seront ajoutés dans le binaire
* `RELEASE` : (uniquement pour les mainteneurs) lorsque défini, le binaire résultant sera uploadé sur GitHub
- `FRANKENPHP_VERSION` : la version de FrankenPHP à utiliser
- `PHP_VERSION` : la version de PHP à utiliser
- `PHP_EXTENSIONS` : les extensions PHP à construire ([liste des extensions prises en charge](https://static-php.dev/en/guide/extensions.html))
- `PHP_EXTENSION_LIBS` : bibliothèques supplémentaires à construire qui ajoutent des fonctionnalités aux extensions
- `XCADDY_ARGS` : arguments à passer à [xcaddy](https://github.com/caddyserver/xcaddy), par exemple pour ajouter des modules Caddy supplémentaires
- `EMBED` : chemin de l'application PHP à intégrer dans le binaire
- `CLEAN` : lorsque défini, `libphp` et toutes ses dépendances sont construites à partir de zéro (pas de cache)
- `DEBUG_SYMBOLS` : lorsque défini, les symboles de débogage ne seront pas supprimés et seront ajoutés dans le binaire
- `NO_COMPRESS`: ne pas compresser le binaire avec UPX
- `MIMALLOC`: (expérimental, Linux seulement) remplace l'allocateur mallocng de musl par [mimalloc](https://github.com/microsoft/mimalloc) pour des performances améliorées
- `RELEASE` : (uniquement pour les mainteneurs) lorsque défini, le binaire résultant sera uploadé sur GitHub
## Extensions
Avec la glibc ou les binaires basés sur macOS, vous pouvez charger des extensions PHP dynamiquement. Cependant, ces extensions devront être compilées avec le support ZTS.
Comme la plupart des gestionnaires de paquets ne proposent pas de versions ZTS de leurs extensions, vous devrez les compiler vous-même.
Pour cela, vous pouvez construire et exécuter le conteneur Docker `static-builder-gnu`, vous y connecter à distance et compiler les extensions avec `./configure --with-php-config=/go/src/app/dist/static-php-cli/buildroot/bin/php-config`.
Exemple d'étapes pour [l'extension 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
```
Cela aura créé `frankenphp` et `xdebug-zts.so` dans le répertoire courant.
Si vous déplacez `xdebug-zts.so` dans votre répertoire d'extension, ajoutez `zend_extension=xdebug-zts.so` à votre php.ini
et lancez FrankenPHP, il chargera Xdebug.

View File

@@ -22,12 +22,19 @@ docker run \
Utilisez l'option --worker de la commande php-server pour servir le contenu du répertoire courant en utilisant un worker :
```console
./frankenphp php-server --worker /path/to/your/worker/script.php
frankenphp php-server --worker /path/to/your/worker/script.php
```
Si votre application PHP est [intégrée dans le binaire](embed.md), vous pouvez également ajouter un `Caddyfile` personnalisé dans le répertoire racine de l'application.
Il sera utilisé automatiquement.
Il est également possible de [redémarrer le worker en cas de changement de fichier](config.md#surveillance-des-modifications-de-fichier) avec l'option `--watch`.
La commande suivante déclenchera un redémarrage si un fichier se terminant par `.php` dans le répertoire `/path/to/your/app/` ou ses sous-répertoires est modifié :
```console
frankenphp php-server --worker /path/to/your/worker/script.php --watch="/path/to/your/app/**/*.php"
```
## Runtime Symfony
Le mode worker de FrankenPHP est pris en charge par le [Composant Runtime de Symfony](https://symfony.com/doc/current/components/runtime.html).
@@ -71,19 +78,22 @@ $myApp->boot();
// En dehors de la boucle pour de meilleures performances (moins de travail effectué)
$handler = static function () use ($myApp) {
// Appelé lorsqu'une requête est reçue,
// les superglobales, php://input, etc., sont réinitialisés
echo $myApp->handle($_GET, $_POST, $_COOKIE, $_FILES, $_SERVER);
// Appelé lorsqu'une requête est reçue,
// les superglobales, php://input, etc., sont réinitialisés
echo $myApp->handle($_GET, $_POST, $_COOKIE, $_FILES, $_SERVER);
};
for($nbRequests = 0, $running = true; isset($_SERVER['MAX_REQUESTS']) && ($nbRequests < ((int)$_SERVER['MAX_REQUESTS'])) && $running; ++$nbRequests) {
$running = \frankenphp_handle_request($handler);
$maxRequests = (int)($_SERVER['MAX_REQUESTS'] ?? 0);
for ($nbRequests = 0; !$maxRequests || $nbRequests < $maxRequests; ++$nbRequests) {
$keepRunning = \frankenphp_handle_request($handler);
// Faire quelque chose après l'envoi de la réponse HTTP
$myApp->terminate();
// Exécuter le ramasse-miettes pour réduire les chances qu'il soit déclenché au milieu de la génération d'une page
gc_collect_cycles();
if (!$keepRunning) break;
}
// Nettoyage
@@ -117,3 +127,48 @@ Comme PHP n'a pas été initialement conçu pour des processus de longue durée,
Une solution pour utiliser ce type de code en mode worker est de redémarrer le script worker après avoir traité un certain nombre de requêtes :
Le code du worker précédent permet de configurer un nombre maximal de requêtes à traiter en définissant une variable d'environnement nommée `MAX_REQUESTS`.
### Redémarrer les workers manuellement
Bien qu'il soit possible de redémarrer les workers [en cas de changement de fichier](config.md#surveillance-des-modifications-de-fichier),
il est également possible de redémarrer tous les workers de manière élégante via l'[API Admin de Caddy](https://caddyserver.com/docs/api).
Si l'administration est activée dans votre [Caddyfile](config.md#configuration-du-caddyfile), vous pouvez envoyer un ping
à l'endpoint de redémarrage avec une simple requête POST comme celle-ci :
```console
curl -X POST http://localhost:2019/frankenphp/workers/restart
```
> [!NOTE]
>
> C'est une fonctionnalité expérimentale et peut être modifiée ou supprimée dans le futur.
### Worker Failures
Si un script de worker se plante avec un code de sortie non nul, FrankenPHP le redémarre avec une stratégie de backoff exponentielle.
Si le script worker reste en place plus longtemps que le dernier backoff \* 2, FrankenPHP ne pénalisera pas le script et le redémarrera à nouveau.
Toutefois, si le script de worker continue d'échouer avec un code de sortie non nul dans un court laps de temps
(par exemple, une faute de frappe dans un script), FrankenPHP plantera avec l'erreur : `too many consecutive failures` (trop d'échecs consécutifs).
## Comportement des superglobales
[Les superglobales PHP](https://www.php.net/manual/fr/language.variables.superglobals.php) (`$_SERVER`, `$_ENV`, `$_GET`...)
se comportent comme suit :
- avant le premier appel à `frankenphp_handle_request()`, les superglobales contiennent des valeurs liées au script worker lui-même
- pendant et après l'appel à `frankenphp_handle_request()`, les superglobales contiennent des valeurs générées à partir de la requête HTTP traitée, chaque appel à `frankenphp_handle_request()` change les valeurs des superglobales
Pour accéder aux superglobales du script worker à l'intérieur de la fonction de rappel, vous devez les copier et importer la copie dans le scope de la fonction :
```php
<?php
// Copier la superglobale $_SERVER du worker avant le premier appel à frankenphp_handle_request()
$workerServer = $_SERVER;
$handler = static function () use ($workerServer) {
var_dump($_SERVER); // $_SERVER lié à la requête
var_dump($workerServer); // $_SERVER du script worker
};
// ...
```

71
docs/fr/x-sendfile.md Normal file
View File

@@ -0,0 +1,71 @@
# Servir efficacement les gros fichiers statiques (`X-Sendfile`/`X-Accel-Redirect`)
Habituellement, les fichiers statiques peuvent être servis directement par le serveur web,
mais parfois, il est nécessaire d'exécuter du code PHP avant de les envoyer :
contrôle d'accès, statistiques, en-têtes HTTP personnalisés...
Malheureusement, utiliser PHP pour servir de gros fichiers statiques est inefficace comparé à
à l'utilisation directe du serveur web (surcharge mémoire, diminution des performances...).
FrankenPHP permet de déléguer l'envoi des fichiers statiques au serveur web
**après** avoir exécuté du code PHP personnalisé.
Pour ce faire, votre application PHP n'a qu'à définir un en-tête HTTP personnalisé
contenant le chemin du fichier à servir. FrankenPHP se chargera du reste.
Cette fonctionnalité est connue sous le nom de **`X-Sendfile`** pour Apache, et **`X-Accel-Redirect`** pour NGINX.
Dans les exemples suivants, nous supposons que le "document root" du projet est le répertoire `public/`
et que nous voulons utiliser PHP pour servir des fichiers stockés en dehors du dossier `public/`,
depuis un répertoire nommé `private-files/`.
## Configuration
Tout d'abord, ajoutez la configuration suivante à votre `Caddyfile` pour activer cette fonctionnalité :
```patch
root public/
# ...
+ # Needed for Symfony, Laravel and other projects using the Symfony HttpFoundation component
+ 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
+
+ # Remove the X-Accel-Redirect header set by PHP for increased security
+ header -X-Accel-Redirect
+
+ file_server
+ }
+ }
php_server
```
## PHP simple
Définissez le chemin relatif du fichier (à partir de `private-files/`) comme valeur de l'en-tête `X-Accel-Redirect` :
```php
header('X-Accel-Redirect: file.txt') ;
```
## Projets utilisant le composant Symfony HttpFoundation (Symfony, Laravel, Drupal...)
Symfony HttpFoundation [supporte nativement cette fonctionnalité](https://symfony.com/doc/current/components/http_foundation.html#serving-files).
Il va automatiquement déterminer la bonne valeur pour l'en-tête `X-Accel-Redirect` et l'ajoutera à la réponse.
```php
use Symfony\Component\HttpFoundation\BinaryFileResponse;
BinaryFileResponse::trustXSendfileTypeHeader();
$response = new BinaryFileResponse(__DIR__.'/../private-files/file.txt');
// ...
```

View File

@@ -8,8 +8,8 @@ every approved pull request or on your own fork once setup.
In the repository settings, under secrets, add the following secrets:
- `REGISTRY_LOGIN_SERVER`: The docker registry to use (e.g. `docker.io`).
- `REGISTRY_USERNAME`: The username to use to login to the registry (e.g. `dunglas`).
- `REGISTRY_PASSWORD`: The password to use to login to the registry (e.g. an access key).
- `REGISTRY_USERNAME`: The username to use to log in to the registry (e.g. `dunglas`).
- `REGISTRY_PASSWORD`: The password to use to log in to the registry (e.g. an access key).
- `IMAGE_NAME`: The name of the image (e.g. `dunglas/frankenphp`).
## Building and Pushing the Image

View File

@@ -1,51 +1,20 @@
# Known Issues
## Fibers
Calling PHP functions and language constructs that themselves call [cgo](https://go.dev/blog/cgo) in [Fibers](https://www.php.net/manual/en/language.fibers.php) is known to cause crashes.
This issue [is being worked on by the Go project](https://github.com/golang/go/issues/62130).
In the meantime, one solution is not to use constructs (like `echo`) and functions (like `header()`) that delegate to Go from inside Fibers.
This code will likely crash because it uses `echo` in the Fiber:
```php
$fiber = new Fiber(function() {
echo 'In the Fiber'.PHP_EOL;
echo 'Still inside'.PHP_EOL;
});
$fiber->start();
```
Instead, return the value from the Fiber and use it outside:
```php
$fiber = new Fiber(function() {
Fiber::suspend('In the Fiber'.PHP_EOL));
Fiber::suspend('Still inside'.PHP_EOL));
});
echo $fiber->start();
echo $fiber->resume();
$fiber->resume();
```
## Unsupported PHP Extensions
The following extensions are known not to be compatible with FrankenPHP:
| Name | Reason | Alternatives |
| ----------------------------------------------------------- | --------------- | -------------------------------------------------------------------------------------------------------------------- |
| [imap](https://www.php.net/manual/en/imap.installation.php) | Not thread-safe | [javanile/php-imap2](https://github.com/javanile/php-imap2), [webklex/php-imap](https://github.com/Webklex/php-imap) |
| Name | Reason | Alternatives |
| ----------------------------------------------------------------------------------------------------------- | --------------- | -------------------------------------------------------------------------------------------------------------------- |
| [imap](https://www.php.net/manual/en/imap.installation.php) | Not thread-safe | [javanile/php-imap2](https://github.com/javanile/php-imap2), [webklex/php-imap](https://github.com/Webklex/php-imap) |
| [newrelic](https://docs.newrelic.com/docs/apm/agents/php-agent/getting-started/introduction-new-relic-php/) | Not thread-safe | - |
## Buggy PHP Extensions
The following extensions have known bugs and unexpected behaviors when used with FrankenPHP:
| Name | Problem |
|---------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [XDebug](https://xdebug.org/) | XDebug may crash, or hang on PHP 8.3. A workaround is to downgrade to PHP 8.2. This problem is [being tracked by XDebug](https://github.com/dunglas/frankenphp/issues/563#issuecomment-1952226212). |
| [Tideways](https://tideways.com/) | In worker mode, the Tideways extension [prevents worker scripts to finish properly](https://github.com/dunglas/frankenphp/issues/578#issuecomment-1966620351) or consumes 100% of the CPU. This has been reported to Tideways. |
| ------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [ext-openssl](https://www.php.net/manual/en/book.openssl.php) | When using a static build of FrankenPHP (built with the musl libc), the OpenSSL extension may crash under heavy loads. A workaround is to use a dynamically linked build (like the one used in Docker images). This bug is [being tracked by PHP](https://github.com/php/php-src/issues/13648). |
## get_browser
@@ -100,7 +69,7 @@ If that's not the case, start FrankenPHP in debug mode to try to figure out the
```console
docker run \
-e CADDY_GLOBAL_OPTIONS="debug"
-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 \
@@ -111,13 +80,13 @@ docker run \
[Composer scripts](https://getcomposer.org/doc/articles/scripts.md) may want to execute a PHP binary for some tasks, e.g. in [a Laravel project](laravel.md) to run `@php artisan package:discover --ansi`. This [currently fails](https://github.com/dunglas/frankenphp/issues/483#issuecomment-1899890915) for two reasons:
* Composer does not know how to call the FrankenPHP binary;
* Composer may add PHP settings using the `-d` flag in the command, which FrankenPHP does not yet support.
- Composer does not know how to call the FrankenPHP binary;
- Composer may add PHP settings using the `-d` flag in the command, which FrankenPHP does not yet support.
As a workaround, we can create a shell script in `/usr/local/bin/php` which strips the unsupported parameters and then calls FrankenPHP:
```bash
#!/bin/bash
#!/usr/bin/env bash
args=("$@")
index=0
for i in "$@"
@@ -132,9 +101,43 @@ done
/usr/local/bin/frankenphp php-cli ${args[@]}
```
Then set the environment variable `PHP_BINARY` to the path of our php script and composer should pass:
Then set the environment variable `PHP_BINARY` to the path of our `php` script and run Composer:
```bash
```console
export PHP_BINARY=/usr/local/bin/php
composer install
```
## Troubleshooting TLS/SSL Issues with Static Binaries
When using the static binaries, you may encounter the following TLS-related errors, for instance when sending emails using 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
```
As the static binary doesn't bundle TLS certificates, you need to point OpenSSL to your local CA certificates installation.
Inspect the output of [`openssl_get_cert_locations()`](https://www.php.net/manual/en/function.openssl-get-cert-locations.php),
to find where CA certificates must be installed and store them at this location.
> [!WARNING]
>
> Web and CLI contexts may have different settings.
> Be sure to run `openssl_get_cert_locations()` in the proper context.
[CA certificates extracted from Mozilla can be downloaded on the curl site](https://curl.se/docs/caextract.html).
Alternatively, many distributions, including Debian, Ubuntu, and Alpine provide packages named `ca-certificates` that contain these certificates.
It's also possible to use the `SSL_CERT_FILE` and `SSL_CERT_DIR` to hint OpenSSL where to look for CA certificates:
```console
# Set TLS certificates environment variables
export SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt
export SSL_CERT_DIR=/etc/ssl/certs
```

View File

@@ -16,27 +16,28 @@ And enjoy!
Alternatively, you can run your Laravel projects with FrankenPHP from your local machine:
1. [Download the binary corresponding to your system](https://github.com/dunglas/frankenphp/releases)
1. [Download the binary corresponding to your system](../#standalone-binary)
2. Add the following configuration to a file named `Caddyfile` in the root directory of your Laravel project:
```caddyfile
{
frankenphp
order php_server before file_server
}
```caddyfile
{
frankenphp
}
# The domain name of your server
localhost {
# Set the webroot to the public/ directory
root * public/
# Enable compression (optional)
encode zstd br gzip
# Execute PHP files from the public/ directory and serve assets
php_server
}
```
# The domain name of your server
localhost {
# Set the webroot to the public/ directory
root public/
# Enable compression (optional)
encode zstd br gzip
# Execute PHP files from the public/ directory and serve assets
php_server {
try_files {path} index.php
}
}
```
3. Start FrankenPHP from the root directory of your Laravel project: `./frankenphp run`
3. Start FrankenPHP from the root directory of your Laravel project: `frankenphp run`
## Laravel Octane
@@ -52,24 +53,132 @@ After installing Octane, you may execute the `octane:install` Artisan command, w
php artisan octane:install --server=frankenphp
```
The Octane server can be started via the `octane:start` Artisan command.
The Octane server can be started via the `octane:frankenphp` Artisan command.
```console
php artisan octane:start
php artisan octane:frankenphp
```
The `octane:start` command can take the following options:
The `octane:frankenphp` command can take the following options:
* `--host`: The IP address the server should bind to (default: `127.0.0.1`)
* `--port`: The port the server should be available on (default: `8000`)
* `--admin-port`: The port the admin server should be available on (default: `2019`)
* `--workers`: The number of workers that should be available to handle requests (default: `auto`)
* `--max-requests`: The number of requests to process before reloading the server (default: `500`)
* `--caddyfile`: The path to the FrankenPHP `Caddyfile` file
* `--https`: Enable HTTPS, HTTP/2, and HTTP/3, and automatically generate and renew certificates
* `--http-redirect`: Enable HTTP to HTTPS redirection (only enabled if --https is passed)
* `--watch`: Automatically reload the server when the application is modified
* `--poll`: Use file system polling while watching in order to watch files over a network
* `--log-level`: Log messages at or above the specified log level
- `--host`: The IP address the server should bind to (default: `127.0.0.1`)
- `--port`: The port the server should be available on (default: `8000`)
- `--admin-port`: The port the admin server should be available on (default: `2019`)
- `--workers`: The number of workers that should be available to handle requests (default: `auto`)
- `--max-requests`: The number of requests to process before reloading the server (default: `500`)
- `--caddyfile`: The path to the FrankenPHP `Caddyfile` file (default: [stubbed `Caddyfile` in Laravel Octane](https://github.com/laravel/octane/blob/2.x/src/Commands/stubs/Caddyfile))
- `--https`: Enable HTTPS, HTTP/2, and HTTP/3, and automatically generate and renew certificates
- `--http-redirect`: Enable HTTP to HTTPS redirection (only enabled if --https is passed)
- `--watch`: Automatically reload the server when the application is modified
- `--poll`: Use file system polling while watching in order to watch files over a network
- `--log-level`: Log messages at or above the specified log level, using the native Caddy logger
> [!TIP]
> To get structured JSON logs (useful when using log analytics solutions), explicitly the pass `--log-level` option.
Learn more about [Laravel Octane in its official documentation](https://laravel.com/docs/octane).
## Laravel Apps As Standalone Binaries
Using [FrankenPHP's application embedding feature](embed.md), it's possible to distribute Laravel
apps as standalone binaries.
Follow these steps to package your Laravel app as a standalone binary for Linux:
1. Create a file named `static-build.Dockerfile` in the repository of your app:
```dockerfile
FROM --platform=linux/amd64 dunglas/frankenphp:static-builder
# Copy your app
WORKDIR /go/src/app/dist/app
COPY . .
# Remove the tests and other unneeded files to save space
# Alternatively, add these files to a .dockerignore file
RUN rm -Rf tests/
# Copy .env file
RUN cp .env.example .env
# Change APP_ENV and APP_DEBUG to be production ready
RUN sed -i'' -e 's/^APP_ENV=.*/APP_ENV=production/' -e 's/^APP_DEBUG=.*/APP_DEBUG=false/' .env
# Make other changes to your .env file if needed
# Install the dependencies
RUN composer install --ignore-platform-reqs --no-dev -a
# Build the static binary
WORKDIR /go/src/app/
RUN EMBED=dist/app/ ./build-static.sh
```
> [!CAUTION]
>
> Some `.dockerignore` files
> 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-laravel-app -f static-build.Dockerfile .
```
3. Extract the binary:
```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. Populate caches:
```console
frankenphp php-cli artisan optimize
```
5. Run database migrations (if any):
```console
frankenphp php-cli artisan migrate
```
6. Generate app's secret key:
```console
frankenphp php-cli artisan key:generate
```
7. Start the server:
```console
frankenphp php-server
```
Your app is now ready!
Learn more about the options available and how to build binaries for other OSes in the [applications embedding](embed.md)
documentation.
### Changing The Storage Path
By default, Laravel stores uploaded files, caches, logs, etc. in the application's `storage/` directory.
This is not suitable for embedded applications, as each new version will be extracted into a different temporary directory.
Set the `LARAVEL_STORAGE_PATH` environment variable (for example, in your `.env` file) or call the `Illuminate\Foundation\Application::useStoragePath()` method to use a directory outside the temporary directory.
### Running Octane With Standalone Binaries
It's even possible to package Laravel Octane apps as standalone binaries!
To do so, [install Octane properly](#laravel-octane) and follow the steps described in [the previous section](#laravel-apps-as-standalone-binaries).
Then, to start FrankenPHP in worker mode through Octane, run:
```console
PATH="$PWD:$PATH" frankenphp php-cli artisan octane:frankenphp
```
> [!CAUTION]
>
> For the command to work, the standalone binary **must** be named `frankenphp`
> because Octane needs a program named `frankenphp` available in the path.

BIN
docs/mercure-hub.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1009 KiB

View File

@@ -1,12 +1,15 @@
# Real-time
FrankenPHP comes with a built-in [Mercure](https://mercure.rocks) hub!
Mercure allows to push events in real-time to all the connected devices: they will receive a JavaScript event instantly.
Mercure allows you to push real-time events to all the connected devices: they will receive a JavaScript event instantly.
No JS library or SDK required!
No JS library or SDK is required!
![Mercure](https://mercure.rocks/static/main.png)
![Mercure](mercure-hub.png)
To enable the Mercure hub, update the `Caddyfile` as described [on Mercure's site](https://mercure.rocks/docs/hub/config).
To push Mercure updates from your code, we recommend the [Symfony Mercure Component](https://symfony.com/components/Mercure) (you don't need the Symfony full stack framework to use it).
The path of the Mercure hub is `/.well-known/mercure`.
When running FrankenPHP inside Docker, the full send URL would look like `http://php/.well-known/mercure` (with `php` being the container's name running FrankenPHP).
To push Mercure updates from your code, we recommend the [Symfony Mercure Component](https://symfony.com/components/Mercure) (you don't need the Symfony full-stack framework to use it).

17
docs/metrics.md Normal file
View File

@@ -0,0 +1,17 @@
# Metrics
When [Caddy metrics](https://caddyserver.com/docs/metrics) are enabled, FrankenPHP exposes the following metrics:
- `frankenphp_total_threads`: The total number of PHP threads.
- `frankenphp_busy_threads`: The number of PHP threads currently processing a request (running workers always consume a thread).
- `frankenphp_queue_depth`: The number of regular queued requests
- `frankenphp_total_workers{worker="[worker_name]"}`: The total number of workers.
- `frankenphp_busy_workers{worker="[worker_name]"}`: The number of workers currently processing a request.
- `frankenphp_worker_request_time{worker="[worker_name]"}`: The time spent processing requests by all workers.
- `frankenphp_worker_request_count{worker="[worker_name]"}`: The number of requests processed by all workers.
- `frankenphp_ready_workers{worker="[worker_name]"}`: The number of workers that have called `frankenphp_handle_request` at least once.
- `frankenphp_worker_crashes{worker="[worker_name]"}`: The number of times a worker has unexpectedly terminated.
- `frankenphp_worker_restarts{worker="[worker_name]"}`: The number of times a worker has been deliberately restarted.
- `frankenphp_worker_queue_depth{worker="[worker_name]"}`: The number of queued requests.
For worker metrics, the `[worker_name]` placeholder is replaced by the worker name in the Caddyfile, otherwise absolute path of worker file will be used.

157
docs/performance.md Normal file
View File

@@ -0,0 +1,157 @@
# Performance
By default, FrankenPHP tries to offer a good compromise between performance and ease of use.
However, it is possible to substantially improve performance using an appropriate configuration.
## Number of Threads and Workers
By default, FrankenPHP starts 2 times more threads and workers (in worker mode) than the available numbers of CPU.
The appropriate values depend heavily on how your application is written, what it does and your hardware.
We strongly recommend changing these values. For best system stability, it is recommended to have `num_threads` x `memory_limit` < `available_memory`.
To find the right values, it's best to run load tests simulating real traffic.
[k6](https://k6.io) and [Gatling](https://gatling.io) are good tools for this.
To configure the number of threads, use the `num_threads` option of the `php_server` and `php` directives.
To change the number of workers, use the `num` option of the `worker` section of the `frankenphp` directive.
### `max_threads`
While it's always better to know exactly what your traffic will look like, real-life applications tend to be more
unpredictable. The `max_threads` [configuration](config.md#caddyfile-config) allows FrankenPHP to automatically spawn additional threads at runtime up to the specified limit.
`max_threads` can help you figure out how many threads you need to handle your traffic and can make the server more resilient to latency spikes.
If set to `auto`, the limit will be estimated based on the `memory_limit` in your `php.ini`. If not able to do so,
`auto` will instead default to 2x `num_threads`. Keep in mind that `auto` might strongly underestimate the number of threads needed.
`max_threads` is similar to PHP FPM's [pm.max_children](https://www.php.net/manual/en/install.fpm.configuration.php#pm.max-children). The main difference is that FrankenPHP uses threads instead of
processes and automatically delegates them across different worker scripts and 'classic mode' as needed.
## Worker Mode
Enabling [the worker mode](worker.md) dramatically improves performance,
but your app must be adapted to be compatible with this mode:
you need to create a worker script and to be sure that the app is not leaking memory.
## Don't Use musl
The Alpine Linux variant of the official Docker images and the default binaries we provide are using [the musl libc](https://musl.libc.org).
PHP is known to be [slower](https://gitlab.alpinelinux.org/alpine/aports/-/issues/14381) when using this alternative C library instead of the traditional GNU library,
especially when compiled in ZTS mode (thread-safe), which is required for FrankenPHP. The difference can be significant in a heavily threaded environment.
Also, [some bugs only happen when using musl](https://github.com/php/php-src/issues?q=sort%3Aupdated-desc+is%3Aissue+is%3Aopen+label%3ABug+musl).
In production environments, we recommend using FrankenPHP linked against glibc.
This can be achieved by using the Debian Docker images (the default), downloading the -gnu suffix binary from our [Releases](https://github.com/dunglas/frankenphp/releases), or by [compiling FrankenPHP from sources](compile.md).
Alternatively, we provide static musl binaries compiled with [the mimalloc allocator](https://github.com/microsoft/mimalloc), which alleviates the problems in threaded scenarios.
## Go Runtime Configuration
FrankenPHP is written in Go.
In general, the Go runtime doesn't require any special configuration, but in certain circumstances,
specific configuration improves performance.
You likely want to set the `GODEBUG` environment variable to `cgocheck=0` (the default in the FrankenPHP Docker images).
If you run FrankenPHP in containers (Docker, Kubernetes, LXC...) and limit the memory available for the containers,
set the `GOMEMLIMIT` environment variable to the available amount of memory.
For more details, [the Go documentation page dedicated to this subject](https://pkg.go.dev/runtime#hdr-Environment_Variables) is a must-read to get the most out of the runtime.
## `file_server`
By default, the `php_server` directive automatically sets up a file server to
serve static files (assets) stored in the root directory.
This feature is convenient, but comes with a cost.
To disable it, use the following config:
```caddyfile
php_server {
file_server off
}
```
## `try_files`
Besides static files and PHP files, `php_server` will also try to serve your application's index
and directory index files (`/path/` -> `/path/index.php`). If you don't need directory indices,
you can disable them by explicitly defining `try_files` like this:
```caddyfile
php_server {
try_files {path} index.php
root /root/to/your/app # explicitly adding the root here allows for better caching
}
```
This can significantly reduce the number of unnecessary file operations.
An alternate approach with 0 unnecessary file system operations would be to instead use the `php` directive and split
files from PHP by path. This approach works well if your entire application is served by one entry file.
An example [configuration](config.md#caddyfile-config) that serves static files behind an `/assets` folder could look like this:
```caddyfile
route {
@assets {
path /assets/*
}
# everything behind /assets is handled by the file server
file_server @assets {
root /root/to/your/app
}
# everything that is not in /assets is handled by your index or worker PHP file
rewrite index.php
php {
root /root/to/your/app # explicitly adding the root here allows for better caching
}
}
```
## Placeholders
You can use [placeholders](https://caddyserver.com/docs/conventions#placeholders) in the `root` and `env` directives.
However, this prevents caching these values, and comes with a significant performance cost.
If possible, avoid placeholders in these directives.
## `resolve_root_symlink`
By default, if the document root is a symbolic link, it is automatically resolved by FrankenPHP (this is necessary for PHP to work properly).
If the document root is not a symlink, you can disable this feature.
```caddyfile
php_server {
resolve_root_symlink false
}
```
This will improve performance if the `root` directive contains [placeholders](https://caddyserver.com/docs/conventions#placeholders).
The gain will be negligible in other cases.
## Logs
Logging is obviously very useful, but, by definition,
it requires I/O operations and memory allocations, which considerably reduces performance.
Make sure you [set the logging level](https://caddyserver.com/docs/caddyfile/options#log) correctly,
and only log what's necessary.
## PHP Performance
FrankenPHP uses the official PHP interpreter.
All usual PHP-related performance optimizations apply with FrankenPHP.
In particular:
- check that [OPcache](https://www.php.net/manual/en/book.opcache.php) is installed, enabled and properly configured
- enable [Composer autoloader optimizations](https://getcomposer.org/doc/articles/autoloader-optimization.md)
- ensure that the `realpath` cache is big enough for the needs of your application
- use [preloading](https://www.php.net/manual/en/opcache.preloading.php)
For more details, read [the dedicated Symfony documentation entry](https://symfony.com/doc/current/performance.html)
(most tips are useful even if you don't use Symfony).

View File

@@ -31,7 +31,7 @@ Refer to "[Building Custom Docker Image](docker.md)" for more details and option
and to learn how to customize the configuration, install PHP extensions and Caddy modules.
If your project uses Composer,
be sure to include it in the Docker image and to install your depedencies.
be sure to include it in the Docker image and to install your dependencies.
Then, add a `compose.yaml` file:
@@ -124,10 +124,10 @@ git clone git@github.com:<username>/<project-name>.git
Go into the directory containing your project (`<project-name>`), and start the app in production mode:
```console
docker compose up -d --wait
docker compose up --wait
```
Your server is up and running, and a HTTPS certificate has been automatically generated for you.
Your server is up and running, and an HTTPS certificate has been automatically generated for you.
Go to `https://your-domain-name.example.com` and enjoy!
> [!CAUTION]

218
docs/ru/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](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
Соберите Caddy с модулем FrankenPHP:
```console
cd caddy/frankenphp/
go build -tags watcher,brotli,nobadger,nomysql,nopgx
cd ../../
```
Запустите Caddy с модулем FrankenPHP:
```console
cd testdata/
../caddy/frankenphp/frankenphp run
```
Сервер будет доступен по адресу `127.0.0.1:8080`:
```console
curl -vk https://localhost/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
```
Соберите образы FrankenPHP для amd64 локально:
```console
docker buildx bake -f docker-bake.hcl --pull --load --set "*.platform=linux/amd64"
```
Соберите образы FrankenPHP для arm64 локально:
```console
docker buildx bake -f docker-bake.hcl --pull --load --set "*.platform=linux/arm64"
```
Соберите образы FrankenPHP с нуля для arm64 и amd64 и отправьте их в Docker Hub:
```console
docker buildx bake -f docker-bake.hcl --pull --no-cache --push
```
## Отладка ошибок сегментации с использованием статических сборок
1. Скачайте отладочную версию бинарного файла FrankenPHP с GitHub или создайте собственную статическую сборку с включённым отладочным режимом:
```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` на бинарный файл с включенным отладочным режимом.
3. Запустите FrankenPHP как обычно (или сразу запустите FrankenPHP с GDB: `gdb --args frankenphp run`).
4. Подключитесь к процессу через GDB:
```console
gdb -p `pidof frankenphp`
```
5. При необходимости введите `continue` в консоли GDB.
6. Вызовите сбой FrankenPHP.
7. Введите `bt` в консоли GDB.
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.dev.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)
- [Книга "Extending and Embedding PHP" Сары Големан](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 bindings](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. Создайте новую директорию с 2-буквенным ISO-кодом языка в папке `docs/`.
2. Скопируйте все `.md` файлы из корня папки `docs/` в новую директорию (используйте английскую версию как основу для перевода).
3. Скопируйте файлы `README.md` и `CONTRIBUTING.md` из корневой директории в новую папку.
4. Переведите содержимое файлов, но не изменяйте имена файлов. Не переводите строки, начинающиеся с `> [!`, это специальная разметка GitHub.
5. Создайте Pull Request с переводом.
6. В [репозитории сайта](https://github.com/dunglas/frankenphp-website/tree/main) скопируйте и переведите файлы в папках `content/`, `data/` и `i18n/`.
7. Переведите значения в созданных YAML-файлах.
8. Откройте Pull Request в репозитории сайта.

86
docs/ru/README.md Normal file
View File

@@ -0,0 +1,86 @@
# FrankenPHP: Современный сервер приложений для PHP
<h1 align="center"><a href="https://frankenphp.dev"><img src="../../frankenphp.png" alt="FrankenPHP" width="600"></a></h1>
**FrankenPHP** — это современный сервер приложений для PHP, построенный на базе веб-сервера [Caddy](https://caddyserver.com/).
FrankenPHP добавляет новые возможности вашим PHP-приложениям благодаря следующим функциям: [_Early Hints_](https://frankenphp.dev/docs/early-hints/), [Worker режим](https://frankenphp.dev/docs/worker/), [Real-time режим](https://frankenphp.dev/docs/mercure/), автоматическая поддержка HTTPS, HTTP/2 и HTTP/3.
FrankenPHP совместим с любыми PHP-приложениями и значительно ускоряет ваши проекты на Laravel и Symfony благодаря их официальной поддержке в worker режиме.
FrankenPHP также может использоваться как автономная Go-библиотека для встраивания PHP в любое приложение с использованием `net/http`.
[**Узнайте больше** на сайте _frankenphp.dev_](https://frankenphp.dev) или из этой презентации:
<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>
## Начало работы
### 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#переменные-окружения).
### Автономный бинарный файл
Если вы предпочитаете не использовать Docker, мы предоставляем автономный бинарный файл FrankenPHP для Linux и macOS, включающий [PHP 8.4](https://www.php.net/releases/8.4/en.php) и большинство популярных PHP-расширений.
Для Windows используйте [WSL](https://learn.microsoft.com/windows/wsl/) для запуска FrankenPHP.
[Скачать FrankenPHP](https://github.com/dunglas/frankenphp/releases) или выполните следующую команду для автоматической установки подходящей версии:
```console
curl https://frankenphp.dev/install.sh | sh
mv frankenphp /usr/local/bin/
```
Для запуска содержимого текущей директории выполните:
```console
frankenphp php-server
```
Вы также можете запускать CLI-скрипты:
```console
frankenphp php-cli /path/to/your/script.php
```
## Документация
- [Worker режим](https://frankenphp.dev/docs/worker/)
- [Поддержка Early Hints (103 HTTP статус код)](https://frankenphp.dev/docs/early-hints/)
- [Real-time режим](https://frankenphp.dev/docs/mercure/)
- [Конфигурация](https://frankenphp.dev/docs/config/)
- [Docker-образы](https://frankenphp.dev/docs/docker/)
- [Деплой в продакшен](https://frankenphp.dev/docs/production/)
- [Оптимизация производительности](https://frankenphp.dev/docs/performance/)
- [Создание автономного PHP-приложений](https://frankenphp.dev/docs/embed/)
- [Создание статических бинарных файлов](https://frankenphp.dev/docs/static/)
- [Компиляция из исходников](https://frankenphp.dev/docs/compile/)
- [Интеграция с Laravel](https://frankenphp.dev/docs/laravel/)
- [Известные проблемы](https://frankenphp.dev/docs/known-issues/)
- [Демо-приложение (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/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)

110
docs/ru/compile.md Normal file
View File

@@ -0,0 +1,110 @@
# Компиляция из исходников
Этот документ объясняет, как создать бинарный файл FrankenPHP, который будет загружать PHP как динамическую библиотеку.
Это рекомендуемый способ.
Альтернативно можно создать [статическую сборку](static.md).
## Установка PHP
FrankenPHP совместим с PHP версии 8.2 и выше.
Сначала [загрузите исходники 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/) для установки
`libiconv`, `bison`, `re2c` и `pkg-config`:
```console
brew install libiconv bison brotli re2c pkg-config
echo 'export PATH="/opt/homebrew/opt/bison/bin:$PATH"' >> ~/.zshrc
```
Затем выполните скрипт configure:
```console
./configure \
--enable-embed=static \
--enable-zts \
--disable-zend-signals \
--disable-opcache-jit \
--enable-static \
--enable-shared=no \
--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-приложения
Теперь можно собрать итоговый бинарный файл:
```console
curl -L https://github.com/dunglas/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
```
### Использование xcaddy
Альтернативно, используйте [xcaddy](https://github.com/caddyserver/xcaddy) для компиляции FrankenPHP с [пользовательскими модулями Caddy](https://caddyserver.com/docs/modules/):
```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 frankenphp.dev/caddy \
--with github.com/dunglas/mercure/caddy \
--with github.com/dunglas/vulcain/caddy
# Добавьте дополнительные модули Caddy здесь
```
> [!TIP]
>
> Если вы используете musl libc (по умолчанию в Alpine 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\'"'`
> (измените значение размера стека в зависимости от требований вашего приложения).

247
docs/ru/config.md Normal file
View File

@@ -0,0 +1,247 @@
# Конфигурация
FrankenPHP, Caddy, а также модули Mercure и Vulcain могут быть настроены с использованием [конфигурационных форматов, поддерживаемых Caddy](https://caddyserver.com/docs/getting-started#your-first-config).
В [Docker-образах](docker.md) файл `Caddyfile` находится по пути `/etc/frankenphp/Caddyfile`.
Статический бинарный файл будет искать `Caddyfile` в директории запуска.
PHP можно настроить [с помощью файла `php.ini`](https://www.php.net/manual/en/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
# Для production:
RUN cp $PHP_INI_DIR/php.ini-production $PHP_INI_DIR/php.ini
# Или для development:
RUN cp $PHP_INI_DIR/php.ini-development $PHP_INI_DIR/php.ini
```
Установка FrankenPHP (.rpm или .deb):
- 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.ini-production` или `php.ini-development`, предоставленных [в исходниках PHP](https://github.com/php/php-src/).
## Конфигурация Caddyfile
[HTTP-директивы](https://caddyserver.com/docs/caddyfile/concepts#directives) `php_server` или `php` могут быть использованы в блоках сайта для обработки вашего PHP-приложения.
Минимальный пример:
```caddyfile
localhost {
# Включить сжатие (опционально)
encode zstd br gzip
# Выполнять PHP-файлы в текущей директории и обслуживать ресурсы
php_server
}
```
Вы также можете явно настроить FrankenPHP с помощью глобальной опции:
[Глобальная опция](https://caddyserver.com/docs/caddyfile/concepts#global-options) `frankenphp` может быть использована для настройки FrankenPHP.
```caddyfile
{
frankenphp {
num_threads <num_threads> # Указывает количество потоков PHP. По умолчанию: 2x от числа доступных CPU.
worker {
file <path> # Указывает путь к worker-скрипту.
num <num> # Указывает количество потоков PHP. По умолчанию: 2x от числа доступных CPU.
env <key> <value> # Устанавливает дополнительную переменную окружения. Можно указать несколько раз для разных переменных.
watch <path> # Указывает путь для отслеживания изменений файлов.Можно указать несколько раз для разных путей.
}
}
}
# ...
```
В качестве альтернативы можно использовать однострочную краткую форму для опции `worker`:
```caddyfile
{
frankenphp {
worker <file> <num>
}
}
# ...
```
Вы также можете определить несколько workers, если обслуживаете несколько приложений на одном сервере:
```caddyfile
app.example.com {
php_server {
root /path/to/app/public
worker index.php <num>
}
}
other.example.com {
php_server {
root /path/to/other/public
worker index.php <num>
}
}
# ...
```
Использование директивы `php_server` — это то, что нужно в большинстве случаев. Однако если требуется полный контроль, вы можете использовать более низкоуровневую директиву `php`:
Использование директивы `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> # Указывает путь к worker-скрипту, может быть относительным к корню php_server
num <num> # Указывает количество потоков PHP. По умолчанию: 2x от числа доступных CPU.
name <name> # Устанавливает имя для worker, используемое в логах и метриках. По умолчанию: абсолютный путь к файлу worker. Всегда начинается с m# при определении в блоке php_server.
watch <path> # Указывает путь для отслеживания изменений файлов. Можно указать несколько раз для разных путей.
env <key> <value> # Устанавливает дополнительную переменную окружения. Можно указать несколько раз для разных переменных. Переменные окружения для этого worker также наследуются от родительского php_server, но могут быть переопределены здесь.
}
worker <other_file> <num> # Также можно использовать краткую форму как в глобальном блоке frankenphp.
}
```
### Отслеживание изменений файлов
Поскольку workers запускают ваше приложение только один раз и держат его в памяти, изменения в PHP-файлах не будут применяться сразу.
Для разработки можно настроить перезапуск workers при изменении файлов с помощью директивы `watch`:
```caddyfile
{
frankenphp {
worker {
file /path/to/app/public/worker.php
watch
}
}
}
```
Если директория для `watch` не указана, по умолчанию будет использоваться путь `./**/*.{php,yaml,yml,twig,env}`,
который отслеживает все файлы с расширениями `.php`, `.yaml`, `.yml`, `.twig` и `.env` в директории, где был запущен процесс FrankenPHP, и во всех её поддиректориях. Вы также можете указать одну или несколько директорий с использованием [шаблона имён файлов](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 # отслеживает файлы с расширением .php в /path/to/app
watch /path/to/app/**/*.php # отслеживает PHP-файлы в /path/to/app и поддиректориях
watch /path/to/app/**/*.{php,twig} # отслеживает PHP и Twig-файлы в /path/to/app и поддиректориях
}
}
}
```
- Шаблон `**` указывает на рекурсивное отслеживание.
- Директории могут быть указаны относительно директории запуска FrankenPHP.
- Если у вас определено несколько workers, все они будут перезапущены при изменении файлов.
- Избегайте отслеживания файлов, создаваемых во время выполнения (например, логов), так как это может вызвать нежелательные перезапуски.
Механизм отслеживания файлов основан на [e-dant/watcher](https://github.com/e-dant/watcher).
### Полный дуплекс (HTTP/1)
При использовании HTTP/1.x можно включить режим полного дуплекса, чтобы разрешить запись ответа до завершения чтения тела запроса (например, для WebSocket, Server-Sent Events и т.д.).
Эта опция включается вручную и должна быть добавлена в глобальные настройки `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` без его изменения:
- `SERVER_NAME`: изменение [адресов для прослушивания](https://caddyserver.com/docs/caddyfile/concepts#addresses); предоставленные хостнеймы также будут использованы для генерации TLS-сертификата.
- `CADDY_GLOBAL_OPTIONS`: добавление [глобальных опций](https://caddyserver.com/docs/caddyfile/options).
- `FRANKENPHP_CONFIG`: добавление конфигурации в директиву `frankenphp`.
Как и для FPM и CLI SAPIs, переменные окружения по умолчанию доступны в суперглобальной переменной `$_SERVER`.
Значение `S` в [директиве PHP `variables_order`](https://www.php.net/manual/en/ini.core.php#ini.variables-order) всегда эквивалентно `ES`, независимо от того, где расположена `E` в этой директиве.
## Конфигурация PHP
Для загрузки [дополнительных конфигурационных файлов PHP](https://www.php.net/manual/en/configuration.file.php#configuration.file.scan) можно использовать переменную окружения `PHP_INI_SCAN_DIR`.
Если она установлена, PHP загрузит все файлы с расширением `.ini`, находящиеся в указанных директориях.
## Включение режима отладки
При использовании 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
```

201
docs/ru/docker.md Normal file
View File

@@ -0,0 +1,201 @@
# Создание кастомных Docker-образов
[Docker-образы FrankenPHP](https://hub.docker.com/r/dunglas/frankenphp) основаны на [официальных PHP-образах](https://hub.docker.com/_/php/). Доступны варианты для Debian и Alpine Linux для популярных архитектур. Рекомендуется использовать Debian-варианты.
Доступны версии для PHP 8.2, 8.3 и 8.4.
Теги следуют следующему шаблону: `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
# Для сборки FrankenPHP необходимо включить CGO
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 frankenphp.dev=./ \
--with frankenphp.dev/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
```
Образ `builder`, предоставляемый FrankenPHP, содержит скомпилированную версию `libphp`.
[Образы builder](https://hub.docker.com/r/dunglas/frankenphp/tags?name=builder) доступны для всех версий FrankenPHP и PHP, как для Debian, так и для Alpine.
> [!TIP]
>
> Если вы используете Alpine Linux и Symfony, возможно, потребуется [увеличить размер стека](compile.md#использование-xcaddy).
## Активировать worker режим по умолчанию
Установите переменную окружения `FRANKENPHP_CONFIG`, чтобы запускать FrankenPHP с Worker-скриптом:
```dockerfile
FROM dunglas/frankenphp
# ...
ENV FRANKENPHP_CONFIG="worker ./public/index.php"
```
## Использование тома в разработке
Для удобной разработки с FrankenPHP смонтируйте директорию с исходным кодом приложения на хосте как том в Docker-контейнере:
```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
# закомментируйте следующую строку в продакшн среде, она позволяет получать удобочитаемые логи в режиме разработки
tty: true
# Томы, необходимые для сертификатов и конфигурации Caddy
volumes:
caddy_data:
caddy_config:
```
## Запуск под обычным пользователем
FrankenPHP поддерживает запуск под обычным пользователем в Docker.
Пример `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; \
# Дайте права на запись для /data/caddy и /config/caddy
chown -R ${USER}:${USER} /data/caddy && chown -R ${USER}:${USER} /config/caddy
USER ${USER}
```
### Запуск без дополнительных прав
Даже при запуске без root-прав, FrankenPHP требуется возможность `CAP_NET_BIND_SERVICE` для привязки веб-сервера к зарезервированным портам (80 и 443).
Если вы открываете доступ к FrankenPHP на непривилегированном порту (1024 и выше), можно запустить веб-сервер от имени обычного пользователя без необходимости предоставления дополнительных возможностей:
```dockerfile
FROM dunglas/frankenphp
ARG USER=appuser
RUN \
# Для Alpine-дистрибутивов используйте команду "adduser -D ${USER}"
useradd ${USER}; \
# Удалите стандартные возможности
setcap -r /usr/local/bin/frankenphp; \
# Дайте права на запись для /data/caddy и /config/caddy
chown -R ${USER}:${USER} /data/caddy && chown -R ${USER}:${USER} /config/caddy
USER ${USER}
```
Затем установите переменную окружения `SERVER_NAME`, чтобы использовать непривилегированный порт.
Пример: `:8000`.
## Обновления
Docker-образы обновляются:
- при выпуске новой версии;
- ежедневно в 4 утра UTC, если доступны новые версии официальных PHP-образов.
## Версии для разработки
Версии для разработки доступны в Docker-репозитории [`dunglas/frankenphp-dev`](https://hub.docker.com/repository/docker/dunglas/frankenphp-dev).
Сборка запускается автоматически при каждом коммите в основную ветку GitHub-репозитория
Теги с префиксом `latest*` указывают на актуальное состояние ветки `main`.
Также доступны теги в формате `sha-<git-commit-hash>`.

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

@@ -0,0 +1,21 @@
# Early Hints
FrankenPHP изначально поддерживает [Early Hints (103 HTTP статус код)](https://developer.chrome.com/blog/early-hints/).
Использование 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;
```
Early Hints поддерживается как в обычном, так и в [worker режиме](worker.md).

140
docs/ru/embed.md Normal file
View File

@@ -0,0 +1,140 @@
# PHP-приложения как автономные бинарные файлы
FrankenPHP позволяет встраивать исходный код и ресурсы PHP-приложений в статический автономный бинарный файл.
Благодаря этой функции PHP-приложения могут распространяться как автономные бинарные файлы, которые содержат само приложение, интерпретатор PHP и Caddy — веб-сервер уровня продакшн.
Подробнее об этой функции [в презентации Кевина на SymfonyCon 2023](https://dunglas.dev/2023/12/php-and-symfony-apps-as-standalone-binaries/).
Для встраивания Laravel-приложений ознакомьтесь с [документацией](laravel.md#laravel-приложения-как-автономные-бинарные-файлы).
## Подготовка приложения
Перед созданием автономного бинарного файла убедитесь, что ваше приложение готово для встраивания.
Например, вам может понадобиться:
- Установить продакшн-зависимости приложения.
- Сгенерировать автозагрузчик.
- Включить продакшн-режим приложения (если он есть).
- Удалить ненужные файлы, такие как `.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
# Удалить тесты и другие ненужные файлы
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
# Скопировать приложение
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, используйте предоставленный скрипт:
```console
git clone https://github.com/dunglas/frankenphp
cd frankenphp
EMBED=/path/to/your/app ./build-static.sh
```
Итоговый бинарный файл будет находиться в директории `dist/` под именем `frankenphp-<os>-<arch>`.
## Использование бинарного файла
Готово! Файл `my-app` (или `dist/frankenphp-<os>-<arch>` для других ОС) содержит ваше автономное приложение.
Для запуска веб-приложения выполните:
```console
./my-app php-server
```
Если ваше приложение содержит [worker-скрипт](worker.md), запустите его следующим образом:
```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-расширения
По умолчанию скрипт собирает расширения, указанные в `composer.json` вашего проекта.
Если файла `composer.json` нет, собираются стандартные расширения, как указано в [документации по статической сборке](static.md).
Чтобы настроить список расширений, используйте переменную окружения `PHP_EXTENSIONS`.
## Настройка сборки
[Ознакомьтесь с документацией по статической сборке](static.md), чтобы узнать, как настроить бинарный файл (расширения, версию PHP и т.д.).
## Распространение бинарного файла
На Linux созданный бинарный файл сжимается с помощью [UPX](https://upx.github.io).
На Mac для уменьшения размера файла перед отправкой его можно сжать. Рекомендуется использовать `xz`.

30
docs/ru/github-actions.md Normal file
View File

@@ -0,0 +1,30 @@
# Использование GitHub Actions
Этот репозиторий автоматически собирает и публикует Docker-образы в [Docker Hub](https://hub.docker.com/r/dunglas/frankenphp) для каждого одобренного pull request или вашего собственного форка после настройки.
## Настройка GitHub Actions
В настройках репозитория, в разделе "Secrets", добавьте следующие секреты:
- `REGISTRY_LOGIN_SERVER`: Docker-реестр, который будет использоваться (например, `docker.io`).
- `REGISTRY_USERNAME`: Имя пользователя для входа в реестр (например, `dunglas`).
- `REGISTRY_PASSWORD`: Пароль для входа в реестр (например, токен доступа).
- `IMAGE_NAME`: Имя образа (например, `dunglas/frankenphp`).
## Сборка и загрузка образа
1. Создайте Pull Request или выполните push в ваш форк.
2. GitHub Actions соберёт образ и выполнит тесты.
3. Если сборка пройдёт успешно, образ будет отправлен в реестр с тегом `pr-x`, где `x` — номер PR.
## Развёртывание образа
1. После слияния Pull Request GitHub Actions выполнит повторные тесты и соберёт новый образ.
2. Если сборка пройдёт успешно, тег `main` будет обновлён в Docker-реестре.
## Релизы
1. Создайте новый тег в репозитории.
2. GitHub Actions соберёт образ и выполнит тесты.
3. Если сборка пройдёт успешно, образ будет отправлен в реестр с именем тега (например, `v1.2.3` и `v1.2` будут созданы).
4. Также будет обновлён тег `latest`.

140
docs/ru/known-issues.md Normal file
View File

@@ -0,0 +1,140 @@
# Известные проблемы
## Неподдерживаемые расширения 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) начинает работать медленно через некоторое время. Решение — кэшировать результаты для каждого User-Agent, например, с помощью [APCu](https://www.php.net/manual/en/book.apcu.php), так как они статичны.
## Автономные бинарные файлы и образы на базе Alpine
Автономные бинарные файлы и образы на базе Alpine (`dunglas/frankenphp:*-alpine`) используют [musl libc](https://musl.libc.org/) вместо [glibc](https://www.etalabs.net/compare_libcs.html) для уменьшения размера бинарных файлов. Это может вызвать проблемы совместимости. В частности, флаг `GLOB_BRACE` в функции glob [не поддерживается](https://www.php.net/manual/en/function.glob.php).
## Использование `https://127.0.0.1` с Docker
По умолчанию FrankenPHP генерирует TLS-сертификат для `localhost`, что является самым простым и рекомендуемым вариантом для локальной разработки.
Если вы всё же хотите использовать `127.0.0.1`, настройте генерацию сертификата, указав в переменной окружения `SERVER_NAME` значение `127.0.0.1`.
Однако этого может не хватить при использовании Docker из-за [особенностей его сетевой системы](https://docs.docker.com/network/). Возможна ошибка TLS вида:
`curl: (35) LibreSSL/3.3.6: error:1404B438:SSL routines:ST_CONNECT:tlsv1 alert internal error`.
Если вы используете Linux, можно воспользоваться [host-драйвером](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
```
Host-драйвер не поддерживается на Mac и Windows. На этих платформах нужно определить IP-адрес контейнера и включить его в `SERVER_NAME`.
Выполните команду `docker network inspect bridge`, найдите ключ `Containers` и определите последний присвоенный IP из `IPv4Address`. Увеличьте его на единицу. Если контейнеров нет, первый IP обычно `172.17.0.2`.
Включите этот IP в переменную окружения `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/dunglas/frankenphp/issues/483#issuecomment-1899890915) по двум причинам:
- Composer не знает, как вызывать бинарный файл FrankenPHP;
- Composer может добавлять настройки PHP через флаг `-d`, который FrankenPHP пока не поддерживает.
Решение — создать shell-скрипт в `/usr/local/bin/php`, который удаляет неподдерживаемые параметры и вызывает 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]
> Веб и CLI контексты могут иметь разные настройки.
> Запустите `openssl_get_cert_locations()` в нужном контексте.
[Сертификаты CA, извлечённые из Mozilla, можно скачать с сайта curl](https://curl.se/docs/caextract.html).
Кроме того, многие дистрибутивы, такие как Debian, Ubuntu и Alpine, предоставляют пакеты `ca-certificates`, содержащие эти сертификаты.
Также можно использовать переменные `SSL_CERT_FILE` и `SSL_CERT_DIR`, чтобы указать OpenSSL, где искать сертификаты CA:
```console
# Установите переменные окружения для TLS-сертификатов
export SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt
export SSL_CERT_DIR=/etc/ssl/certs
```

176
docs/ru/laravel.md Normal file
View File

@@ -0,0 +1,176 @@
# Laravel
## Docker
Запустить [Laravel](https://laravel.com) веб-приложение с FrankenPHP очень просто: достаточно смонтировать проект в директорию `/app` официального Docker-образа.
Выполните эту команду из корневой директории вашего Laravel-приложения:
```console
docker run -p 80:80 -p 443:443 -p 443:443/udp -v $PWD:/app dunglas/frankenphp
```
И наслаждайтесь!
## Локальная установка
Вы также можете запустить ваши Laravel-проекты с FrankenPHP на локальной машине:
1. [Скачайте бинарный файл для вашей системы](README.md#автономный-бинарный-файл)
2. Добавьте следующую конфигурацию в файл с именем `Caddyfile` в корневой директории вашего Laravel-проекта:
```caddyfile
{
frankenphp
}
# Доменное имя вашего сервера
localhost {
# Укажите веб-корень как директорию public/
root public/
# Включите сжатие (опционально)
encode zstd br gzip
# Выполняйте PHP-файлы из директории public/ и обслуживайте статические файлы
php_server
}
```
3. Запустите FrankenPHP из корневой директории вашего Laravel-проекта: `frankenphp run`
## Laravel Octane
Octane можно установить с помощью менеджера пакетов Composer:
```console
composer require laravel/octane
```
После установки Octane выполните Artisan-команду `octane:install`, которая создаст конфигурационный файл Octane в вашем приложении:
```console
php artisan octane:install --server=frankenphp
```
Сервер Octane можно запустить с помощью Artisan-команды `octane:frankenphp`:
```console
php artisan octane:frankenphp
```
Команда `octane:frankenphp` поддерживает следующие опции:
- `--host`: IP-адрес, к которому должен привязаться сервер (по умолчанию: `127.0.0.1`)
- `--port`: Порт, на котором сервер будет доступен (по умолчанию: `8000`)
- `--admin-port`: Порт, на котором будет доступен административный сервер (по умолчанию: `2019`)
- `--workers`: Количество worker-скриптов для обработки запросов (по умолчанию: `auto`)
- `--max-requests`: Количество запросов, обрабатываемых перед перезагрузкой сервера (по умолчанию: `500`)
- `--caddyfile`: Путь к файлу `Caddyfile` FrankenPHP (по умолчанию: [stubbed `Caddyfile` в Laravel Octane](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`: Установить уровень логирования, используя встроенный логгер Caddy
> [!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
# Скопируйте ваше приложение
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-приложения-как-автономные-бинарные-файлы).
Затем, чтобы запустить FrankenPHP в worker-режиме через Octane, выполните:
```console
PATH="$PWD:$PATH" frankenphp php-cli artisan octane:frankenphp
```
> [!CAUTION]
> Для работы команды автономный бинарник **обязательно** должен быть назван `frankenphp`, так как Octane требует наличия программы с именем `frankenphp` в PATH.

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

@@ -0,0 +1,12 @@
# Real-time режим
FrankenPHP поставляется с встроенным хабом [Mercure](https://mercure.rocks)!
Mercure позволяет отправлять события в режиме реального времени на все подключённые устройства: они мгновенно получат JavaScript-событие.
Не требуются JS-библиотеки или SDK!
![Mercure](../mercure-hub.png)
Чтобы включить хаб Mercure, обновите `Caddyfile` в соответствии с инструкциями [на сайте Mercure](https://mercure.rocks/docs/hub/config).
Для отправки обновлений Mercure из вашего кода мы рекомендуем использовать [Symfony Mercure Component](https://symfony.com/components/Mercure) (для его использования не требуется полный стек Symfony).

15
docs/ru/metrics.md Normal file
View File

@@ -0,0 +1,15 @@
# Метрики
При включении [метрик Caddy](https://caddyserver.com/docs/metrics) FrankenPHP предоставляет следующие метрики:
- `frankenphp_[worker]_total_workers`: Общее количество worker-скриптов.
- `frankenphp_[worker]_busy_workers`: Количество worker-скриптов, которые в данный момент обрабатывают запрос.
- `frankenphp_[worker]_worker_request_time`: Время, затраченное всеми worker-скриптами на обработку запросов.
- `frankenphp_[worker]_worker_request_count`: Количество запросов, обработанных всеми worker-скриптами.
- `frankenphp_[worker]_ready_workers`: Количество worker-скриптов, которые вызвали `frankenphp_handle_request` хотя бы один раз.
- `frankenphp_[worker]_worker_crashes`: Количество случаев неожиданного завершения worker-скриптов.
- `frankenphp_[worker]_worker_restarts`: Количество случаев, когда worker-скрипт был перезапущен целенаправленно.
- `frankenphp_total_threads`: Общее количество потоков PHP.
- `frankenphp_busy_threads`: Количество потоков PHP, которые в данный момент обрабатывают запрос (работающие worker-скрипты всегда используют поток).
Для метрик worker-скриптов плейсхолдер `[worker]` заменяется на путь к Worker-скрипту, указанному в Caddyfile.

101
docs/ru/performance.md Normal file
View File

@@ -0,0 +1,101 @@
# Производительность
По умолчанию FrankenPHP предлагает хороший баланс между производительностью и удобством использования.
Однако, используя подходящую конфигурацию, можно существенно улучшить производительность.
## Количество потоков и worker-скриптов
По умолчанию FrankenPHP запускает потоков и worker-скриптов (в worker режиме) вдвое больше, чем количество доступных процессорных ядер.
Оптимальные значения зависят от структуры вашего приложения, его функциональности и аппаратного обеспечения.
Мы настоятельно рекомендуем изменить эти значения.
Чтобы найти подходящие параметры, лучше всего провести нагрузочные тесты, имитирующие реальный трафик.
Хорошими инструментами для этого являются [k6](https://k6.io) и [Gatling](https://gatling.io).
Чтобы настроить количество потоков, используйте опцию `num_threads` в директивах `php_server` и `php`.
Для изменения количества worker-скриптов используйте опцию `num` в секции `worker` директивы `frankenphp`.
## Worker режим
Включение [Worker режима](worker.md) значительно улучшает производительность,
но ваше приложение должно быть адаптировано для совместимости с этим режимом:
необходимо создать worker-скрипт и убедиться, что приложение не имеет утечек памяти.
## Избегайте использования musl
Статические бинарники, которые мы предоставляем, а также Alpine Linux-вариант официальных Docker-образов используют [библиотеку musl libc](https://musl.libc.org).
Известно, что PHP [значительно медленнее работает](https://gitlab.alpinelinux.org/alpine/aports/-/issues/14381) с этой библиотекой по сравнению с традиционной GNU libc, особенно при компиляции в ZTS режиме (потокобезопасный режим), который требуется для FrankenPHP.
Кроме того, [некоторые ошибки проявляются исключительно при использовании musl](https://github.com/php/php-src/issues?q=sort%3Aupdated-desc+is%3Aissue+is%3Aopen+label%3ABug+musl).
В производственной среде настоятельно рекомендуется использовать glibc.
Это можно сделать, используя Debian Docker-образы (по умолчанию) и [компилируя FrankenPHP из исходников](compile.md).
В качестве альтернативы мы предоставляем статические бинарники, скомпилированные с [аллокатором mimalloc](https://github.com/microsoft/mimalloc), что делает FrankenPHP+musl быстрее (но всё же медленнее, чем FrankenPHP+glibc).
## Настройка среды выполнения Go
FrankenPHP написан на языке Go.
В большинстве случаев среда выполнения Go не требует особой настройки, но в некоторых ситуациях специфическая конфигурация может улучшить производительность.
Рекомендуется установить переменную окружения `GODEBUG` в значение `cgocheck=0` (по умолчанию в Docker-образах FrankenPHP).
Если вы запускаете FrankenPHP в контейнерах (Docker, Kubernetes, LXC и т.д.) и ограничиваете доступную память, установите переменную окружения `GOMEMLIMIT` в значение доступного объёма памяти.
Для более детальной информации ознакомьтесь с [документацией Go по этой теме](https://pkg.go.dev/runtime#hdr-Environment_Variables).
## `file_server`
По умолчанию директива `php_server` автоматически настраивает файловый сервер для обслуживания статических файлов (ресурсов), хранящихся в корневой директории.
Эта функция удобна, но имеет издержки. Чтобы отключить её, используйте следующую конфигурацию:
```caddyfile
php_server {
file_server off
}
```
## Плейсхолдеры
Вы можете использовать [плейсхолдеры](https://caddyserver.com/docs/conventions#placeholders) в директивах `root` и `env`.
Однако это предотвращает кеширование значений и существенно снижает производительность.
По возможности избегайте использования плейсхолдеров в этих директивах.
## `resolve_root_symlink`
По умолчанию, если корневая директория документа является символьной ссылкой, она автоматически разрешается FrankenPHP (это необходимо для корректной работы PHP).
Если корневая директория документа не является символьной ссылкой, вы можете отключить эту функцию.
```caddyfile
php_server {
resolve_root_symlink false
}
```
Это улучшит производительность, если директива `root` содержит [плейсхолдеры](https://caddyserver.com/docs/conventions#placeholders).
В остальных случаях прирост производительности будет минимальным.
## Логи
Логирование, безусловно, полезно, но требует операций ввода-вывода и выделения памяти, что значительно снижает производительность.
Убедитесь, что вы [правильно настроили уровень логирования](https://caddyserver.com/docs/caddyfile/options#log) и логируете только необходимое.
## Производительность PHP
FrankenPHP использует официальный интерпретатор PHP.
Все стандартные оптимизации производительности PHP применимы к FrankenPHP.
В частности:
- убедитесь, что [OPcache](https://www.php.net/manual/en/book.opcache.php) установлен, включён и настроен должным образом;
- включите [оптимизацию автозагрузки Composer](https://getcomposer.org/doc/articles/autoloader-optimization.md);
- убедитесь, что кеш `realpath` достаточно велик для нужд вашего приложения;
- используйте [предварительную загрузку](https://www.php.net/manual/en/opcache.preloading.php).
Для более детальной информации ознакомьтесь с [документацией Symfony о производительности](https://symfony.com/doc/current/performance.html) (большинство советов полезны даже если вы не используете Symfony).

125
docs/ru/production.md Normal file
View File

@@ -0,0 +1,125 @@
# Деплой в продакшен
В этом руководстве мы рассмотрим, как развернуть PHP-приложение на одном сервере с использованием Docker Compose.
Если вы используете Symfony, рекомендуется прочитать раздел "[Deploy in production](https://github.com/dunglas/symfony-docker/blob/main/docs/production.md)" документации проекта Symfony Docker (в котором используется FrankenPHP).
Если вы используете API Platform (который также работает с FrankenPHP), ознакомьтесь с [документацией по деплою этого фреймворка](https://api-platform.com/docs/deployment/).
## Подготовка приложения
Сначала создайте файл `Dockerfile` в корневой директории вашего PHP-проекта:
```dockerfile
FROM dunglas/frankenphp
# Замените "your-domain-name.example.com" на ваш домен
ENV SERVER_NAME=your-domain-name.example.com
# Если вы хотите отключить HTTPS, используйте вместо этого:
#ENV SERVER_NAME=:80
# Включите настройки PHP для продакшн
RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
# Скопируйте файлы PHP вашего проекта в публичную директорию
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-сервер.
Если у вас уже есть Linux-сервер с установленным Docker, вы можете сразу перейти к [следующему разделу](#настройка-доменного-имени).
В противном случае, используйте [эту ссылку](https://m.do.co/c/5d8aabe3ab80), чтобы получить $200 на баланс, создайте аккаунт, затем нажмите "Create a Droplet".
Перейдите во вкладку "Marketplace" в разделе "Choose an image" и найдите приложение "Docker". Это создаст сервер на Ubuntu с установленными Docker и Docker Compose.
Для тестов подойдут самые дешёвые тарифы. Для реального продакшна выберите тариф из раздела "general purpose" в зависимости от ваших потребностей.
![Деплой FrankenPHP на DigitalOcean с Docker](../digitalocean-droplet.png)
После этого подключитесь к серверу через SSH:
```console
ssh root@<droplet-ip>
```
## Настройка доменного имени
В большинстве случаев вам потребуется связать доменное имя с вашим сайтом.
Создайте запись DNS типа `A`, указывающую на IP вашего сервера:
```dns
your-domain-name.example.com. IN A 207.154.233.113
```
Пример настройки через DigitalOcean ("Networking" > "Domains"):
![Настройка DNS в DigitalOcean](../digitalocean-dns.png)
> [!NOTE]
>
> Let's Encrypt, сервис, используемый FrankenPHP для автоматической генерации TLS-сертификатов, не поддерживает использование IP-адресов. Для работы необходим домен.
## Деплой
Скопируйте ваш проект на сервер с помощью `git clone`, `scp` или любого другого инструмента.
Если вы используете GitHub, настройте [ключи развёртывания](https://docs.github.com/en/free-pro-team@latest/developers/overview/managing-deploy-keys#deploy-keys).
Пример с использованием Git:
```console
git clone git@github.com:<username>/<project-name>.git
```
Перейдите в директорию проекта и запустите приложение в режиме продакшн:
```console
docker compose up -d --wait
```
Сервер готов, а HTTPS-сертификат был автоматически сгенерирован. Перейдите на `https://your-domain-name.example.com` и наслаждайтесь!
> [!CAUTION]
>
> Docker может кэшировать слои. Убедитесь, что вы используете актуальную сборку, или используйте опцию `--no-cache` для предотвращения проблем с кэшем.
## Деплой на несколько узлов
Если вам нужно развернуть приложение на кластер машин, используйте [Docker Swarm](https://docs.docker.com/engine/swarm/stack-deploy/), который совместим с предоставленными файлами Compose.
Для деплоя на Kubernetes ознакомьтесь с [Helm-чартом API Platform](https://api-platform.com/docs/deployment/kubernetes/), который использует FrankenPHP.

100
docs/ru/static.md Normal file
View File

@@ -0,0 +1,100 @@
# Создание статических бинарных файлов
Вместо использования локальной установки библиотеки PHP, можно создать статическую сборку FrankenPHP благодаря проекту [static-php-cli](https://github.com/crazywhalecc/static-php-cli) (несмотря на название, проект поддерживает все SAPI, а не только CLI).
С помощью этого метода создаётся единый переносимый бинарник, который включает PHP-интерпретатор, веб-сервер Caddy и FrankenPHP!
FrankenPHP также поддерживает [встраивание PHP-приложений в статический бинарный файл](embed.md).
## Linux
Мы предоставляем Docker-образ для сборки статического бинарника для Linux:
```console
docker buildx bake --load static-builder
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
```
Созданный статический бинарный файл называется `frankenphp` и будет доступен в текущей директории.
Чтобы собрать статический бинарный файл без Docker, используйте инструкции для macOS — они подходят и для Linux.
### Дополнительные расширения
По умолчанию компилируются самые популярные PHP-расширения.
Чтобы уменьшить размер бинарного файла и сократить возможные векторы атак, можно указать список расширений, которые следует включить в сборку, используя Docker-аргумент `PHP_EXTENSIONS`.
Например, выполните следующую команду, чтобы собрать только расширение `opcache`:
```console
docker buildx bake --load --set static-builder.args.PHP_EXTENSIONS=opcache,pdo_sqlite static-builder
# ...
```
Чтобы добавить библиотеки, расширяющие функциональность включённых расширений, используйте Docker-аргумент `PHP_EXTENSION_LIBS`:
```console
docker buildx bake \
--load \
--set static-builder.args.PHP_EXTENSIONS=gd \
--set static-builder.args.PHP_EXTENSION_LIBS=libjpeg,libwebp \
static-builder
```
### Дополнительные модули Caddy
Чтобы добавить дополнительные модули Caddy или передать аргументы в [xcaddy](https://github.com/caddyserver/xcaddy), используйте Docker-аргумент `XCADDY_ARGS`:
```console
docker buildx bake \
--load \
--set static-builder.args.XCADDY_ARGS="--with github.com/darkweak/souin/plugins/caddy --with github.com/dunglas/caddy-cbrotli --with github.com/dunglas/mercure/caddy --with github.com/dunglas/vulcain/caddy" \
static-builder
```
В этом примере добавляются модуль HTTP-кэширования [Souin](https://souin.io) для Caddy, а также модули [cbrotli](https://github.com/dunglas/caddy-cbrotli), [Mercure](https://mercure.rocks) и [Vulcain](https://vulcain.rocks).
> [!TIP]
>
> Модули cbrotli, Mercure и Vulcain включены по умолчанию, если `XCADDY_ARGS` пуст или не установлен.
> Если вы изменяете значение `XCADDY_ARGS`, добавьте их явно, если хотите включить их в сборку.
См. также, как [настроить сборку](#настройка-сборки).
### Токен GitHub
Если вы достигли лимита запросов к API GitHub, задайте личный токен доступа GitHub в переменной окружения `GITHUB_TOKEN`:
```console
GITHUB_TOKEN="xxx" docker --load buildx bake static-builder
# ...
```
## macOS
Запустите следующий скрипт, чтобы создать статический бинарный файл для macOS (должен быть установлен [Homebrew](https://brew.sh/)):
```console
git clone https://github.com/dunglas/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/en/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) заменяет musl's mallocng на [mimalloc](https://github.com/microsoft/mimalloc) для повышения производительности
- `RELEASE`: (только для мейнтейнеров) если задано, бинарник будет загружен на GitHub

159
docs/ru/worker.md Normal file
View File

@@ -0,0 +1,159 @@
# Worker режим в FrankenPHP
Загрузите приложение один раз и держите его в памяти.
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
```
### Автономный бинарный файл
Используйте опцию `--worker` команды `php-server`, чтобы обслуживать содержимое текущей директории через worker-скрипт:
```console
frankenphp php-server --worker /path/to/your/worker/script.php
```
Если ваше PHP-приложение [встроено в бинарник](embed.md), вы можете добавить пользовательский `Caddyfile` в корневую директорию приложения.
Он будет использоваться автоматически.
Также можно настроить [автоматический перезапуск worker-скрипта при изменении файлов](config.md#отслеживание-изменений-файлов) с помощью опции `--watch`.
Следующая команда выполнит перезапуск, если будет изменён любой файл с расширением `.php` в директории `/path/to/your/app/` или её поддиректориях:
```console
frankenphp php-server --worker /path/to/your/worker/script.php --watch="/path/to/your/app/**/*.php"
```
## Symfony Runtime
Worker режим FrankenPHP поддерживается компонентом [Symfony Runtime](https://symfony.com/doc/current/components/runtime.html).
Чтобы запустить любое Symfony-приложение в worker режиме, установите пакет FrankenPHP для [PHP Runtime](https://github.com/php-runtime/runtime):
```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();
```
Запустите приложение, настроив worker-скрипт с помощью переменной окружения `FRANKENPHP_CONFIG`:
```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
```
## Настройка количества worker-скриптов
По умолчанию запускается по 2 worker-скрипта на каждый CPU.
Вы можете задать своё значение:
```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-скрипта после обработки определённого количества запросов.
В предыдущем примере максимальное количество запросов задаётся с помощью переменной окружения `MAX_REQUESTS`.
### Сбои worker-скрипта
Если worker-скрипт завершится с ненулевым кодом выхода, FrankenPHP перезапустит его с использованием экспоненциальной задержки.
Если worker-скрипт проработает дольше, чем время последней задержки \* 2, он будет считаться стабильным, и задержка сбросится.
Однако, если worker-скрипт продолжает завершаться с ненулевым кодом выхода в течение короткого промежутка времени (например, из-за опечатки в коде), FrankenPHP завершит работу с ошибкой: `too many consecutive failures`.
## Поведение суперглобальных переменных
[PHP суперглобальные переменные](https://www.php.net/manual/en/language.variables.superglobals.php) (`$_SERVER`, `$_ENV`, `$_GET` и т.д.) ведут себя следующим образом:
- до первого вызова `frankenphp_handle_request()` суперглобальные переменные содержат значения, связанные с самим worker-скриптом
- во время и после вызова `frankenphp_handle_request()` суперглобальные переменные содержат значения, сгенерированные на основе обработанного HTTP-запроса, каждый вызов изменяет значения суперглобальных переменных
Чтобы получить доступ к суперглобальным переменным worker-скрипта внутри колбэка, необходимо скопировать их и импортировать копию в область видимости колбэка:
```php
<?php
// Копирование $_SERVER worker-скрипта перед первым вызовом frankenphp_handle_request()
$workerServer = $_SERVER;
$handler = static function () use ($workerServer) {
var_dump($_SERVER); // $_SERVER для запроса
var_dump($workerServer); // $_SERVER worker-скрипта
};
// ...
```

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